Android Navigation Component, Dagger Hilt and other things

This commit is contained in:
2021-07-10 17:39:43 +03:00
parent 98bbc6a791
commit 1c773df3e1
136 changed files with 1214 additions and 401 deletions
+59 -42
View File
@@ -2,18 +2,26 @@ plugins {
id("com.android.application")
id("kotlin-android")
id("kotlin-kapt")
id("androidx.navigation.safeargs.kotlin")
id("dagger.hilt.android.plugin")
}
android {
compileSdkVersion(ConfigData.compileSdkVersion)
buildToolsVersion(ConfigData.buildToolsVersion)
compileSdkVersion(30)
buildToolsVersion("30.0.3")
defaultConfig {
applicationId = "com.meloda.fast"
minSdkVersion(ConfigData.minSdkVersion)
targetSdkVersion(ConfigData.targetSdkVersion)
versionCode = ConfigData.versionCode
versionName = ConfigData.versionName
minSdkVersion(23)
targetSdkVersion(30)
versionCode = 1
versionName = "1.0"
javaCompileOptions {
annotationProcessorOptions {
arguments += mapOf("room.schemaLocation" to "$projectDir/schemas")
}
}
}
buildTypes {
@@ -43,53 +51,62 @@ android {
dataBinding = true
viewBinding = true
}
}
java {
val kotlinSrcDir = "src/main/kotlin"
println(sourceSets.names)
// val mainJavaSourceSet: SourceDirectorySet = sourceSets.getByName("main").java
// mainJavaSourceSet.srcDir(kotlinSrcDir)
// println(mainJavaSourceSet.srcDirs)
kapt {
correctErrorTypes = true
//use this shit if you don't want to have hilt errors
javacOptions {
option("-Adagger.hilt.android.internal.disableAndroidSuperclassValidation=true")
}
}
//java.sourceSets.create("src/main/kotlin")
//sourceSets {
// main.java.srcDirs += "src/main/kotlin"
//}
dependencies {
implementation(Deps.kotlin)
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.5.20")
coreLibraryDesugaring(Deps.desugaring)
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:1.1.5")
implementation(Deps.appCompat)
implementation(Deps.material)
implementation(Deps.core)
implementation(Deps.preferences)
implementation(Deps.swipeRefreshLayout)
implementation(Deps.recyclerView)
implementation(Deps.cardView)
implementation(Deps.fragment)
implementation("androidx.appcompat:appcompat:1.4.0-alpha03")
implementation("com.google.android.material:material:1.4.0")
implementation("androidx.core:core-ktx:1.7.0-alpha01")
implementation("androidx.preference:preference-ktx:1.1.1")
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.2.0-alpha01")
implementation("androidx.recyclerview:recyclerview:1.2.1")
implementation("androidx.cardview:cardview:1.0.0")
implementation("androidx.fragment:fragment-ktx:1.3.5")
implementation(Deps.coroutineCore)
implementation(Deps.coroutineAndroid)
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.1")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.1")
implementation(Deps.roomRuntime)
kapt(Deps.roomCompiler)
implementation("androidx.room:room-runtime:2.3.0")
kapt("androidx.room:room-compiler:2.3.0")
implementation("androidx.navigation:navigation-fragment-ktx:2.3.5")
implementation("androidx.navigation:navigation-ui-ktx:2.3.5")
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.1")
implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.3.1")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.3.1")
implementation("androidx.lifecycle:lifecycle-viewmodel-savedstate:2.3.1")
implementation("androidx.lifecycle:lifecycle-common-java8:2.3.1")
implementation("com.squareup.okhttp3:okhttp:5.0.0-alpha.2")
implementation("com.squareup.okhttp3:logging-interceptor:5.0.0-alpha.2")
implementation("com.squareup.retrofit2:retrofit:2.9.0")
implementation("com.squareup.retrofit2:converter-gson:2.9.0")
implementation("com.google.dagger:hilt-android:2.37")
kapt("com.google.dagger:hilt-android-compiler:2.37")
implementation("androidx.hilt:hilt-navigation-fragment:1.0.0")
implementation(Deps.gson)
implementation(Deps.jsoup)
implementation(Deps.acra)
implementation("com.github.yogacp:android-viewbinding:1.0.2")
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:${Versions.lifecycle}")
implementation("androidx.lifecycle:lifecycle-livedata-ktx:${Versions.lifecycle}")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:${Versions.lifecycle}")
implementation("androidx.lifecycle:lifecycle-viewmodel-savedstate:${Versions.lifecycle}")
implementation("androidx.lifecycle:lifecycle-common-java8:${Versions.lifecycle}")
implementation("io.coil-kt:coil:1.2.2")
implementation("com.google.code.gson:gson:2.8.7")
implementation("org.jsoup:jsoup:1.14.1")
implementation("ch.acra:acra:4.11.1")
implementation("com.squareup.retrofit2:retrofit:${Versions.retrofit}")
implementation("com.squareup.retrofit2:converter-gson:${Versions.retrofit}")
}
+1
View File
@@ -2,6 +2,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.meloda.fast">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<application
+12 -18
View File
@@ -10,32 +10,26 @@ object UserConfig {
const val API_ID = "6964679"
var token = ""
var userId = 0
fun save() {
AppGlobal.preferences.edit()
.putString(TOKEN, token)
.putInt(USER_ID, userId)
.apply()
var userId: Int = -1
get() = AppGlobal.preferences.getInt(USER_ID, -1)
set(value) {
field = value
AppGlobal.preferences.edit().putInt(USER_ID, value).apply()
}
fun restore() {
token = AppGlobal.preferences.getString(TOKEN, "") ?: ""
userId = AppGlobal.preferences.getInt(USER_ID, -1)
var accessToken: String = ""
get() = AppGlobal.preferences.getString(TOKEN, "") ?: ""
set(value) {
field = value
AppGlobal.preferences.edit().putString(TOKEN, value).apply()
}
fun clear() {
token = ""
accessToken = ""
userId = -1
AppGlobal.preferences.edit()
.remove(TOKEN)
.remove(USER_ID)
.apply()
}
fun isLoggedIn(): Boolean {
return userId > 0 && !TextUtils.isEmpty(token)
return userId > 0 && !TextUtils.isEmpty(accessToken)
}
}
@@ -5,19 +5,15 @@ import android.viewbinding.library.activity.viewBinding
import com.meloda.fast.R
import com.meloda.fast.base.BaseActivity
import com.meloda.fast.databinding.ActivityMainBinding
import com.meloda.fast.fragment.LoginFragment
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class MainActivity : BaseActivity(R.layout.activity_main) {
private val binding: ActivityMainBinding by viewBinding()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
supportFragmentManager.beginTransaction()
.replace(R.id.fragmentContainer, LoginFragment())
.commit()
}
}
@@ -0,0 +1,6 @@
package com.meloda.fast.api
sealed class Answer<out R> {
data class Success<out T>(val data: T) : Answer<T>()
data class Error(val errorString: String) : Answer<Nothing>()
}
@@ -0,0 +1,2 @@
package com.meloda.fast.api
@@ -29,12 +29,12 @@ object VKAuth {
fun getDirectAuthUrl(login: String, password: String, captcha: String = ""): String {
return "https://oauth.vk.com/token?grant_type=password&" +
"client_id=6146827&" +
"client_id=${VKConstants.VK_APP_ID}&" +
"scope=$settings&" +
"client_secret=qVxWRF1CwHERuIrKBnqe&" +
"client_secret=${VKConstants.VK_APP_SECRET}&" +
"username=$login&" +
"password=$password" +
(if (captcha.isEmpty()) "" else "&$captcha") +
(if (captcha.isBlank()) "" else "&$captcha") +
"&v=${VKApi.API_VERSION}"
// return "https://oauth.vk.com/token?grant_type=password&" +
// "client_id=2274003&" +
@@ -61,20 +61,19 @@ object VKAuth {
"v=${URLEncoder.encode(VKApi.API_VERSION, "utf-8")}"
}
@Throws(Exception::class)
fun parseRedirectUrl(url: String): Array<String> {
val accessToken = VKUtil.extractPattern(url, "access_token=(.*?)&")
val userId = VKUtil.extractPattern(url, "user_id=(\\d*)")
fun parseRedirectUrl(url: String): Pair<String, Int> {
val accessToken = VKUtil.extractPattern(url, "access_token=(.*?)&") ?: ""
val userId = VKUtil.extractPattern(url, "user_id=(\\d*)")?.toIntOrNull() ?: -1
if (BuildConfig.DEBUG) {
Log.i(TAG, "access_token=$accessToken")
Log.i(TAG, "user_id=$userId")
}
if (userId == null || userId.isEmpty() || accessToken == null || accessToken.isEmpty()) throw Exception(
"Failed to parse redirect url $url"
if (accessToken.isEmpty() || userId == -1) throw Exception(
"Failed to parse redirect url: $url"
)
return arrayOf(accessToken, userId)
return accessToken to userId
}
}
@@ -8,6 +8,12 @@ object VKConstants {
const val USER_FIELDS =
"photo_50,photo_100,photo_200,status,screen_name,online,online_mobile,last_seen,verified,sex"
const val VK_APP_ID = "2274003"
const val VK_APP_SECRET = "hHbZxrka2uZ6jB1inYsH"
const val VK_ME_ID = "6146827"
const val VK_ME_SECRET = "qVxWRF1CwHERuIrKBnqe"
/*
const val ACTION_CHAT_CREATE = "chat_create"
@@ -0,0 +1,7 @@
package com.meloda.fast.api
interface VKRepo {
}
@@ -0,0 +1,33 @@
package com.meloda.fast.base
import android.os.Bundle
import android.view.View
import androidx.annotation.LayoutRes
import androidx.lifecycle.lifecycleScope
import com.meloda.fast.base.viewmodel.BaseVM
import com.meloda.fast.base.viewmodel.VKEvent
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.collect
abstract class BaseVMFragment<VM : BaseVM> : BaseFragment {
constructor() : super()
constructor(@LayoutRes resId: Int) : super(resId)
protected abstract val viewModel: VM
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewLifecycleOwner.lifecycleScope.launchWhenStarted {
viewModel.tasksEvent.onEach { onEvent(it) }.collect()
}
}
protected open fun onEvent(event: VKEvent) {
when (event) {
}
}
}
@@ -0,0 +1,30 @@
package com.meloda.fast.base.viewmodel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.meloda.fast.api.Answer
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.launch
abstract class BaseVM : ViewModel() {
protected val tasksEventChannel = Channel<VKEvent>()
val tasksEvent = tasksEventChannel.receiveAsFlow()
protected fun <T> makeJob(
job: suspend () -> Answer<T>,
onAnswer: suspend (T) -> Unit = {},
onStart: (suspend () -> Unit)? = null,
onEnd: (suspend () -> Unit)? = null,
onError: (suspend (String) -> Unit)? = null
) = viewModelScope.launch {
onStart?.invoke()
when (val response = job()) {
is Answer.Success -> onAnswer(response.data)
is Answer.Error -> onError?.invoke(response.errorString) ?: tasksEventChannel.send(
ShowDialogInfoEvent(null, response.errorString)
)
}
}.also { it.invokeOnCompletion { viewModelScope.launch { onEnd?.invoke() } } }
}
@@ -0,0 +1,8 @@
package com.meloda.fast.base.viewmodel
data class ShowDialogInfoEvent(
val title: String? = null,
val message: String,
val positiveBtn: String? = null,
val negativeBtn: String? = null
) : VKEvent()
@@ -0,0 +1,3 @@
package com.meloda.fast.base.viewmodel
abstract class VKEvent
@@ -11,9 +11,9 @@ import androidx.core.content.pm.PackageInfoCompat
import androidx.preference.PreferenceManager
import com.meloda.fast.BuildConfig
import com.meloda.fast.R
import com.meloda.fast.UserConfig
import com.meloda.fast.database.DatabaseHelper
import com.meloda.fast.util.AndroidUtils
import dagger.hilt.android.HiltAndroidApp
import org.acra.ACRA
import org.acra.ReportingInteractionMode
import org.acra.annotation.ReportsCrashes
@@ -29,6 +29,7 @@ import java.util.*
resDialogPositiveButtonText = R.string.send_crash_report,
resDialogNegativeButtonText = R.string.ok
)
@HiltAndroidApp
class AppGlobal : Application() {
companion object {
@@ -81,8 +82,6 @@ class AppGlobal : Application() {
screenWidth = AndroidUtils.getDisplayWidth()
screenHeight = AndroidUtils.getDisplayHeight()
UserConfig.restore()
}
}
@@ -0,0 +1,261 @@
package com.meloda.fast.extensions
import android.content.Intent
import android.util.SparseArray
import androidx.core.util.forEach
import androidx.core.util.set
import androidx.fragment.app.FragmentManager
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.navigation.NavController
import androidx.navigation.fragment.NavHostFragment
import com.google.android.material.bottomnavigation.BottomNavigationView
import com.meloda.fast.R
/**
* Manages the various graphs needed for a [BottomNavigationView].
*
* This sample is a workaround until the Navigation Component supports multiple back stacks.
*/
object NavigationExtensions {
fun BottomNavigationView.setupWithNavController(
navGraphIds: List<Int>,
fragmentManager: FragmentManager,
// dataManager: DataManager,
containerId: Int,
intent: Intent
): LiveData<NavController> {
// Map of tags
val graphIdToTagMap = SparseArray<String>()
// Result. Mutable live data with the selected controlled
val selectedNavController = MutableLiveData<NavController>()
var firstFragmentGraphId = 0
// First create a NavHostFragment for each NavGraph ID
navGraphIds.forEachIndexed { index, navGraphId ->
val fragmentTag = getFragmentTag(index)
// Find or create the Navigation host fragment
val navHostFragment = obtainNavHostFragment(
fragmentManager,
fragmentTag,
navGraphId,
containerId
)
// Obtain its id
val graphId = navHostFragment.navController.graph.id
if (index == 0) {
firstFragmentGraphId = graphId
}
// Save to the map
graphIdToTagMap[graphId] = fragmentTag
// Attach or detach nav host fragment depending on whether it's the selected item.
if (this.selectedItemId == graphId) {
// Update livedata with the selected graph
selectedNavController.value = navHostFragment.navController
attachNavHostFragment(fragmentManager, navHostFragment, index == 0)
} else {
detachNavHostFragment(fragmentManager, navHostFragment)
}
}
// Now connect selecting an item with swapping Fragments
var selectedItemTag = graphIdToTagMap[this.selectedItemId]
val firstFragmentTag = graphIdToTagMap[firstFragmentGraphId]
var isOnFirstFragment = selectedItemTag == firstFragmentTag
// When a navigation item is selected
setOnNavigationItemSelectedListener { item ->
// Don't do anything if the state is state has already been saved.
if (fragmentManager.isStateSaved) {
false
} else {
val navController =
(fragmentManager.findFragmentByTag(selectedItemTag) as NavHostFragment).navController
navController.popBackStack(navController.graph.startDestination, false)
if (selectedItemTag != graphIdToTagMap[item.itemId]) {
// val listCheck = listOf(R.id.contacts, R.id.chats)
val newlySelectedItemTag =
//if (listCheck.contains(item.itemId) && dataManager.token.isBlank()) graphIdToTagMap[R.id.signIn] else
graphIdToTagMap[item.itemId]
// Pop everything above the first fragment (the "fixed start destination")
fragmentManager.popBackStack(
firstFragmentTag,
FragmentManager.POP_BACK_STACK_INCLUSIVE
)
val selectedFragment =
fragmentManager.findFragmentByTag(newlySelectedItemTag) as NavHostFragment
// Exclude the first fragment tag because it's always in the back stack.
if (firstFragmentTag != newlySelectedItemTag) {
// Commit a transaction that cleans the back stack and adds the first fragment
// to it, creating the fixed started destination.
fragmentManager.beginTransaction()
.setCustomAnimations(
R.anim.nav_default_enter_anim,
R.anim.nav_default_exit_anim,
R.anim.nav_default_pop_enter_anim,
R.anim.nav_default_pop_exit_anim
)
.attach(selectedFragment)
.setPrimaryNavigationFragment(selectedFragment)
.apply {
// Detach all other Fragments
graphIdToTagMap.forEach { _, fragmentTagIter ->
if (fragmentTagIter != newlySelectedItemTag) {
detach(fragmentManager.findFragmentByTag(firstFragmentTag)!!)
}
}
}
.addToBackStack(firstFragmentTag)
.setReorderingAllowed(true)
.commit()
}
selectedItemTag = newlySelectedItemTag
isOnFirstFragment = selectedItemTag == firstFragmentTag
selectedNavController.value = selectedFragment.navController
true
} else {
false
}
}
}
setOnNavigationItemReselectedListener { item ->
val newlySelectedItemTag = graphIdToTagMap[item.itemId]
val selectedFragment =
fragmentManager.findFragmentByTag(newlySelectedItemTag) as NavHostFragment
val navController = selectedFragment.navController
// Pop the back stack to the start destination of the current navController graph
if (selectedItemTag != graphIdToTagMap[item.itemId]) {
fragmentManager.beginTransaction()
.setCustomAnimations(
R.anim.nav_default_enter_anim,
R.anim.nav_default_exit_anim,
R.anim.nav_default_pop_enter_anim,
R.anim.nav_default_pop_exit_anim
)
.attach(selectedFragment)
.setPrimaryNavigationFragment(selectedFragment)
.apply {
// Detach all other Fragments
graphIdToTagMap.forEach { _, fragmentTagIter ->
if (fragmentTagIter != newlySelectedItemTag) {
detach(fragmentManager.findFragmentByTag(firstFragmentTag)!!)
}
}
}
.addToBackStack(firstFragmentTag)
.setReorderingAllowed(true)
.commit()
selectedItemTag = newlySelectedItemTag
isOnFirstFragment = selectedItemTag == firstFragmentTag
selectedNavController.value = selectedFragment.navController
} else navController.popBackStack(navController.graph.startDestination, false)
}
// Optional: on item reselected, pop back stack to the destination of the graph
setupDeepLinks(navGraphIds, fragmentManager, containerId, intent)
// Finally, ensure that we update our BottomNavigationView when the back stack changes
fragmentManager.addOnBackStackChangedListener {
if (!isOnFirstFragment && !fragmentManager.isOnBackStack(firstFragmentTag)) {
this.selectedItemId = firstFragmentGraphId
}
// Reset the graph if the currentDestination is not valid (happens when the back
// stack is popped after using the back button).
selectedNavController.value?.let { controller ->
if (controller.currentDestination == null) {
controller.navigate(controller.graph.id)
}
}
}
return selectedNavController
}
private fun BottomNavigationView.setupDeepLinks(
navGraphIds: List<Int>,
fragmentManager: FragmentManager,
containerId: Int,
intent: Intent
) {
navGraphIds.forEachIndexed { index, navGraphId ->
val fragmentTag = getFragmentTag(index)
// Find or create the Navigation host fragment
val navHostFragment = obtainNavHostFragment(
fragmentManager,
fragmentTag,
navGraphId,
containerId
)
// Handle Intent
if (navHostFragment.navController.handleDeepLink(intent) &&
selectedItemId != navHostFragment.navController.graph.id
) {
this.selectedItemId = navHostFragment.navController.graph.id
}
}
}
private fun detachNavHostFragment(
fragmentManager: FragmentManager,
navHostFragment: NavHostFragment
) {
fragmentManager.beginTransaction()
.detach(navHostFragment)
.commitNow()
}
private fun attachNavHostFragment(
fragmentManager: FragmentManager,
navHostFragment: NavHostFragment,
isPrimaryNavFragment: Boolean
) {
fragmentManager.beginTransaction()
.attach(navHostFragment)
.apply {
if (isPrimaryNavFragment) {
setPrimaryNavigationFragment(navHostFragment)
}
}
.commitNow()
}
private fun obtainNavHostFragment(
fragmentManager: FragmentManager,
fragmentTag: String,
navGraphId: Int,
containerId: Int,
): NavHostFragment {
// If the Nav Host fragment exists, return it
val existingFragment = fragmentManager.findFragmentByTag(fragmentTag) as NavHostFragment?
existingFragment?.let { return it }
// Otherwise, create it and return it.
val navHostFragment = NavHostFragment.create(navGraphId)
fragmentManager.beginTransaction()
.add(containerId, navHostFragment, fragmentTag)
.commitNow()
return navHostFragment
}
private fun FragmentManager.isOnBackStack(backStackName: String): Boolean {
val backStackCount = backStackEntryCount
for (index in 0 until backStackCount) {
if (getBackStackEntryAt(index).name == backStackName) {
return true
}
}
return false
}
private fun getFragmentTag(index: Int) = "bottomNavigation#$index"
}
@@ -1,22 +0,0 @@
package com.meloda.fast.fragment
import android.os.Bundle
import android.view.View
import android.viewbinding.library.fragment.viewBinding
import com.meloda.fast.R
import com.meloda.fast.base.BaseFragment
import com.meloda.fast.databinding.FragmentLoginBinding
class LoginFragment : BaseFragment(R.layout.fragment_login) {
private val binding: FragmentLoginBinding by viewBinding()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
}
}
@@ -0,0 +1,220 @@
package com.meloda.fast.fragment.login
import android.os.Bundle
import android.view.Gravity
import android.view.KeyEvent
import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.EditorInfo
import android.viewbinding.library.fragment.viewBinding
import android.widget.LinearLayout
import androidx.appcompat.app.AlertDialog
import androidx.core.os.bundleOf
import androidx.core.widget.addTextChangedListener
import androidx.fragment.app.setFragmentResultListener
import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import coil.load
import com.google.android.material.imageview.ShapeableImageView
import com.google.android.material.textfield.TextInputEditText
import com.google.android.material.textfield.TextInputLayout
import com.meloda.fast.R
import com.meloda.fast.base.BaseVMFragment
import com.meloda.fast.base.viewmodel.VKEvent
import com.meloda.fast.databinding.FragmentLoginBinding
import com.meloda.fast.util.KeyboardUtils
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.util.*
import kotlin.concurrent.schedule
import kotlin.math.roundToInt
@AndroidEntryPoint
class LoginFragment : BaseVMFragment<LoginVM>(R.layout.fragment_login) {
override val viewModel: LoginVM by viewModels()
private val binding: FragmentLoginBinding by viewBinding()
private var lastEmail: String = ""
private var lastPassword: String = ""
private var errorTimer: Timer? = null
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel.checkUserSession()
prepareViews()
setFragmentResultListener("validation") { _, bundle ->
lifecycleScope.launch { viewModel.getValidatedData(bundle) }
}
binding.loginInput.setText("danil_nikolaev_02@mail.ru")
binding.passwordInput.setText("iwanttofuck24")
}
override fun onEvent(event: VKEvent) {
super.onEvent(event)
when (event) {
is ShowCaptchaDialog -> showCaptchaDialog(event.captchaImage, event.captchaSid)
is GoToValidationEvent -> goToValidation(event.redirectUrl)
GoToMainEvent -> goToMain()
}
}
private fun prepareViews() {
prepareEmailEditText()
preparePasswordEditText()
prepareAuthButton()
}
private fun prepareEmailEditText() {
binding.loginInput.addTextChangedListener {
if (!binding.loginLayout.error.isNullOrBlank()) binding.loginLayout.error = ""
}
}
private fun preparePasswordEditText() {
binding.passwordLayout.endIconMode = TextInputLayout.END_ICON_NONE
binding.passwordInput.addTextChangedListener {
if (!binding.passwordLayout.error.isNullOrBlank()) binding.passwordLayout.error = ""
}
binding.passwordInput.setOnFocusChangeListener { _, hasFocus ->
binding.passwordLayout.endIconMode =
if (hasFocus) TextInputLayout.END_ICON_PASSWORD_TOGGLE
else TextInputLayout.END_ICON_NONE
}
binding.passwordInput.setOnEditorActionListener { _, _, event ->
if (event == null) return@setOnEditorActionListener false
return@setOnEditorActionListener if (event.action == EditorInfo.IME_ACTION_GO ||
(event.action == KeyEvent.ACTION_DOWN && (event.keyCode == KeyEvent.KEYCODE_ENTER || event.keyCode == KeyEvent.KEYCODE_NUMPAD_ENTER))
) {
KeyboardUtils.hideKeyboardFrom(binding.passwordInput)
binding.authorize.performClick()
true
} else false
}
}
private fun prepareAuthButton() {
binding.authorize.setOnClickListener {
val loginString = binding.loginInput.text.toString().trim()
val passwordString = binding.passwordInput.text.toString().trim()
if (!validateInputData(loginString, passwordString)) return@setOnClickListener
viewModel.login(requireContext(), loginString, passwordString)
KeyboardUtils.hideKeyboardFrom(it)
}
}
private fun validateInputData(loginString: String, passwordString: String): Boolean {
var isValidated = true
if (loginString.isEmpty()) {
isValidated = false
setError("Input login", binding.loginLayout)
}
if (passwordString.isEmpty()) {
isValidated = false
setError("Input password", binding.passwordLayout)
}
return isValidated
}
private fun setError(error: String, inputLayout: TextInputLayout) {
inputLayout.error = error
if (errorTimer != null) {
errorTimer?.cancel()
errorTimer = null
}
if (errorTimer == null) {
errorTimer = Timer()
}
errorTimer?.schedule(2500) {
lifecycleScope.launch(Dispatchers.Main) { clearErrors() }
}
}
private fun clearErrors() {
binding.loginLayout.error = ""
binding.passwordLayout.error = ""
}
// TODO: 7/10/2021 extract layout to resources
private fun showCaptchaDialog(captchaImage: String, captchaSid: String) {
val metrics = resources.displayMetrics
val width = (metrics.widthPixels / 3.5).roundToInt()
val height = metrics.heightPixels / 7
val image = ShapeableImageView(requireContext()).also {
it.layoutParams = ViewGroup.LayoutParams(width, height)
}
val shapeModel = image.shapeAppearanceModel
image.shapeAppearanceModel = shapeModel.withCornerSize { 12f }
image.load(captchaImage) { crossfade(100) }
val captchaCodeEditText = TextInputEditText(requireContext())
captchaCodeEditText.setHint(R.string.captcha_hint)
captchaCodeEditText.layoutParams =
LinearLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT
)
val builder = AlertDialog.Builder(requireContext())
val layout = LinearLayout(requireContext())
layout.orientation = LinearLayout.VERTICAL
layout.gravity = Gravity.CENTER
layout.addView(image)
layout.addView(captchaCodeEditText)
builder.setView(layout)
builder.setNegativeButton(android.R.string.cancel, null)
builder.setPositiveButton(android.R.string.ok) { _, _ ->
val captchaCode = captchaCodeEditText.text.toString().trim()
viewModel.login(
requireContext(),
lastEmail,
lastPassword,
"&captcha_sid=$captchaSid&captcha_key=$captchaCode"
)
}
builder.setTitle(R.string.input_captcha)
builder.show()
}
private fun goToValidation(redirectUrl: String) {
findNavController().navigate(
R.id.toValidation,
bundleOf("redirectUrl" to redirectUrl)
)
}
private fun goToMain() {
findNavController().navigate(R.id.toMain)
}
}
@@ -0,0 +1,161 @@
package com.meloda.fast.fragment.login
import android.annotation.SuppressLint
import android.content.Context
import android.os.Bundle
import android.view.ViewGroup
import android.webkit.CookieManager
import android.webkit.JavascriptInterface
import android.webkit.WebView
import android.webkit.WebViewClient
import androidx.lifecycle.viewModelScope
import com.meloda.fast.UserConfig
import com.meloda.fast.api.VKAuth
import com.meloda.fast.base.viewmodel.BaseVM
import com.meloda.fast.base.viewmodel.VKEvent
import kotlinx.coroutines.launch
import org.json.JSONObject
import org.jsoup.Jsoup
class LoginVM : BaseVM() {
fun login(
context: Context,
email: String,
password: String,
captcha: String = ""
) {
val urlToGo = VKAuth.getDirectAuthUrl(email, password, captcha)
// val builder = AlertDialog.Builder(context)
val webView = createWebView(context)
webView.layoutParams = ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT
)
// builder.setTitle("Auth")
// builder.setView(webView)
// builder.show()
webView.addJavascriptInterface(WebViewHandlerInterface(), "HtmlHandler")
webView.webViewClient = object : WebViewClient() {
override fun onPageFinished(view: WebView?, url: String?) {
webView.loadUrl(
"javascript:window.HtmlHandler.handleHtml" +
"('<html>'+document.getElementsByTagName('html')[0].innerHTML+'</html>');"
)
}
}
webView.loadUrl(urlToGo)
}
@SuppressLint("SetJavaScriptEnabled")
private fun createWebView(context: Context): WebView {
val loginWebView = WebView(context)
loginWebView.settings.javaScriptEnabled = true
loginWebView.settings.domStorageEnabled = true
loginWebView.settings.loadsImagesAutomatically = false
loginWebView.settings.userAgentString = "Chrome/41.0.2228.0 Safari/537.36"
loginWebView.clearCache(true)
val cookieManager = CookieManager.getInstance()
cookieManager.removeAllCookies(null)
cookieManager.flush()
cookieManager.setAcceptCookie(false)
return loginWebView
}
@Suppress("MoveVariableDeclarationIntoWhen")
private fun checkResponse(response: JSONObject) {
viewModelScope.launch {
if (response.has("error")) {
val errorString = response.optString("error")
when (errorString) {
"need_validation" -> {
val redirectUrl = response.optString("redirect_uri")
tasksEventChannel.send(GoToValidationEvent(redirectUrl))
// val bundle = Bundle()
// bundle.putString("url", redirectUrl)
/* fragment.setFragmentResultListener("validation") { _, bundle ->
val userId = bundle.getInt("userId")
val token = bundle.getString("token") ?: ""
saveUserData(userId, token)
openMainScreen()
}
fragment.parentFragmentManager.beginTransaction()
.replace(
R.id.fragmentContainer,
ValidationFragment().apply { arguments = bundle })
.addToBackStack("")
.commit()*/
}
"need_captcha" -> {
val captchaImage = response.optString("captcha_img")
val captchaSid = response.optString("captcha_sid")
tasksEventChannel.send(ShowCaptchaDialog(captchaImage, captchaSid))
// showCaptchaDialog(captchaImage, captchaSid)
}
}
} else {
val userId = response.optInt("user_id", -1)
val accessToken = response.optString("access_token")
UserConfig.accessToken = accessToken
UserConfig.userId = userId
tasksEventChannel.send(GoToMainEvent)
// openMainScreen()
// onResponseListener?.onResponse(null)
}
}
}
suspend fun getValidatedData(bundle: Bundle) {
val accessToken = bundle.getString("token") ?: ""
val userId = bundle.getInt("userId")
UserConfig.accessToken = accessToken
UserConfig.userId = userId
tasksEventChannel.send(GoToMainEvent)
}
fun checkUserSession() = viewModelScope.launch {
if (UserConfig.isLoggedIn()) tasksEventChannel.send(GoToMainEvent)
}
inner class WebViewHandlerInterface {
@JavascriptInterface
fun handleHtml(html: String) {
val doc = Jsoup.parse(html)
val responseString =
doc.select("pre[style=\"word-wrap: break-word; white-space: pre-wrap;\"]").first()
?.text() ?: ""
checkResponse(JSONObject(responseString))
}
}
}
data class ShowCaptchaDialog(val captchaImage: String, val captchaSid: String) : VKEvent()
data class GoToValidationEvent(val redirectUrl: String) : VKEvent()
object GoToMainEvent : VKEvent()
@@ -0,0 +1,75 @@
package com.meloda.fast.fragment.login
import android.graphics.Bitmap
import android.os.Bundle
import android.util.Log
import android.view.View
import android.view.ViewGroup
import android.viewbinding.library.fragment.viewBinding
import android.webkit.CookieManager
import android.webkit.WebView
import android.webkit.WebViewClient
import androidx.core.os.bundleOf
import androidx.navigation.fragment.findNavController
import com.meloda.fast.R
import com.meloda.fast.api.VKAuth
import com.meloda.fast.base.BaseFragment
import com.meloda.fast.databinding.FragmentValidationBinding
class ValidationFragment : BaseFragment(R.layout.fragment_validation) {
private val binding: FragmentValidationBinding by viewBinding()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val redirectUrl = getRedirectUrl()
binding.webView.webViewClient = object : WebViewClient() {
override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {
Log.d("Fast::Validation", "onPageStarted: url: $url")
parseUrl(url ?: "")
}
}
binding.webView.settings.domStorageEnabled = true
binding.webView.clearCache(true)
binding.webView.layoutParams = ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
)
val manager = CookieManager.getInstance()
manager.removeAllCookies(null)
manager.flush()
manager.setAcceptCookie(true)
binding.webView.loadUrl(redirectUrl)
}
private fun getRedirectUrl() = requireArguments().getString("redirectUrl", "")
private fun parseUrl(url: String) {
if (url.startsWith("https://oauth.vk.com/blank.html#success=1")) {
if (!url.contains("error=")) {
val data = VKAuth.parseRedirectUrl(url)
val accessToken = data.first
val userId = data.second
parentFragmentManager.setFragmentResult(
"validation",
bundleOf(
"accessToken" to accessToken,
"userId" to userId
)
)
findNavController().navigate(R.id.toLogin)
}
} else {
Log.d("Fast::Validation", "parseUrl: $url")
}
}
}
@@ -0,0 +1,38 @@
package com.meloda.fast.fragment.main
import android.os.Bundle
import android.view.View
import android.viewbinding.library.fragment.viewBinding
import androidx.fragment.app.viewModels
import com.meloda.fast.R
import com.meloda.fast.base.BaseVMFragment
import com.meloda.fast.databinding.FragmentMainBinding
import com.meloda.fast.extensions.NavigationExtensions.setupWithNavController
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class MainFragment : BaseVMFragment<MainVM>(R.layout.fragment_main) {
override val viewModel: MainVM by viewModels()
private val binding: FragmentMainBinding by viewBinding()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
if (savedInstanceState == null) setupBottomBar()
}
private fun setupBottomBar() {
val navGraphIds = listOf(R.id.messages, R.id.friends)
binding.bottomBar.setupWithNavController(
navGraphIds = listOf(),
fragmentManager = childFragmentManager,
containerId = R.id.fragmentContainer,
intent = requireActivity().intent
)
}
}
@@ -0,0 +1,5 @@
package com.meloda.fast.fragment.main
import com.meloda.fast.base.viewmodel.BaseVM
class MainVM : BaseVM()
@@ -0,0 +1,9 @@
package com.meloda.fast.fragment.messages
import com.meloda.fast.R
import com.meloda.fast.base.BaseFragment
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class ConversationsFragment : BaseFragment(R.layout.fragment_conversations) {
}
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="?colorAccent" android:state_focused="true" />
<item android:alpha="0.87" android:color="?colorOnSurface" android:state_hovered="true" />
<item android:alpha="0.12" android:color="?colorOnSurface" android:state_enabled="false" />
<item android:alpha="0.38" android:color="?colorOnSurface" />
</selector>
+16 -38
View File
@@ -1,43 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.drawerlayout.widget.DrawerLayout xmlns:android="http://schemas.android.com/apk/res/android"
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/drawerLayout"
xmlns:tools="http://schemas.android.com/tools">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<FrameLayout
android:id="@+id/fragmentContainer"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<com.google.android.material.bottomnavigation.BottomNavigationView
android:id="@+id/bottomBar"
android:layout_width="match_parent"
android:layout_height="48dp"
android:layout_gravity="bottom"
android:visibility="gone"
app:backgroundTint="?colorPrimary"
app:elevation="0.5dp"
app:itemIconTint="@drawable/navigation_view_items_colors"
app:itemTextColor="@drawable/navigation_view_items_colors"
app:labelVisibilityMode="unlabeled"
app:menu="@menu/activity_main_bottom" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
<com.google.android.material.navigation.NavigationView
android:id="@+id/navigationView"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="start"
app:headerLayout="@layout/activity_main_drawer_header"
app:itemBackground="@drawable/navigation_view_item_background_selector"
app:itemIconTint="@drawable/navigation_view_item_icon_colors"
app:itemTextColor="@drawable/navigation_view_item_text_colors"
app:menu="@menu/activity_main_drawer" />
android:orientation="vertical"
tools:context=".MainActivity">
</androidx.drawerlayout.widget.DrawerLayout>
<androidx.fragment.app.FragmentContainerView
android:id="@+id/fragmentContainer"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
app:defaultNavHost="true"
app:navGraph="@navigation/main" />
</LinearLayout>
</layout>
@@ -18,7 +18,11 @@
android:layout_height="wrap_content"
app:layout_scrollFlags="scroll|enterAlways">
<include layout="@layout/toolbar" />
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?actionBarSize"
app:titleCentered="true" />
</com.google.android.material.appbar.CollapsingToolbarLayout>
</com.google.android.material.appbar.AppBarLayout>
+71 -68
View File
@@ -1,6 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.appcompat.widget.LinearLayoutCompat xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<androidx.appcompat.widget.LinearLayoutCompat
android:id="@+id/loginRoot"
android:layout_width="match_parent"
android:layout_height="match_parent"
@@ -11,125 +13,126 @@
android:layout_height="match_parent"
android:fillViewport="true">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.material.card.MaterialCardView
android:id="@+id/loginCard"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:minWidth="380dp"
app:cardCornerRadius="9dp"
app:cardElevation="0dp"
app:strokeColor="?dividerHorizontal"
app:strokeWidth="2dp">
<LinearLayout
android:id="@+id/loginCardLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:id="@+id/loginLogoText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="12dp"
android:fontFamily="@font/google_sans_regular"
android:text="@string/fast_messenger"
android:textColor="@color/login_secondary_color"
android:textSize="26sp" />
<FrameLayout
android:id="@+id/loginLogoContainer"
android:layout_width="176dp"
android:layout_height="176dp"
android:id="@+id/logoContainer"
android:layout_width="140dp"
android:layout_height="140dp"
android:layout_gravity="center_horizontal"
android:layout_marginTop="12dp"
android:background="@drawable/ic_logo_fast_border">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/loginLogoImage"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
<com.meloda.fast.widget.CircleImageView
android:id="@+id/logoImage"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
android:padding="42dp"
android:src="@drawable/ic_fast_lightning"
app:tint="?colorAccent" />
</FrameLayout>
<LinearLayout
android:id="@+id/loginEmailContainer"
android:id="@+id/loginContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:layout_marginTop="48dp"
android:orientation="horizontal">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/loginEmailImage"
android:id="@+id/loginImage"
style="@style/AppTheme.Login.EditText.Icon"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:src="@drawable/ic_email" />
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:src="@drawable/ic_baseline_account_circle_24"
app:tint="?colorAccent" />
<androidx.appcompat.widget.AppCompatEditText
android:id="@+id/loginEmailEditText"
style="@style/AppTheme.Login.EditText"
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/loginLayout"
style="@style/Widget.TextInputLayout.NoError.Dense"
android:layout_width="match_parent"
android:hint="@string/email_login_hint"
android:layout_height="wrap_content">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/loginInput"
android:layout_width="match_parent"
android:layout_height="48dp"
android:hint="@string/login_hint"
android:imeOptions="actionGo"
android:inputType="textEmailAddress" />
android:inputType="textEmailAddress"
android:textCursorDrawable="@null" />
</com.google.android.material.textfield.TextInputLayout>
</LinearLayout>
<LinearLayout
android:id="@+id/loginPasswordContainer"
android:id="@+id/passwordContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/activity_vertical_margin"
android:orientation="horizontal">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/loginPasswordImage"
android:id="@+id/passwordImage"
style="@style/AppTheme.Login.EditText.Icon"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:src="@drawable/ic_key" />
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:src="@drawable/ic_key"
app:tint="?colorAccent" />
<androidx.appcompat.widget.AppCompatEditText
android:id="@+id/loginPasswordEditText"
style="@style/AppTheme.Login.EditText"
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/passwordLayout"
style="@style/Widget.TextInputLayout.NoError.Dense"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:passwordToggleEnabled="true"
app:passwordToggleTint="?colorAccent">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/passwordInput"
android:layout_width="match_parent"
android:layout_height="48dp"
android:hint="@string/password_login_hint"
android:inputType="textPassword" />
android:inputType="textPassword"
android:textCursorDrawable="@null" />
</com.google.android.material.textfield.TextInputLayout>
</LinearLayout>
<com.google.android.material.button.MaterialButton
android:id="@+id/loginAuthorize"
style="@style/Widget.MaterialComponents.Button.UnelevatedButton"
android:id="@+id/authorize"
style="@style/Widget.MaterialButton"
android:layout_width="wrap_content"
android:layout_height="60dp"
android:layout_gravity="center_horizontal"
android:layout_gravity="center_horizontal|bottom"
android:layout_marginTop="12dp"
android:fontFamily="@font/google_sans_regular"
android:fontFamily="@font/google_sans_medium"
android:letterSpacing="0"
android:paddingStart="36dp"
android:paddingEnd="36dp"
android:paddingStart="24dp"
android:paddingEnd="16dp"
android:text="@string/log_in"
android:textColor="@color/login_button_text_color"
android:textColor="?colorAccent"
android:textSize="14sp"
app:backgroundTint="?colorAccent"
app:cornerRadius="50dp" />
app:backgroundTint="?colorSurface"
app:cornerRadius="50dp"
app:elevation="16dp"
app:icon="@drawable/ic_arrow_end"
app:iconGravity="end"
app:iconTint="?colorAccent"
app:rippleColor="?colorAccent" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
</RelativeLayout>
</androidx.core.widget.NestedScrollView>
</androidx.appcompat.widget.LinearLayoutCompat>
</layout>
+29
View File
@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.fragment.app.FragmentContainerView
android:id="@+id/fragmentContainer"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginBottom="48dp" />
<com.google.android.material.bottomnavigation.BottomNavigationView
android:id="@+id/bottomBar"
android:layout_width="match_parent"
android:layout_height="48dp"
android:layout_gravity="bottom"
app:backgroundTint="?colorPrimary"
app:elevation="0.5dp"
app:itemIconTint="@drawable/navigation_view_items_colors"
app:itemTextColor="@drawable/navigation_view_items_colors"
app:labelVisibilityMode="unlabeled"
app:menu="@menu/activity_main_bottom" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</layout>
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<WebView
android:id="@+id/webView"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</layout>
+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/friends">
</navigation>
+46
View File
@@ -0,0 +1,46 @@
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main"
app:startDestination="@id/loginFragment">
<fragment
android:id="@+id/mainFragment"
android:name="com.meloda.fast.fragment.main.MainFragment"
android:label="MainFragment"
tools:layout="@layout/fragment_main" />
<fragment
android:id="@+id/loginFragment"
android:name="com.meloda.fast.fragment.login.LoginFragment"
android:label="LoginFragment"
tools:layout="@layout/fragment_login">
<action
android:id="@+id/toMain"
app:destination="@id/mainFragment"
app:popUpTo="@id/loginFragment"
app:popUpToInclusive="true" />
<action
android:id="@+id/toValidation"
app:destination="@id/validationFragment" />
</fragment>
<fragment
android:id="@+id/validationFragment"
android:name="com.meloda.fast.fragment.login.ValidationFragment"
android:label="ValidationFragment"
tools:layout="@layout/fragment_validation">
<action
android:id="@+id/toLogin"
app:destination="@id/loginFragment"
app:popUpTo="@id/validationFragment"
app:popUpToInclusive="true" />
</fragment>
</navigation>
+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/messages">
</navigation>
-48
View File
@@ -1,48 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools">
<style name="AppTheme" parent="Theme.MaterialComponents.NoActionBar.Bridge">
<item name="colorPrimary">@color/dark_primary</item>
<item name="colorPrimaryDark">@color/dark_primaryDark</item>
<item name="colorAccent">@color/dark_accent</item>
<item name="toolbarStyle">@style/AppTheme.Toolbar</item>
<item name="preferenceTheme">@style/PreferenceThemeOverlay</item>
<item name="dialogCornerRadius">12dp</item>
<item name="android:windowBackground">@color/dark_background</item>
<item name="itemTitleColor">@android:color/white</item>
<item name="dividerHorizontal">@color/dark_divider</item>
<item name="textColorSecondary">@color/dark_textSecondary</item>
<item name="editTextFilledBackgroundColor">@color/dark_edittext_filled_background</item>
<item name="messageInTextColor">@color/dark_message_in</item>
<item name="messageOutTextColor">@color/dark_message_out</item>
<item name="android:windowAnimationStyle">@style/AppTheme.ActivityAnimation</item>
<item name="android:navigationBarColor" tools:targetApi="M">@color/dark_navigationBar</item>
<item name="android:navigationBarDividerColor" tools:targetApi="o_mr1">@android:color/transparent</item>
</style>
<style name="AppTheme.FullScreenDialog" parent="Theme.MaterialComponents.Dialog.Bridge">
<item name="colorPrimary">@color/dark_primary</item>
<item name="colorPrimaryDark">@color/dark_primaryDark</item>
<item name="colorAccent">@color/dark_accent</item>
<item name="android:windowIsFloating">false</item>
<item name="android:windowBackground">@color/dark_background</item>
<item name="actionMenuTextColor">?colorAccent</item>
<item name="android:navigationBarColor" tools:targetApi="o_mr1">?colorPrimaryDark</item>
<item name="android:windowLightNavigationBar" tools:targetApi="o_mr1">true</item>
<item name="android:navigationBarDividerColor" tools:targetApi="o_mr1">
@android:color/transparent
</item>
<item name="colorControlNormal">?colorAccent</item>
</style>
<style name="AppTheme.ProfileDialog" parent="Theme.MaterialComponents.BottomSheetDialog">
<item name="colorAccent">@color/dark_accent</item>
<item name="colorPrimary">@color/dark_primary</item>
<item name="colorPrimaryDark">@color/dark_primaryDark</item>
</style>
</resources>
+1
View File
@@ -153,5 +153,6 @@
<string name="captcha_hint">Captcha</string>
<string name="input_captcha">Input code from picture</string>
<string name="login_hint">Login</string>
</resources>
+21 -2
View File
@@ -5,7 +5,9 @@
<item name="colorPrimary">@color/primary</item>
<item name="colorPrimaryDark">@color/primaryDark</item>
<item name="colorAccent">@color/accent</item>
<item name="toolbarStyle">@style/AppTheme.Toolbar</item>
<item name="android:windowLightStatusBar">true</item>
<item name="preferenceTheme">@style/PreferenceThemeOverlay</item>
<item name="dialogCornerRadius">12dp</item>
@@ -26,14 +28,14 @@
@android:color/transparent
</item>
</style>
<style name="AppTheme.Toolbar" parent="Widget.MaterialComponents.Toolbar.PrimarySurface">
<item name="titleTextAppearance">@style/Toolbar.Title</item>
<item name="android:textSize">24sp</item>
<item name="android:elevation">3dp</item>
<item name="titleTextColor">?colorAccent</item>
<item name="android:background">?colorPrimary</item>
<item name="titleTextColor">@color/accent</item>
</style>
<style name="Toolbar.Title" parent="TextAppearance.MaterialComponents.Body1">
@@ -86,4 +88,21 @@
<item name="colorPrimaryDark">@color/primaryDark</item>
</style>
<style name="Widget.TextInputLayout" parent="Widget.MaterialComponents.TextInputLayout.OutlinedBox">
<item name="errorEnabled">true</item>
</style>
<style name="Widget.TextInputLayout.NoError.Dense" parent="Widget.MaterialComponents.TextInputLayout.OutlinedBox.Dense">
<item name="errorEnabled">true</item>
<item name="hintTextColor">?colorAccent</item>
<item name="boxStrokeColor">@drawable/edit_text_box_background</item>
<item name="boxStrokeErrorColor">?colorAccent</item>
<item name="boxStrokeWidth">2dp</item>
</style>
<style name="Widget.MaterialButton" parent="Widget.MaterialComponents.Button">
</style>
</resources>
+5 -2
View File
@@ -5,8 +5,11 @@ buildscript {
mavenCentral()
}
dependencies {
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:${Versions.kotlin}")
classpath("com.android.tools.build:gradle:${Versions.gradlePlugin}")
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.5.20")
classpath("com.android.tools.build:gradle:4.2.2")
classpath("androidx.navigation:navigation-safe-args-gradle-plugin:2.3.5")
classpath("com.google.dagger:hilt-android-gradle-plugin:2.37")
}
}
-9
View File
@@ -1,9 +0,0 @@
import org.gradle.kotlin.dsl.`kotlin-dsl`
plugins {
`kotlin-dsl`
}
repositories {
mavenCentral()
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1 +0,0 @@
D:\Workspace\Android\fast-messenger\buildSrc\build\classes\kotlin\main\BuildPlugins$android$2.class;D:\Workspace\Android\fast-messenger\buildSrc\build\classes\kotlin\main\BuildPlugins$kotlin$2.class;D:\Workspace\Android\fast-messenger\buildSrc\build\classes\kotlin\main\BuildPlugins.class;D:\Workspace\Android\fast-messenger\buildSrc\build\classes\kotlin\main\ConfigData.class;D:\Workspace\Android\fast-messenger\buildSrc\build\classes\kotlin\main\Deps$acra$2.class;D:\Workspace\Android\fast-messenger\buildSrc\build\classes\kotlin\main\Deps$appCompat$2.class;D:\Workspace\Android\fast-messenger\buildSrc\build\classes\kotlin\main\Deps$cardView$2.class;D:\Workspace\Android\fast-messenger\buildSrc\build\classes\kotlin\main\Deps$core$2.class;D:\Workspace\Android\fast-messenger\buildSrc\build\classes\kotlin\main\Deps$coroutineAndroid$2.class;D:\Workspace\Android\fast-messenger\buildSrc\build\classes\kotlin\main\Deps$coroutineCore$2.class;D:\Workspace\Android\fast-messenger\buildSrc\build\classes\kotlin\main\Deps$desugaring$2.class;D:\Workspace\Android\fast-messenger\buildSrc\build\classes\kotlin\main\Deps$fragment$2.class;D:\Workspace\Android\fast-messenger\buildSrc\build\classes\kotlin\main\Deps$gson$2.class;D:\Workspace\Android\fast-messenger\buildSrc\build\classes\kotlin\main\Deps$jsoup$2.class;D:\Workspace\Android\fast-messenger\buildSrc\build\classes\kotlin\main\Deps$kotlin$2.class;D:\Workspace\Android\fast-messenger\buildSrc\build\classes\kotlin\main\Deps$material$2.class;D:\Workspace\Android\fast-messenger\buildSrc\build\classes\kotlin\main\Deps$preferences$2.class;D:\Workspace\Android\fast-messenger\buildSrc\build\classes\kotlin\main\Deps$recyclerView$2.class;D:\Workspace\Android\fast-messenger\buildSrc\build\classes\kotlin\main\Deps$roomCompiler$2.class;D:\Workspace\Android\fast-messenger\buildSrc\build\classes\kotlin\main\Deps$roomRuntime$2.class;D:\Workspace\Android\fast-messenger\buildSrc\build\classes\kotlin\main\Deps$swipeRefreshLayout$2.class;D:\Workspace\Android\fast-messenger\buildSrc\build\classes\kotlin\main\Deps.class;D:\Workspace\Android\fast-messenger\buildSrc\build\classes\kotlin\main\Versions.class

Some files were not shown because too many files have changed in this diff Show More