From 8535cf6370810f39a6fcec91dd3af2911ff00b75 Mon Sep 17 00:00:00 2001 From: Terebov_Maksim Date: Wed, 19 Feb 2025 18:59:50 +0300 Subject: [PATCH] =?UTF-8?q?day2=5Fcommit=5F1=5Fxml=5F=D0=B7123123213?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 122 +----------------- app/build.gradle.kts | 67 +++++----- .../java/ru/myitschool/work/api/ApiModule.kt | 24 +++- .../java/ru/myitschool/work/api/ApiService.kt | 21 +-- .../java/ru/myitschool/work/core/Constants.kt | 2 +- .../myitschool/work/ui/Main/AdminFragment.kt | 2 +- .../myitschool/work/ui/login/LoginFragment.kt | 29 ++--- .../work/ui/login/LoginViewModel.kt | 66 ++++------ app/src/main/res/layout/fragment_login.xml | 9 +- app/src/main/res/layout/fragment_main.xml | 9 +- app/src/main/res/layout/fragment_qr_scan.xml | 1 + .../res/layout/fragment_qr_scan_result.xml | 6 +- app/src/main/res/values-en-rGB/strings.xml | 2 + app/src/main/res/values/strings.xml | 7 +- app/src/main/res/values/strings_qr.xml | 2 +- 15 files changed, 126 insertions(+), 243 deletions(-) diff --git a/README.md b/README.md index ef91da2..a792b29 100644 --- a/README.md +++ b/README.md @@ -1,121 +1 @@ -[![Android Studio version](https://img.shields.io/endpoint?url=https%3A%2F%2Fsicampus.ru%2Fgitea%2Fcore%2Fdocs%2Fraw%2Fbranch%2Fmain%2Fandroid-studio-label.json)](https://sicampus.ru/gitea/core/docs/src/branch/main/how-upload-project.md) - -# НТО 2024. II отборочный этап. Командные задани — клиентская часть - -## 📖 Предыстория -В компании S контроль доступа в офис осуществляется с помощью СКУД (системы контроля управления доступом). На данный момент у каждого сотрудника компании есть карта-пропуск с NFC меткой. А у каждой входной двери есть считыватель с обеих сторон. При поднесении карты к считывателю, дверь открывается, а информация о времени входа или выхода сотрудника фиксируется в базе данных. -Администрации компании S требуется мобильное приложение, как для рядовых сотрудников, так и для администрации с возможностью просмотра посещений и работой электронного пропуска как временной замены обычного (при помощи сканировании QR кода, который находится на считывателе). Поскольку в приложении есть возможность использовать телефон как пропуск - то к данному приложению повышенные требования к безопасности всех данных, находящихся внутри него. - - - -## 📋 Системные требования - -| **Параметр** | **Требование** | -|-----------------------------|---------------------------------------| -| **Минимальная версия Android** | 9.0 (API 28) | -| **Целевая версия Android** | 14 (API 34) | -| **Поддерживаемые устройства** | смартфоны, планшеты | -| **Ориентация экранов** | портретная | -| **Языки** | русский, английский | -| **Разрешения** | доступ к интернету, камера (при необходимости) | - - - -## 📱 Техническое задание -Требуется разработать нативное мобильное приложение, которое будет содержать следующие экраны. - - -### 1. Экран авторизации - -> Данный экран должен быть показан при первом входе в приложение, а также в ситуациях, когда пользователь не зарегистрировался в приложении. - -#### Элементы, которые должны присутствовать на экране: -- Поле ввода (`id/username`), в котором пользователю необходимо ввести свой логин. -- Кнопка входа (`id/login`), по нажатию на которую пользователь авторизуется в системе. -- По умолчанию скрытое (`GONE`) текстовое поле с ошибкой (`id/error`). - -#### Требования к компонентам: -1. В пустом поле ввода должна отображаться подсказка, что требуется ввести пользователю. -2. Если хотя бы одно из условий ниже соблюдено - кнопка должна быть неактивной: - - Поле ввода пустое. - - Количество символов менее 3х. - - Логин начинается с цифры. - - Логин содержит символы, отличные от латинского алфавита и цифр. -3. Поле ввода и кнопку должно быть видно при раскрытии клавиатуры. -4. - При нажатии на кнопку входа необходимо проверить, что данный пользователь существует с помощью запроса `api//auth` (подробное описание представлено в техническом задании серверной части). -5. В случае отсутствия логина или любой другой неполадки - необходимо вывести ошибку, пока пользователь не изменит текстовое поле или повторно не нажмёт на кнопку. -6. После нажатия на кнопку - логин должен быть сохранён и при следующем открытии приложения экран авторизации не должен быть показан. -7. После нажатия на кнопку - при нажатии стрелки назад - экран авторизации не должен быть показан повторно. -8. Экран авторизации показывается только в случае, если пользователь неавторизован. - - - - -### 2. Главный экран - -> Данный экран содержит общую информацию о пользователе: ->- ФИО ->- Фото ->- Должность ->- Время последнего входа - -#### Элементы, которые должны присутствовать на экране: -- Текстовое поле (`id/fullname`), в котором написано имя пользователя. -- Изображение (`id/photo`), на котором отображено фото пользователя. -- Текстовое поле (`id/position`), в котором написана должность пользователя. -- Текстовое поле (`id/lastEntry`), в котором написана дата и время последнего входа пользователя. -- Кнопка (`id/logout`) для выхода пользователя из аккаунта. -- Кнопка (`id/refresh`) для обновления данных. -- Кнопка (`id/scan`) для сканирования QR кода. -- По умолчанию скрытое текстовое поле с ошибкой (`id/error`). - -#### Требования к компонентам: -- В случае любой ошибки необходимо скрыть все элементы, кроме текстового поля с ошибкой и кнопки обновления данных. -- Для получения данных необходимо использовать сетевой запрос `/api//info`. -- Формат даты и времени последнего входа пользователя: `yyyy-MM-dd HH:mm` (например: 2024-02-31 08:31). Время необходимо отображать с сервера, без поправок на часовой пояс или локальное смещение. -- При нажатии на кнопку выход все данные (если есть) пользователя должны быть очищены, а приложение должно открыть экран авторизации. -- При нажатии кнопки сканирования необходимо открыть экран сканирования QR кода. -- При нажатии на кнопку обновления данных - необходимо повторно вызывать сетевой запрос для получения актуальных данных. - - - -### 3. Экран сканирования QR-кода - -> Данный экран позволяет отсканировать код на турникете и войти с помощью смартфона. В данном случае данный экран будет уже написан и представлен dам в готовом виде в заготовке. Вам необходимо только подписаться на его результат с помощью **Result API** и обработать считанные данные из QR кода. **Данный экран нельзя модифицировать. Он поставляется как есть.** - - - -### 4. Экран с результатом сканирования QR кода - -> На данном экране необходимо вывести успешность или неуспешность входа с помощью метода QR кода. - -#### Элементы, которые должны присутствовать на экране: -- Текстовое поле (`id/result`), где содержится текст об успешности или неуспешности входа. -- Кнопка (`id/close`) для закрытия данного экрана. - -#### Требования к компонентам: -- В случае, если результат пришёл пустым или со статусом “Отмена” - необходимо вывести пользователю текст: - *"Вход был отменён/Operation was cancelled"* -- В случае, если данные пришли, то необходимо их отправить на сервер с запросом `api//open`, добавив данные из результата и получить ответ. -- Если сервер ответил успешно - то отображаем текст: - *"Успешно/Success"* -- Если сервер ответил любой ошибкой - то отображаем текст: - *"Что-то пошло не так/Something wrong"* -- Кнопка закрытия всегда открывает главный экран. - - - -## 🛠 Решение - -Необходимо загрузить свое решение в систему [по ссылке](https://innovationcampus.ru/lms/mod/quiz/view.php?id=2149). - -Отметим, что работу необходимо осуществлять в представленных проектах-заготовках (шаблонах). - - - -## ✅ Особенности оценивания - -Оценивание происходит с помощью автоматической системы тестирования, которая в автоматическом режиме находит элементы и взаимодействует с ними (именно для этого у каждого элемента указан уникальный идентификатор, по которому будет производится поиск). Каждый тест происходит с чистой установки приложения. -В случае тестирования сервера на него поочередно отправляются команды, описанные в API и ожидаются определенные корректные ответы. -Сервер и приложение тестируются независимо. - +Figma: https://www.figma.com/design/v9YlfUjxz6ChHS5mNWSQPN/TheDevs_Final?node-id=2-3&t=Hnk1mGCVo7joisAC-1 \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts index b543084..23a69ca 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,9 +1,9 @@ plugins { - kotlinAndroid - androidApplication - jetbrainsKotlinSerialization version Version.Kotlin.language - kotlinAnnotationProcessor - id("com.google.dagger.hilt.android").version("2.51.1") + id("com.android.application") + id("kotlin-android") + id("kotlin-kapt") // Добавлено для KAPT + id("dagger.hilt.android.plugin") // Используйте этот синтаксис для Hilt + id("org.jetbrains.kotlin.plugin.serialization") version Version.Kotlin.language // Убедитесь, что версия актуальна } val packageName = "ru.myitschool.work" @@ -22,9 +22,9 @@ android { testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } - buildFeatures.viewBinding = true - - + buildFeatures { + viewBinding = true + } compileOptions { sourceCompatibility = Version.Kotlin.javaSource @@ -37,34 +37,41 @@ android { } dependencies { - implementation ("com.squareup.retrofit2:retrofit:2.9.0") - implementation ("com.squareup.retrofit2:converter-gson:2.9.0") - implementation ("com.squareup.okhttp3:okhttp:4.9.0") - implementation ("com.github.bumptech.glide:glide:4.15.1") - kapt ("com.github.bumptech.glide:compiler:4.15.1") + // Retrofit and OkHttp + implementation("com.squareup.retrofit2:retrofit:2.9.0") + implementation("com.squareup.retrofit2:converter-gson:2.9.0") + implementation("com.squareup.okhttp3:okhttp:4.9.0") - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.0") - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.0") + // Glide + implementation("com.github.bumptech.glide:glide:4.15.1") + kapt("com.github.bumptech.glide:compiler:4.15.1") + + // AndroidX Libraries + implementation("androidx.appcompat:appcompat:1.6.1") + implementation("com.google.android.material:material:1.10.0") + implementation("androidx.activity:activity:1.10.0") + implementation("androidx.constraintlayout:constraintlayout:2.1.4") implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.5.1") - defaultLibrary() + // Coroutines + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.0") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.0") - implementation(Dependencies.AndroidX.activity) - implementation(Dependencies.AndroidX.fragment) - implementation(Dependencies.AndroidX.constraintLayout) + // Hilt dependencies + implementation("com.google.dagger:hilt-android:2.51.1") + kapt("com.google.dagger:hilt-android-compiler:2.51.1") + // Navigation implementation(Dependencies.AndroidX.Navigation.fragment) implementation(Dependencies.AndroidX.Navigation.navigationUi) - implementation(Dependencies.Retrofit.library) - implementation(Dependencies.Retrofit.gsonConverter) - - - implementation("com.squareup.picasso:picasso:2.8") - implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.1") + // DataStore implementation("androidx.datastore:datastore-preferences:1.1.1") + + // ML Kit implementation("com.google.mlkit:barcode-scanning:17.3.0") + // CameraX val cameraX = "1.3.4" implementation("androidx.camera:camera-core:$cameraX") implementation("androidx.camera:camera-camera2:$cameraX") @@ -72,11 +79,13 @@ dependencies { implementation("androidx.camera:camera-view:$cameraX") implementation("androidx.camera:camera-mlkit-vision:1.4.0-rc04") - val hilt = "2.51.1" - implementation("com.google.dagger:hilt-android:$hilt") - kapt("com.google.dagger:hilt-android-compiler:$hilt") + // Picasso + implementation("com.squareup.picasso:picasso:2.8") + + // Kotlin Serialization + implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.1") } kapt { correctErrorTypes = true -} +} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/api/ApiModule.kt b/app/src/main/java/ru/myitschool/work/api/ApiModule.kt index a13ebd5..4d8d491 100644 --- a/app/src/main/java/ru/myitschool/work/api/ApiModule.kt +++ b/app/src/main/java/ru/myitschool/work/api/ApiModule.kt @@ -4,10 +4,9 @@ import dagger.Module import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent +import okhttp3.OkHttpClient import retrofit2.Retrofit import retrofit2.converter.gson.GsonConverterFactory -import ru.myitschool.work.api.ApiService -import ru.myitschool.work.core.Constants import javax.inject.Singleton @Module @@ -16,9 +15,26 @@ object ApiModule { @Provides @Singleton - fun provideRetrofit(): Retrofit { + fun provideOkHttpClient(authInterceptor: AuthInterceptor): OkHttpClient { + return OkHttpClient.Builder() + .addInterceptor(authInterceptor) // Добавляем интерсептор + .build() + } + + @Provides + @Singleton + fun provideAuthInterceptor(): AuthInterceptor { + val username = "pivanov" // Замените на ваш логин + val password = "password123" // Замените на ваш пароль + return AuthInterceptor(username, password) + } + + @Provides + @Singleton + fun provideRetrofit(client: OkHttpClient): Retrofit { return Retrofit.Builder() - .baseUrl(Constants.SERVER_ADDRESS) + .baseUrl("http://10.6.66.110:8080/") // Убедитесь, что URL корректен + .client(client) .addConverterFactory(GsonConverterFactory.create()) .build() } diff --git a/app/src/main/java/ru/myitschool/work/api/ApiService.kt b/app/src/main/java/ru/myitschool/work/api/ApiService.kt index 9328037..6147d40 100644 --- a/app/src/main/java/ru/myitschool/work/api/ApiService.kt +++ b/app/src/main/java/ru/myitschool/work/api/ApiService.kt @@ -1,19 +1,23 @@ package ru.myitschool.work.api +import okhttp3.ResponseBody import retrofit2.Response import retrofit2.http.Body import retrofit2.http.GET import retrofit2.http.PATCH import retrofit2.http.POST import retrofit2.http.Path +import retrofit2.http.Query interface ApiService { // Метод для аутентификации - @POST("/api/auth") // Изменено на POST и путь к аутентификации + @GET("/api/auth") suspend fun authenticate( - @Body payload: AuthRequest // Передаем объект с логином и паролем - ): Response // Возвращаем UserAuthResponse + @Query("login") login: String, + @Query("password") password: String + ): Response // Измените ResponseBody на String // Возвращаем ResponseBody вместо String + // Другие методы... @GET("/api/{login}/info") // Получение информации о пользователе suspend fun getUserInfo(@Path("login") login: String): Response> @@ -30,17 +34,6 @@ interface ApiService { suspend fun getAllWorkers(): Response> } -// Модель данных для запроса аутентификации -data class AuthRequest( - val login: String, - val password: String // Поле для пароля -) - -// Модель данных для ответа аутентификации -data class UserAuthResponse( - val role: String // Добавляем поле для роли -) - // Модель данных для информации о сотруднике data class EmployeeData( val name: String, diff --git a/app/src/main/java/ru/myitschool/work/core/Constants.kt b/app/src/main/java/ru/myitschool/work/core/Constants.kt index 7a5d665..63a66e1 100644 --- a/app/src/main/java/ru/myitschool/work/core/Constants.kt +++ b/app/src/main/java/ru/myitschool/work/core/Constants.kt @@ -1,5 +1,5 @@ package ru.myitschool.work.core // БЕРИТЕ И ИЗМЕНЯЙТЕ ХОСТ ТОЛЬКО ЗДЕСЬ И НЕ БЕРИТЕ ИЗ ДРУГИХ МЕСТ. ФАЙЛ ПЕРЕМЕЩАТЬ НЕЛЬЗЯ object Constants { - const val SERVER_ADDRESS = "const val SERVER_ADDRESS = \"http://localhost:8080\"\n" + const val SERVER_ADDRESS = "http://10.6.66.110:8080" } \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/ui/Main/AdminFragment.kt b/app/src/main/java/ru/myitschool/work/ui/Main/AdminFragment.kt index e104bb1..d9f6dd3 100644 --- a/app/src/main/java/ru/myitschool/work/ui/Main/AdminFragment.kt +++ b/app/src/main/java/ru/myitschool/work/ui/Main/AdminFragment.kt @@ -1,4 +1,4 @@ -package ru.myitschool.work.ui.admin +package ru.myitschool.work.ui.Main import android.os.Bundle import android.view.View diff --git a/app/src/main/java/ru/myitschool/work/ui/login/LoginFragment.kt b/app/src/main/java/ru/myitschool/work/ui/login/LoginFragment.kt index 4d85985..c6daf17 100644 --- a/app/src/main/java/ru/myitschool/work/ui/login/LoginFragment.kt +++ b/app/src/main/java/ru/myitschool/work/ui/login/LoginFragment.kt @@ -1,5 +1,6 @@ package ru.myitschool.work.ui.login +import ru.myitschool.work.api.ApiService import android.os.Bundle import android.text.Editable import android.text.InputType @@ -15,7 +16,6 @@ import androidx.navigation.fragment.findNavController import dagger.hilt.android.AndroidEntryPoint import ru.myitschool.work.R import ru.myitschool.work.SessionManager -import ru.myitschool.work.api.UserAuthResponse import ru.myitschool.work.databinding.FragmentLoginBinding import ru.myitschool.work.utils.collectWhenStarted import ru.myitschool.work.utils.visibleOrGone @@ -40,7 +40,7 @@ class LoginFragment : Fragment(R.layout.fragment_login) { } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) + super.onViewCreated(view, savedInstanceState) // Убедитесь, что этот вызов находится здесь _binding = FragmentLoginBinding.bind(view) setupUI() @@ -85,32 +85,23 @@ class LoginFragment : Fragment(R.layout.fragment_login) { } private fun subscribe() { - viewModel.state.collectWhenStarted(this) { state -> - binding.loading.visibleOrGone(false) + lifecycleScope.launch { + viewModel.state.collect { state -> + binding.loading.visibleOrGone(false) - try { if (state.maintenance) { showMaintenanceDialog() // Показываем диалог о техработах - } else if (state.error != null) { - binding.error.text = state.error - binding.error.visibility = View.VISIBLE } else if (state.success) { binding.error.visibility = View.GONE authPreferences.saveLoginState(true) authPreferences.saveLogin(binding.username.text.toString()) // Сохраняем логин - // Сохраняем роль пользователя в SessionManager - val userAuthResponse: UserAuthResponse? = state.userAuthResponse - userAuthResponse?.let { - SessionManager.userRole = it.role // Сохраняем роль - } - Toast.makeText(context, "Авторизация прошла успешно", Toast.LENGTH_SHORT).show() - navigateToMainScreen() + navigateToMainScreen() // Перенаправление на следующий экран + } else if (state.error != null) { + binding.error.text = state.error + binding.error.visibility = View.VISIBLE } - } catch (e: Exception) { - Log.e("LoginFragment", "Ошибка при обработке состояния", e) - Toast.makeText(context, "Произошла ошибка. Пожалуйста, попробуйте снова.", Toast.LENGTH_SHORT).show() } } } @@ -119,7 +110,7 @@ class LoginFragment : Fragment(R.layout.fragment_login) { AlertDialog.Builder(requireContext()) .setTitle("Технические работы") .setMessage("Проводятся техработы, пожалуйста, подождите.") - .setPositiveButton("ОК") { dialog, _ -> dialog.dismiss() } + .setPositiveButton("ОК ") { dialog, _ -> dialog.dismiss() } .setCancelable(false) .show() } diff --git a/app/src/main/java/ru/myitschool/work/ui/login/LoginViewModel.kt b/app/src/main/java/ru/myitschool/work/ui/login/LoginViewModel.kt index a62b768..6d53cd4 100644 --- a/app/src/main/java/ru/myitschool/work/ui/login/LoginViewModel.kt +++ b/app/src/main/java/ru/myitschool/work/ui/login/LoginViewModel.kt @@ -1,53 +1,41 @@ package ru.myitschool.work.ui.login -import android.content.Context +import ru.myitschool.work.api.ApiService +import android.util.Log import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel -import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch -import okhttp3.Credentials -import retrofit2.Retrofit -import retrofit2.converter.gson.GsonConverterFactory -import ru.myitschool.work.SessionManager -import ru.myitschool.work.api.ApiService -import ru.myitschool.work.api.AuthRequest -import ru.myitschool.work.api.UserAuthResponse -import ru.myitschool.work.core.Constants import javax.inject.Inject @HiltViewModel class LoginViewModel @Inject constructor( - @ApplicationContext private val context: Context, + private val apiService: ApiService ) : ViewModel() { - private val _state = MutableStateFlow(LoginState()) - val state = _state.asStateFlow() - private val apiService: ApiService by lazy { - Retrofit.Builder() - .baseUrl(Constants.SERVER_ADDRESS) - .addConverterFactory(GsonConverterFactory.create()) - .build() - .create(ApiService::class.java) - } + private val _state = MutableStateFlow(LoginState()) + val state: StateFlow get() = _state fun authenticate(username: String, password: String) { if (isValidUsername(username)) { viewModelScope.launch { try { - val payload = AuthRequest(username, password) // Создаем объект запроса - val response = apiService.authenticate(payload) // Вызываем метод аутентификации - if (response.isSuccessful) { - val userAuthResponse = response.body() // Получаем ответ с ролью пользователя - userAuthResponse?.let { - SessionManager.userLogin = username // Сохраняем логин - SessionManager.userRole = it.role // Сохраняем роль + val response = apiService.authenticate(username, password) + Log.d("LoginViewModel", "Response code: ${response.code()}") + + // Проверяем код ответа + when (response.code()) { + 200 -> { + _state.value = LoginState(success = true) // Успешная авторизация + } + 400 -> { + _state.value = LoginState(error = "Ошибка авторизации: Неверные учетные данные.") + } + else -> { + _state.value = LoginState(error = "Ошибка авторизации: ${response.message()}") } - _state.value = LoginState(success = true, userAuthResponse = userAuthResponse) - } else { - _state.value = LoginState(error = "Ошибка авторизации") } } catch (e: Exception) { e.printStackTrace() @@ -60,13 +48,13 @@ class LoginViewModel @Inject constructor( } private fun isValidUsername(username: String): Boolean { - return username.length >= 3 && !username.first().isDigit() && username.all { it.isLetterOrDigit() } + return username.isNotEmpty() // Пример проверки логина } +} - data class LoginState( - val success: Boolean = false, - val error: String? = null, - val maintenance: Boolean = false, - val userAuthResponse: UserAuthResponse? = null // Добавляем поле для хранения информации о роли - ) -} \ No newline at end of file +// Состояние аутентификации +data class LoginState( + val success: Boolean = false, // Успешность аутентификации + val error: String? = null, // Сообщение об ошибке + val maintenance: Boolean = false // Состояние техобслуживания +) \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_login.xml b/app/src/main/res/layout/fragment_login.xml index a1c6975..525aea1 100644 --- a/app/src/main/res/layout/fragment_login.xml +++ b/app/src/main/res/layout/fragment_login.xml @@ -11,10 +11,10 @@ android:id="@+id/username" android:layout_width="match_parent" android:layout_height="wrap_content" - android:hint="Введите логин" + android:hint="@string/username_hint" android:inputType="text" android:padding="12dp" - android:background="#F0F0F0" + android:background="@drawable/ic_android_black_24dp" android:layout_marginBottom="16dp"/>