diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 55805e6..705a308 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -10,12 +10,12 @@ val packageName = "ru.myitschool.work" android { namespace = packageName - compileSdk = Version.Android.Sdk.compile + compileSdk = 35 defaultConfig { applicationId = packageName - minSdk = Version.Android.Sdk.min - targetSdk = Version.Android.Sdk.target + minSdk = 35 + targetSdk = 35 versionCode = 1 versionName = "1.0" diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 44f6361..2ae5448 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,6 +1,6 @@ + xmlns:tools="http://schemas.android.com/tools"> @@ -8,7 +8,6 @@ + tools:targetApi="31"> - + android:exported="true"> diff --git a/app/src/main/java/ru/myitschool/work/ui/login/LoginActivity.java b/app/src/main/java/ru/myitschool/work/ui/login/LoginActivity.java new file mode 100644 index 0000000..a2ef4b8 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/ui/login/LoginActivity.java @@ -0,0 +1,115 @@ +package ru.myitschool.work.ui.login; + +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.os.Bundle; +import android.text.Editable; +import android.view.View; + +import androidx.activity.EdgeToEdge; +import androidx.appcompat.app.AppCompatActivity; +import androidx.core.graphics.Insets; +import androidx.core.view.ViewCompat; +import androidx.core.view.WindowInsetsCompat; +import androidx.lifecycle.ViewModelProvider; + +import com.example.myapplication.core.SettingConstants; +import com.google.android.material.snackbar.Snackbar; +import com.google.android.material.textfield.TextInputEditText; + +import ru.myitschool.work.R; +import ru.myitschool.work.databinding.ActivityLoginBinding; +import ru.myitschool.work.ui.main.MainActivity; + +public class LoginActivity extends AppCompatActivity { + + private ActivityLoginBinding binding; + private LoginViewModel viewModel; + + + public LoginActivity() { + super(R.layout.activity_login); + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + EdgeToEdge.enable(this); + setContentView(R.layout.activity_login); + ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> { + Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars()); + v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom); + return insets; + + binding = ActivityLoginBinding.bind(v); + viewModel = new ViewModelProvider(this).get(ActivityLoginBinding.class); + + binding.email.addTextChangedListener(new TextChangedListener<>(binding.email) { + @Override + public void onTextChanged(TextInputEditText target, Editable s) { + listenerEmailEditText(s); + } + }); + + binding.password.addTextChangedListener(new TextChangedListener<>(binding.password) { + @Override + public void onTextChanged(TextInputEditText target, Editable s) { + listenerPasswordEditText(s); + } + }); + + binding.btEnter.setOnClickListener(this.onClickListenerLoginButton); + + subscribe(); + }); + } + + private void subscribe() { + viewModel.errorLiveData.observe(getViewLifecycleOwner(), error -> { + binding.btEnter.setEnabled(true); + Snackbar.make(requireView(), error, Snackbar.LENGTH_LONG).show(); + }); + viewModel.openLiveData.observe(getViewLifecycleOwner(), employee -> { + binding.btEnter.setEnabled(true); + + SharedPreferences settings = requireView().getContext().getSharedPreferences( + SettingConstants.PREFS_FILE, Context.MODE_PRIVATE + ); + settings.edit().putLong(SettingConstants.PREF_ID, employee.getId()).apply(); + settings.edit().putString(SettingConstants.PREF_ROLE, employee.getRole()).apply(); + + startActivity(new Intent(getApplicationContext(), MainActivity.class)); + }); + } + + private void listenerEmailEditText(Editable s) { + if (s.toString().isEmpty()) { + binding.email.setError("Обязательное поле"); + } else if (!isEmailValid(s.toString())) { + binding.emailLay.setError("Неверный формат"); + } else { + binding.emailLay.setErrorEnabled(false); + } + } + + private void listenerPasswordEditText(Editable s) { + if (s.toString().isEmpty()) { + binding.passwordLay.setError("Обязательное поле"); + } else { + binding.passwordLay.setErrorEnabled(false); + } + } + + private void onClickListenerLoginButton(View view) { + binding.btEnter.setEnabled(false); + + viewModel.changeEmail(String.valueOf(binding.email.getText())); + viewModel.changePassword(String.valueOf(binding.password.getText())); + viewModel.confirm(); + } + + private boolean isEmailValid(String email) { + return android.util.Patterns.EMAIL_ADDRESS.matcher(email).matches(); + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/ui/login/LoginViewModel.java b/app/src/main/java/ru/myitschool/work/ui/login/LoginViewModel.java new file mode 100644 index 0000000..2b7a25d --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/ui/login/LoginViewModel.java @@ -0,0 +1,56 @@ +package ru.myitschool.work.ui.login; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; + +import ru.myitschool.work.api.data.SignRepositoryImpl; +import ru.myitschool.work.api.domain.entity.employee.EmpolyeeEntity; +import ru.myitschool.work.api.domain.useCases.sign.LoginEmployeeUseCase; + +public class LoginViewModel { + private final MutableLiveData mutableErrorLiveData = new MutableLiveData<>(); + public final LiveData errorLiveData = mutableErrorLiveData; + + private final MutableLiveData mutableOpenLiveData = new MutableLiveData<>(); + public final LiveData openLiveData = mutableOpenLiveData; + + private final LoginEmployeeUseCase loginEmployeeUseCase = new LoginEmployeeUseCase( + SignRepositoryImpl.getInstance() + ); + + @Nullable + private String email; + @Nullable + private String password; + + public void changeEmail(@NonNull String email) { + this.email = email; + } + + public void changePassword(@NonNull String password) { + this.password = password; + } + + public void confirm() { + final String currentEmail = email; + final String currentPassword = password; + + if (currentEmail == null || currentEmail.isEmpty()) { + mutableErrorLiveData.postValue("Пароль пустой!"); + return; + } + if (currentPassword == null || currentPassword.isEmpty()) { + mutableErrorLiveData.postValue("email пустой!"); + return; + } + loginEmployeeUseCase.execute(currentEmail, currentPassword, status -> { + if (status.getStatusCode() == 200 && status.getErrors() == null && status.getValue() != null) { + mutableOpenLiveData.postValue(status.getValue()); + } else if (status.getStatusCode() == 401) mutableErrorLiveData.postValue("Данные не верны. Попробуйте ещё разок :("); + else mutableErrorLiveData.postValue("Вы не подключены к интернету :("); + }); + } + +} diff --git a/app/src/main/java/ru/myitschool/work/ui/main/MainActivity.java b/app/src/main/java/ru/myitschool/work/ui/main/MainActivity.java new file mode 100644 index 0000000..339e6a7 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/ui/main/MainActivity.java @@ -0,0 +1,26 @@ +package ru.myitschool.work.ui.main; + +import android.os.Bundle; + +import androidx.activity.EdgeToEdge; +import androidx.appcompat.app.AppCompatActivity; +import androidx.core.graphics.Insets; +import androidx.core.view.ViewCompat; +import androidx.core.view.WindowInsetsCompat; + +import ru.myitschool.work.R; + +public class MainActivity extends AppCompatActivity { + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + EdgeToEdge.enable(this); + setContentView(R.layout.activity_main); + ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> { + Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars()); + v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom); + return insets; + }); + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/ui/qr/scan/QrScanDestination.kt b/app/src/main/java/ru/myitschool/work/ui/qr/scan/QrScanDestination.kt deleted file mode 100644 index 7e34b28..0000000 --- a/app/src/main/java/ru/myitschool/work/ui/qr/scan/QrScanDestination.kt +++ /dev/null @@ -1,30 +0,0 @@ -package ru.myitschool.work.ui.qr.scan - -import android.os.Bundle -import androidx.core.os.bundleOf -import kotlinx.serialization.Serializable - -// НЕ ИЗМЕНЯЙТЕ ЭТОТ ФАЙЛ. В ТЕСТАХ ОН БУДЕМ ВОЗВРАЩЁН В ИСХОДНОЕ СОСТОЯНИЕ -@Serializable -data object QrScanDestination { - const val REQUEST_KEY = "qr_result" - private const val KEY_QR_DATA = "key_qr" - - fun newInstance(): QrScanFragment { - return QrScanFragment() - } - - fun getDataIfExist(bundle: Bundle): String? { - return if (bundle.containsKey(KEY_QR_DATA)) { - bundle.getString(KEY_QR_DATA) - } else { - null - } - } - - internal fun packToBundle(data: String): Bundle { - return bundleOf( - KEY_QR_DATA to data - ) - } -} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/ui/qr/scan/QrScanFragment.kt b/app/src/main/java/ru/myitschool/work/ui/qr/scan/QrScanFragment.kt deleted file mode 100644 index a9ddaab..0000000 --- a/app/src/main/java/ru/myitschool/work/ui/qr/scan/QrScanFragment.kt +++ /dev/null @@ -1,139 +0,0 @@ -package ru.myitschool.work.ui.qr.scan - -import android.os.Bundle -import android.view.View -import androidx.activity.result.contract.ActivityResultContracts -import androidx.camera.core.ImageAnalysis -import androidx.camera.mlkit.vision.MlKitAnalyzer -import androidx.camera.view.LifecycleCameraController -import androidx.camera.view.PreviewView -import androidx.core.content.ContextCompat -import androidx.core.os.bundleOf -import androidx.fragment.app.Fragment -import androidx.fragment.app.setFragmentResult -import androidx.fragment.app.viewModels -import androidx.navigation.NavController -import androidx.navigation.fragment.findNavController -import com.google.mlkit.vision.barcode.BarcodeScanner -import com.google.mlkit.vision.barcode.BarcodeScannerOptions -import com.google.mlkit.vision.barcode.BarcodeScanning -import com.google.mlkit.vision.barcode.common.Barcode -import ru.myitschool.work.R -import ru.myitschool.work.databinding.FragmentQrScanBinding -import ru.myitschool.work.utils.collectWhenStarted -import ru.myitschool.work.utils.visibleOrGone - -// НЕ ИЗМЕНЯЙТЕ ЭТОТ ФАЙЛ. В ТЕСТАХ ОН БУДЕМ ВОЗВРАЩЁН В ИСХОДНОЕ СОСТОЯНИЕ -class QrScanFragment : Fragment(R.layout.fragment_qr_scan) { - private var _binding: FragmentQrScanBinding? = null - private val binding: FragmentQrScanBinding get() = _binding!! - - private var barcodeScanner: BarcodeScanner? = null - private var isCameraInit: Boolean = false - private val permissionLauncher = registerForActivityResult( - ActivityResultContracts.RequestPermission() - ) { isGranted -> viewModel.onPermissionResult(isGranted) } - - private val viewModel: QrScanViewModel by viewModels() - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - _binding = FragmentQrScanBinding.bind(view) - sendResult(bundleOf()) - subscribe() - initCallback() - } - - private fun initCallback() { - binding.close.setOnClickListener { viewModel.close() } - } - - private fun subscribe() { - viewModel.state.collectWhenStarted(this) { state -> - binding.loading.visibleOrGone(state is QrScanViewModel.State.Loading) - binding.viewFinder.visibleOrGone(state is QrScanViewModel.State.Scan) - if (!isCameraInit && state is QrScanViewModel.State.Scan) { - startCamera() - isCameraInit = true - } - } - - viewModel.action.collectWhenStarted(this) { action -> - when (action) { - is QrScanViewModel.Action.RequestPermission -> requestPermission(action.permission) - is QrScanViewModel.Action.CloseWithCancel -> { - goBack() - } - is QrScanViewModel.Action.CloseWithResult -> { - sendResult(QrScanDestination.packToBundle(action.result)) - goBack() - } - } - } - } - - private fun requestPermission(permission: String) { - permissionLauncher.launch(permission) - } - - private fun startCamera() { - val context = requireContext() - val cameraController = LifecycleCameraController(context) - val previewView: PreviewView = binding.viewFinder - val executor = ContextCompat.getMainExecutor(context) - - val options = BarcodeScannerOptions.Builder() - .setBarcodeFormats(Barcode.FORMAT_QR_CODE) - .build() - val barcodeScanner = BarcodeScanning.getClient(options) - this.barcodeScanner = barcodeScanner - - cameraController.setImageAnalysisAnalyzer( - executor, - MlKitAnalyzer( - listOf(barcodeScanner), - ImageAnalysis.COORDINATE_SYSTEM_VIEW_REFERENCED, - executor - ) { result -> - result?.getValue(barcodeScanner)?.firstOrNull()?.let { value -> - viewModel.findBarcode(value) - - } - } - ) - - cameraController.bindToLifecycle(this) - previewView.controller = cameraController - } - - override fun onDestroyView() { - barcodeScanner?.close() - barcodeScanner = null - _binding = null - super.onDestroyView() - } - - private fun goBack() { - findNavControllerOrNull()?.popBackStack() - ?: requireActivity().onBackPressedDispatcher.onBackPressed() - } - - private fun sendResult(bundle: Bundle) { - setFragmentResult( - QrScanDestination.REQUEST_KEY, - bundle - ) - findNavControllerOrNull() - ?.previousBackStackEntry - ?.savedStateHandle - ?.set(QrScanDestination.REQUEST_KEY, bundle) - } - - private fun findNavControllerOrNull(): NavController? { - return try { - findNavController() - } catch (_: Throwable) { - null - } - } -} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/ui/qr/scan/QrScanViewModel.kt b/app/src/main/java/ru/myitschool/work/ui/qr/scan/QrScanViewModel.kt deleted file mode 100644 index 14565ab..0000000 --- a/app/src/main/java/ru/myitschool/work/ui/qr/scan/QrScanViewModel.kt +++ /dev/null @@ -1,93 +0,0 @@ -package ru.myitschool.work.ui.qr.scan - -import android.Manifest -import android.app.Application -import android.content.pm.PackageManager -import androidx.core.content.ContextCompat -import androidx.lifecycle.AndroidViewModel -import androidx.lifecycle.viewModelScope -import com.google.mlkit.vision.barcode.common.Barcode -import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asSharedFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.update -import kotlinx.coroutines.launch -import ru.myitschool.work.utils.MutablePublishFlow - -// НЕ ИЗМЕНЯЙТЕ ЭТОТ ФАЙЛ. В ТЕСТАХ ОН БУДЕМ ВОЗВРАЩЁН В ИСХОДНОЕ СОСТОЯНИЕ -class QrScanViewModel( - application: Application -) : AndroidViewModel(application) { - - private val _action = MutablePublishFlow() - val action = _action.asSharedFlow() - - private val _state = MutableStateFlow(initialState) - val state = _state.asStateFlow() - - init { - checkPermission() - } - - fun onPermissionResult(isGranted: Boolean) { - viewModelScope.launch { - if (isGranted) { - _state.update { State.Scan } - } else { - _action.emit(Action.CloseWithCancel) - } - } - } - - private fun checkPermission() { - viewModelScope.launch { - val isPermissionGranted = ContextCompat.checkSelfPermission( - getApplication(), - CAMERA_PERMISSION - ) == PackageManager.PERMISSION_GRANTED - if (isPermissionGranted) { - _state.update { State.Scan } - } else { - delay(1000) - _action.emit(Action.RequestPermission(CAMERA_PERMISSION)) - } - } - } - - fun findBarcode(barcode: Barcode) { - viewModelScope.launch { - barcode.rawValue?.let { value -> - _action.emit(Action.CloseWithResult(value)) - } - } - } - - fun close() { - viewModelScope.launch { - _action.emit(Action.CloseWithCancel) - } - } - - sealed interface State { - data object Loading : State - - data object Scan : State - } - - sealed interface Action { - data class RequestPermission( - val permission: String - ) : Action - data object CloseWithCancel : Action - data class CloseWithResult( - val result: String - ) : Action - } - - private companion object { - val initialState = State.Loading - - const val CAMERA_PERMISSION = Manifest.permission.CAMERA - } -} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/utils/TextChangedListener.java b/app/src/main/java/ru/myitschool/work/utils/TextChangedListener.java new file mode 100644 index 0000000..4e3be1e --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/utils/TextChangedListener.java @@ -0,0 +1,30 @@ +package ru.myitschool.work.utils; + +import android.text.Editable; +import android.text.TextWatcher; + +public abstract class TextChangedListener implements TextWatcher { + private final T target; + + public TextChangedListener(T target) { + this.target = target; + } + + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + + } + + @Override + public void afterTextChanged(Editable s) { + + } + + public abstract void onTextChanged(T target, Editable s); +} + diff --git a/app/src/main/java/ru/myitschool/work/utils/TextChangedListener.kt b/app/src/main/java/ru/myitschool/work/utils/TextChangedListener.kt deleted file mode 100644 index c81147d..0000000 --- a/app/src/main/java/ru/myitschool/work/utils/TextChangedListener.kt +++ /dev/null @@ -1,12 +0,0 @@ -package ru.myitschool.work.utils - -import android.text.Editable -import android.text.TextWatcher - -open class TextChangedListener: TextWatcher { - override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) = Unit - - override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) = Unit - - override fun afterTextChanged(s: Editable?) = Unit -} \ No newline at end of file diff --git a/app/src/main/res/drawable/account.png b/app/src/main/res/drawable/account.png new file mode 100644 index 0000000..0afe7f4 Binary files /dev/null and b/app/src/main/res/drawable/account.png differ diff --git a/app/src/main/res/drawable/background.png b/app/src/main/res/drawable/background.png new file mode 100644 index 0000000..012ddd9 Binary files /dev/null and b/app/src/main/res/drawable/background.png differ diff --git a/app/src/main/res/drawable/management.png b/app/src/main/res/drawable/management.png new file mode 100644 index 0000000..6c11a52 Binary files /dev/null and b/app/src/main/res/drawable/management.png differ diff --git a/app/src/main/res/drawable/office.png b/app/src/main/res/drawable/office.png new file mode 100644 index 0000000..82b5ef5 Binary files /dev/null and b/app/src/main/res/drawable/office.png differ diff --git a/app/src/main/res/drawable/scanqr.png b/app/src/main/res/drawable/scanqr.png new file mode 100644 index 0000000..88b9025 Binary files /dev/null and b/app/src/main/res/drawable/scanqr.png differ diff --git a/app/src/main/res/layout/activity_login.xml b/app/src/main/res/layout/activity_login.xml new file mode 100644 index 0000000..b9e5d30 --- /dev/null +++ b/app/src/main/res/layout/activity_login.xml @@ -0,0 +1,96 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..c025673 --- /dev/null +++ b/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,36 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_main.xml b/app/src/main/res/layout/fragment_main.xml new file mode 100644 index 0000000..e170100 --- /dev/null +++ b/app/src/main/res/layout/fragment_main.xml @@ -0,0 +1,115 @@ + + + + + + + + + + + +