- Approver for documents as Admin (#19)

- Approver for documents as Admin
 - UI tests for document approving
 - update config.yml
 - update android test suite
 - idling resources added for toast
 - toast methods refactored
 - tests for approving updated
This commit is contained in:
2023-06-20 09:12:47 +01:00
committed by GitHub
parent 786761f67c
commit 600f82d2a1
45 changed files with 715 additions and 189 deletions

View File

@@ -27,8 +27,15 @@ jobs:
# Add steps to the job # Add steps to the job
# See: https://circleci.com/docs/2.0/configuration-reference/#steps # See: https://circleci.com/docs/2.0/configuration-reference/#steps
steps: steps:
# Checkout the code as the first step. # Checkout the code and its submodule as the first step.
- checkout - checkout
# config git user
- run:
name: Setup subtree for test data
command: |
git config --global user.email "$GIT_EMAIL"
git config --global user.name "$GIT_EMAIL"
git subtree add --prefix=driver_app_data https://github.com/hmalik144/driver_app_data main
# Setup files for build. # Setup files for build.
- run: - run:
name: Setup variables for build name: Setup variables for build
@@ -50,7 +57,7 @@ jobs:
- run: - run:
name: Start firebase emulator name: Start firebase emulator
command: | command: |
firebase emulators:start firebase emulators:start --import=driver_app_data/export_directory
background: true background: true
# Then start the emulator and run the Instrumentation tests! # Then start the emulator and run the Instrumentation tests!
- android/start-emulator-and-run-tests: - android/start-emulator-and-run-tests:

3
.gitignore vendored
View File

@@ -19,3 +19,6 @@ local
/.circleci/run_local.bash /.circleci/run_local.bash
/Gemfile.lock /Gemfile.lock
/playstore.json /playstore.json
database-debug.log
firebase-debug.log

3
.idea/.gitignore generated vendored Normal file
View File

@@ -0,0 +1,3 @@
# Default ignored files
/shelf/
/workspace.xml

1
.idea/vcs.xml generated
View File

@@ -2,5 +2,6 @@
<project version="4"> <project version="4">
<component name="VcsDirectoryMappings"> <component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" /> <mapping directory="" vcs="Git" />
<mapping directory="$PROJECT_DIR$/driver_app_data" vcs="Git" />
</component> </component>
</project> </project>

View File

@@ -1,5 +1,6 @@
package h_mal.appttude.com.driver.application package h_mal.appttude.com.driver.application
import android.content.res.Resources
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import h_mal.appttude.com.driver.data.FirebaseAuthSource import h_mal.appttude.com.driver.data.FirebaseAuthSource
@@ -12,7 +13,8 @@ class ApplicationViewModelFactory(
private val auth: FirebaseAuthSource, private val auth: FirebaseAuthSource,
private val database: FirebaseDatabaseSource, private val database: FirebaseDatabaseSource,
private val storage: FirebaseStorageSource, private val storage: FirebaseStorageSource,
private val preferences: PreferenceProvider private val preferences: PreferenceProvider,
private val resources: Resources
) : ViewModelProvider.Factory { ) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
@@ -57,6 +59,7 @@ class ApplicationViewModelFactory(
database, database,
preferences preferences
) )
isAssignableFrom(ApproverViewModel::class.java) -> ApproverViewModel(resources , database)
else -> throw IllegalArgumentException("Unknown ViewModel class") else -> throw IllegalArgumentException("Unknown ViewModel class")
} as T } as T
} }

View File

@@ -0,0 +1,31 @@
package h_mal.appttude.com.driver.application
import android.app.Application
import android.content.res.Resources
import h_mal.appttude.com.driver.data.FirebaseAuthSource
import h_mal.appttude.com.driver.data.FirebaseDatabaseSource
import h_mal.appttude.com.driver.data.FirebaseStorageSource
import h_mal.appttude.com.driver.data.prefs.PreferenceProvider
import org.kodein.di.Kodein
import org.kodein.di.KodeinAware
import org.kodein.di.android.x.androidXModule
import org.kodein.di.generic.bind
import org.kodein.di.generic.instance
import org.kodein.di.generic.provider
import org.kodein.di.generic.singleton
class DriverApplication : BaseApplication() {
override val flavourModule = super.flavourModule.copy {
bind() from singleton { PreferenceProvider(this@DriverApplication) }
bind() from provider {
ApplicationViewModelFactory(
instance(),
instance(),
instance(),
instance(),
instance()
)
}
}
}

View File

@@ -0,0 +1,19 @@
package h_mal.appttude.com.driver.model
import h_mal.appttude.com.driver.R
enum class ApprovalStatus(val stringId: Int, val drawableId: Int, val score: Int) {
NOT_SUBMITTED(R.string.not_submitted, R.drawable.denied, 0),
DENIED(R.string.denied, R.drawable.denied, 1),
PENDING_APPROVAL(R.string.pending, R.drawable.pending, 2),
APPROVED(R.string.approved, R.drawable.approved, 3);
companion object {
infix fun getByScore(value: Int): ApprovalStatus? =
ApprovalStatus.values().firstOrNull { it.score == value }
infix fun getByStringId(value: Int): ApprovalStatus? =
ApprovalStatus.values().firstOrNull { it.stringId == value }
infix fun getByDrawableId(value: Int): ApprovalStatus? =
ApprovalStatus.values().firstOrNull { it.drawableId == value }
}
}

View File

@@ -1,77 +1,57 @@
package h_mal.appttude.com.driver.ui package h_mal.appttude.com.driver.ui
import android.content.Context
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.BaseAdapter import android.widget.ArrayAdapter
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import h_mal.appttude.com.driver.R
import h_mal.appttude.com.driver.databinding.ApprovalListItemBinding import h_mal.appttude.com.driver.databinding.ApprovalListItemBinding
import h_mal.appttude.com.driver.model.ApprovalStatus
import h_mal.appttude.com.driver.utils.hide import h_mal.appttude.com.driver.utils.hide
import java.io.IOException
class ApprovalListAdapter( class ApprovalListAdapter(
private val layoutInflater: LayoutInflater, private val context: Context,
private var approvals: Map<String, Int?>, private val data: List<Pair<String, ApprovalStatus>>,
private val callback: (String) -> Unit private val callback: (String) -> Unit
) : BaseAdapter() { ) : ArrayAdapter<Pair<String, ApprovalStatus>>(context, 0, data) {
override fun getCount(): Int = approvals.size
override fun getItem(position: Int): Map.Entry<String, Int?> = approvals.entries.elementAt(position) override fun getCount(): Int = data.size
override fun getItemId(position: Int): Long = position.toLong()
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View { override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
var listItemView: View? = convertView var listItemView: View? = convertView
val binding: ApprovalListItemBinding val binding: ApprovalListItemBinding
if (listItemView == null) { if (listItemView == null) {
binding = ApprovalListItemBinding.inflate(layoutInflater, parent, false) // Inflate view binding into listview cell
binding = ApprovalListItemBinding.inflate(LayoutInflater.from(context), parent, false)
listItemView = binding.root listItemView = binding.root
listItemView.setTag(listItemView.id, binding) listItemView.setTag(listItemView.id, binding)
} else { } else {
// cell exists so recycling view
binding = listItemView.getTag(listItemView.id) as ApprovalListItemBinding binding = listItemView.getTag(listItemView.id) as ApprovalListItemBinding
} }
val key = getItem(position).key val key: String = getItem(position)?.first ?: throw IOException("No document name provided")
val itemValue = getItem(position).value val approvalStatus: ApprovalStatus? = getItem(position)?.second
binding.approvalText.text = key binding.approvalText.text = key
if (itemValue != 0) { approvalStatus?.let { item ->
item.score.takeIf { it != 0 }?.let {
binding.root.setOnClickListener { callback.invoke(key) } binding.root.setOnClickListener { callback.invoke(key) }
} }
binding.approvalIv.setImageResource(getImageResourceBasedOnApproval(itemValue)) binding.approvalIv.setImageResource(item.drawableId)
binding.approvalStatus.text = listItemView.context.getString(getStringResourceBasedOnApproval(itemValue)) binding.approvalStatus.text = context.getString(item.stringId)
if (position == 0) {
binding.divider.hide()
} }
// hide divider for first cell
if (position == 0) binding.divider.hide()
return (listItemView) return (listItemView)
} }
@DrawableRes
private fun getImageResourceBasedOnApproval(value: Int?): Int {
return when(value) {
0 -> R.drawable.denied
1 -> R.drawable.denied
2 -> R.drawable.pending
3 -> R.drawable.approved
else -> R.drawable.pending
}
}
@StringRes fun updateAdapter(date: List<Pair<String, ApprovalStatus>>) {
private fun getStringResourceBasedOnApproval(value: Int?): Int { clear()
return when(value) { addAll(date)
0 -> R.string.not_submitted
1 -> R.string.denied
2 -> R.string.pending
3 -> R.string.approved
else -> R.string.pending
}
}
fun updateAdapter(data: Map<String, Int?>) {
approvals = data
notifyDataSetChanged()
} }
} }

View File

@@ -0,0 +1,40 @@
package h_mal.appttude.com.driver.ui
import com.google.android.material.snackbar.Snackbar
import h_mal.appttude.com.driver.base.BaseFragment
import h_mal.appttude.com.driver.databinding.FragmentApproverBinding
import h_mal.appttude.com.driver.model.ApprovalStatus
import h_mal.appttude.com.driver.viewmodels.ApproverViewModel
class ApproverFragment : BaseFragment<ApproverViewModel, FragmentApproverBinding>() {
override fun setupView(binding: FragmentApproverBinding) = binding.run {
super.setupView(binding)
val args = requireArguments()
viewModel.init(args)
// Retrieve fragment name argument saved from previous fragment
val fragmentClass = viewModel.getFragmentClass()
childFragmentManager.beginTransaction()
.replace(container.id, fragmentClass, args, null)
.commitNow()
approve.setOnClickListener { viewModel.approveDocument() }
decline.setOnClickListener { viewModel.declineDocument() }
}
override fun onSuccess(data: Any?) {
super.onSuccess(data)
when (data) {
ApprovalStatus.APPROVED -> displaySnackBar("approved")
ApprovalStatus.DENIED -> displaySnackBar("declined")
}
}
private fun displaySnackBar(status: String) {
showSnackBar("Document has been $status")
}
}

View File

@@ -1,11 +1,12 @@
package h_mal.appttude.com.driver.ui package h_mal.appttude.com.driver.ui
import android.view.View
import android.widget.ListView import android.widget.ListView
import h_mal.appttude.com.driver.R import h_mal.appttude.com.driver.R
import h_mal.appttude.com.driver.base.BaseFragment import h_mal.appttude.com.driver.base.BaseFragment
import h_mal.appttude.com.driver.data.USER_CONST import h_mal.appttude.com.driver.data.USER_CONST
import h_mal.appttude.com.driver.databinding.FragmentUserMainBinding import h_mal.appttude.com.driver.databinding.FragmentUserMainBinding
import h_mal.appttude.com.driver.model.ApprovalStatus
import h_mal.appttude.com.driver.utils.FRAGMENT
import h_mal.appttude.com.driver.utils.navigateTo import h_mal.appttude.com.driver.utils.navigateTo
import h_mal.appttude.com.driver.utils.toBundle import h_mal.appttude.com.driver.utils.toBundle
import h_mal.appttude.com.driver.viewmodels.DriverOverviewViewModel import h_mal.appttude.com.driver.viewmodels.DriverOverviewViewModel
@@ -19,40 +20,30 @@ class DriverOverviewFragment : BaseFragment<DriverOverviewViewModel, FragmentUse
override fun setupView(binding: FragmentUserMainBinding) { override fun setupView(binding: FragmentUserMainBinding) {
listView = binding.approvalsList listView = binding.approvalsList
}
driverId = requireArguments().getString(USER_CONST) ?: throw IOException("No user ID has been passed") override fun onResume() {
super.onResume()
driverId = requireArguments().getString(USER_CONST)
?: throw IOException("No user ID has been passed")
viewModel.loadDriverApprovals(driverId) viewModel.loadDriverApprovals(driverId)
} }
override fun onSuccess(data: Any?) { override fun onSuccess(data: Any?) {
super.onSuccess(data) super.onSuccess(data)
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
if (data is Map<*, *>) { if (data is List<*>) {
val listData = data as List<Pair<String, ApprovalStatus>>
if (listView.adapter == null) { if (listView.adapter == null) {
listView.adapter = ApprovalListAdapter(layoutInflater, data as Map<String, Int?>) { listView.adapter = ApprovalListAdapter(requireContext(), listData) {
this.view?.applyNavigation(it) this.view?.navigateTo(
R.id.to_approverFragment,
driverId.toBundle(USER_CONST).apply { putString(FRAGMENT, it) })
} }
listView.isScrollContainer = false listView.isScrollContainer = false
} else { } else {
(listView.adapter as ApprovalListAdapter).updateAdapter(data as Map<String, Int?>) (listView.adapter as ApprovalListAdapter).updateAdapter(listData)
} }
} }
} }
private fun View.applyNavigation(key: String) {
val navId = when (key) {
context.getString(R.string.driver_profile) -> R.id.to_driverProfileFragment
context.getString(R.string.drivers_license) -> R.id.to_driverLicenseFragment
context.getString(R.string.private_hire_license) -> R.id.to_privateHireLicenseFragment
context.getString(R.string.vehicle_profile) -> R.id.to_vehicleProfileFragment
context.getString(R.string.insurance) -> R.id.to_insuranceFragment
context.getString(R.string.m_o_t) -> R.id.to_motFragment
context.getString(R.string.log_book) -> R.id.to_logbookFragment
context.getString(R.string.private_hire_vehicle_license) -> R.id.to_privateHireVehicleFragment
else -> {
throw StringIndexOutOfBoundsException("No resource for $key")
}
}
navigateTo(navId, driverId.toBundle(USER_CONST))
}
} }

View File

@@ -105,10 +105,8 @@ class HomeSuperUserFragment : BaseFragment<SuperUserViewModel, FragmentHomeSuper
override fun onDataChanged() { override fun onDataChanged() {
super.onDataChanged() super.onDataChanged()
applyBinding { applyBinding {
// If there are no chat messages, show a view that invites the user to add a message. // If there are no driver data, show a view that informs the admin.
if (itemCount == 0) {
emptyView.root.visibility = if (itemCount == 0) View.VISIBLE else View.GONE emptyView.root.visibility = if (itemCount == 0) View.VISIBLE else View.GONE
}
progressCircular.hide() progressCircular.hide()
} }
} }
@@ -124,7 +122,7 @@ class HomeSuperUserFragment : BaseFragment<SuperUserViewModel, FragmentHomeSuper
} }
override fun connectionLost() { override fun connectionLost() {
requireContext().displayToast("No connection available") showToast("No connection available")
} }
} }
} }
@@ -134,7 +132,7 @@ class HomeSuperUserFragment : BaseFragment<SuperUserViewModel, FragmentHomeSuper
setTag(R.string.driver_identifier, "DriverIdentifierInput") setTag(R.string.driver_identifier, "DriverIdentifierInput")
setText(defaultNumber) setText(defaultNumber)
setSelectAllOnFocus(true) setSelectAllOnFocus(true)
doOnTextChanged { _, _, count, _ -> if (count > 6) context.displayToast("Identifier cannot be larger than 6") } doOnTextChanged { _, _, count, _ -> if (count > 6) showToast("Identifier cannot be larger than 6") }
} }
val layout = LinearLayout(context).apply { val layout = LinearLayout(context).apply {
orientation = LinearLayout.VERTICAL orientation = LinearLayout.VERTICAL

View File

@@ -12,6 +12,7 @@ class VehicleProfileFragment :
override fun setupView(binding: FragmentVehicleSetupBinding) { override fun setupView(binding: FragmentVehicleSetupBinding) {
super.setupView(binding) super.setupView(binding)
viewsToHide(binding.submit) viewsToHide(binding.submit)
binding.seizedCheckbox.isEnabled = false
} }
override fun setFields(data: VehicleProfile) { override fun setFields(data: VehicleProfile) {

View File

@@ -0,0 +1,3 @@
package h_mal.appttude.com.driver.utils
const val FRAGMENT = "fragment"

View File

@@ -0,0 +1,100 @@
package h_mal.appttude.com.driver.viewmodels
import android.content.res.Resources
import android.os.Bundle
import androidx.fragment.app.Fragment
import com.google.firebase.database.DatabaseReference
import h_mal.appttude.com.driver.R
import h_mal.appttude.com.driver.base.BaseViewModel
import h_mal.appttude.com.driver.data.FirebaseCompletion
import h_mal.appttude.com.driver.data.FirebaseDatabaseSource
import h_mal.appttude.com.driver.data.USER_CONST
import h_mal.appttude.com.driver.model.ApprovalStatus
import h_mal.appttude.com.driver.objects.ApprovalsObject
import h_mal.appttude.com.driver.ui.driverprofile.DriverLicenseFragment
import h_mal.appttude.com.driver.ui.driverprofile.DriverProfileFragment
import h_mal.appttude.com.driver.ui.driverprofile.PrivateHireLicenseFragment
import h_mal.appttude.com.driver.ui.vehicleprofile.*
import h_mal.appttude.com.driver.utils.Coroutines.io
import h_mal.appttude.com.driver.utils.FRAGMENT
import h_mal.appttude.com.driver.utils.getDataFromDatabaseRef
class ApproverViewModel(
private val resources: Resources,
private val database: FirebaseDatabaseSource
) : BaseViewModel() {
private lateinit var name: String
private lateinit var docRef: DatabaseReference
private var score: ApprovalStatus? = null
fun init(args: Bundle) {
// Retried uid & fragment class name from args
val uid = args.getString(USER_CONST) ?: throw NullPointerException("No user Id was passed")
name = args.getString(FRAGMENT)
?: throw NullPointerException("No fragment name argument passed")
// Define a document name based on fragment class name
val documentName = when (name) {
resources.getString(R.string.driver_profile) -> ApprovalsObject::driver_details_approval.name
resources.getString(R.string.drivers_license) -> ApprovalsObject::driver_license_approval.name
resources.getString(R.string.private_hire_license) -> ApprovalsObject::private_hire_approval.name
resources.getString(R.string.vehicle_profile) -> ApprovalsObject::vehicle_details_approval.name
resources.getString(R.string.insurance) -> ApprovalsObject::insurance_details_approval.name
resources.getString(R.string.m_o_t) -> ApprovalsObject::mot_details_approval.name
resources.getString(R.string.log_book) -> ApprovalsObject::log_book_approval.name
resources.getString(R.string.private_hire_vehicle_license) -> ApprovalsObject::ph_car_approval.name
else -> {
throw StringIndexOutOfBoundsException("No resource for $name")
}
}
docRef = database.getDocumentApprovalRef(uid, documentName)
io {
doTryOperation("") {
val data = docRef.getDataFromDatabaseRef<Int>()
score = data?.let { ApprovalStatus.getByScore(it) } ?: ApprovalStatus.NOT_SUBMITTED
onSuccess(FirebaseCompletion.Default)
}
}
}
fun getFragmentClass(): Class<out Fragment> {
return when (name) {
resources.getString(R.string.driver_profile) -> DriverProfileFragment::class.java
resources.getString(R.string.drivers_license) -> DriverLicenseFragment::class.java
resources.getString(R.string.private_hire_license) -> PrivateHireLicenseFragment::class.java
resources.getString(R.string.vehicle_profile) -> VehicleProfileFragment::class.java
resources.getString(R.string.insurance) -> InsuranceFragment::class.java
resources.getString(R.string.m_o_t) -> MotFragment::class.java
resources.getString(R.string.log_book) -> LogbookFragment::class.java
resources.getString(R.string.private_hire_vehicle_license) -> PrivateHireVehicleFragment::class.java
else -> {
throw StringIndexOutOfBoundsException("No resource for $name")
}
}
}
fun approveDocument() {
updateDocument(ApprovalStatus.APPROVED)
}
fun declineDocument() {
updateDocument(ApprovalStatus.DENIED)
}
private fun updateDocument(approval: ApprovalStatus) {
if (approval == score) {
val result = if (approval == ApprovalStatus.APPROVED) "approved" else "declined"
onError("Document already $result")
return
}
io {
doTryOperation("Failed to decline document") {
database.postToDatabaseRed(docRef, approval.score)
onSuccess(approval)
}
}
}
}

View File

@@ -5,10 +5,12 @@ import com.google.firebase.storage.StorageReference
import h_mal.appttude.com.driver.base.DataSubmissionBaseViewModel import h_mal.appttude.com.driver.base.DataSubmissionBaseViewModel
import h_mal.appttude.com.driver.data.FirebaseAuthentication import h_mal.appttude.com.driver.data.FirebaseAuthentication
import h_mal.appttude.com.driver.data.FirebaseDatabaseSource import h_mal.appttude.com.driver.data.FirebaseDatabaseSource
import h_mal.appttude.com.driver.model.ApprovalStatus
import h_mal.appttude.com.driver.objects.ApprovalsObject import h_mal.appttude.com.driver.objects.ApprovalsObject
import h_mal.appttude.com.driver.utils.Coroutines.io import h_mal.appttude.com.driver.utils.Coroutines.io
import h_mal.appttude.com.driver.utils.getDataFromDatabaseRef import h_mal.appttude.com.driver.utils.getDataFromDatabaseRef
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import java.io.IOException
class DriverOverviewViewModel( class DriverOverviewViewModel(
auth: FirebaseAuthentication, auth: FirebaseAuthentication,
@@ -37,17 +39,23 @@ class DriverOverviewViewModel(
} }
} }
private fun mapApprovalsForView(data: ApprovalsObject): Map<String, Int> { private fun mapApprovalsForView(data: ApprovalsObject): List<Pair<String, ApprovalStatus>> {
return mutableMapOf<String, Int>().apply { val list = mutableListOf<Pair<String, ApprovalStatus>>()
put("Driver Profile", data.driver_details_approval) return list.apply {
put("Drivers License", data.driver_license_approval) add(0, Pair("Driver Profile", getApprovalStatusByScore(data.driver_details_approval)))
put("Private Hire License", data.private_hire_approval) add(1, Pair("Drivers License", getApprovalStatusByScore(data.driver_license_approval)))
put("Vehicle Profile", data.vehicle_details_approval) add(2, Pair("Private Hire License", getApprovalStatusByScore(data.private_hire_approval)))
put("Insurance", data.insurance_details_approval) add(3, Pair("Vehicle Profile", getApprovalStatusByScore(data.vehicle_details_approval)))
put("M.O.T", data.mot_details_approval) add(4, Pair("Insurance", getApprovalStatusByScore(data.insurance_details_approval)))
put("Log book", data.log_book_approval) add(5, Pair("M.O.T", getApprovalStatusByScore(data.mot_details_approval)))
put("Private Hire Vehicle License", data.ph_car_approval) add(6, Pair("Log book", getApprovalStatusByScore(data.log_book_approval)))
}.toMap() add(7, Pair("Private Hire Vehicle License", getApprovalStatusByScore(data.ph_car_approval)))
}
}
private fun getApprovalStatusByScore(score: Int): ApprovalStatus {
if (score == 0) return ApprovalStatus.NOT_SUBMITTED
return ApprovalStatus.getByScore(score) ?: throw IOException("No approval for score $score")
} }
} }

View File

@@ -0,0 +1,51 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
tools:context=".ui.ApproverFragment">
<androidx.fragment.app.FragmentContainerView
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintBottom_toTopOf="@id/buttonContainer"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:layout="@layout/fragment_driver_license" />
<LinearLayout
android:id="@+id/buttonContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
android:gravity="center"
android:orientation="horizontal"
tools:ignore="RtlHardcoded">
<com.google.android.material.button.MaterialButton
android:id="@+id/approve"
android:layout_marginLeft="12dp"
android:layout_marginRight="6dp"
android:layout_width="0dp"
android:layout_weight="1"
style="@style/TextButton.WithIcon"
app:icon="@drawable/baseline_check_24"
android:text="@string/approve" />
<com.google.android.material.button.MaterialButton
android:id="@+id/decline"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_marginLeft="6dp"
android:layout_marginRight="12dp"
style="@style/TextButton.WithIcon"
app:icon="@drawable/baseline_clear_24"
app:iconTint="@android:color/holo_red_light"
android:backgroundTint="@color/colour_ten"
android:text="@string/decline" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -23,68 +23,16 @@
android:id="@+id/userMainFragment" android:id="@+id/userMainFragment"
android:name="h_mal.appttude.com.driver.ui.DriverOverviewFragment" android:name="h_mal.appttude.com.driver.ui.DriverOverviewFragment"
android:label="fragment_user_main" android:label="fragment_user_main"
tools:layout="@layout/fragment_user_main" > tools:layout="@layout/fragment_user_main">
<action <action
android:id="@+id/to_driverLicenseFragment" android:id="@+id/to_approverFragment"
app:destination="@id/driverLicenseFragment" /> app:destination="@id/approverFragment" />
<action
android:id="@+id/to_driverProfileFragment"
app:destination="@id/driverProfileFragment" />
<action
android:id="@+id/to_privateHireLicenseFragment"
app:destination="@id/privateHireLicenseFragment" />
<action
android:id="@+id/to_insuranceFragment"
app:destination="@id/insuranceFragment" />
<action
android:id="@+id/to_logbookFragment"
app:destination="@id/logbookFragment" />
<action
android:id="@+id/to_motFragment"
app:destination="@id/motFragment" />
<action
android:id="@+id/to_privateHireVehicleFragment"
app:destination="@id/privateHireVehicleFragment" />
<action
android:id="@+id/to_vehicleProfileFragment"
app:destination="@id/vehicleProfileFragment" />
</fragment> </fragment>
<fragment <fragment
android:id="@+id/driverLicenseFragment" android:id="@+id/approverFragment"
android:name="h_mal.appttude.com.driver.ui.driverprofile.DriverLicenseFragment" android:name="h_mal.appttude.com.driver.ui.ApproverFragment"
android:label="DriverLicenseFragment" /> android:label="fragment_approver"
<fragment tools:layout="@layout/fragment_approver" />
android:id="@+id/driverProfileFragment"
android:name="h_mal.appttude.com.driver.ui.driverprofile.DriverProfileFragment"
android:label="fragment_driver_profile"
tools:layout="@layout/fragment_driver_profile" />
<fragment
android:id="@+id/privateHireLicenseFragment"
android:name="h_mal.appttude.com.driver.ui.driverprofile.PrivateHireLicenseFragment"
android:label="fragment_private_hire_license"
tools:layout="@layout/fragment_private_hire_license" />
<fragment
android:id="@+id/insuranceFragment"
android:name="h_mal.appttude.com.driver.ui.vehicleprofile.InsuranceFragment"
android:label="InsuranceFragment" />
<fragment
android:id="@+id/logbookFragment"
android:name="h_mal.appttude.com.driver.ui.vehicleprofile.LogbookFragment"
android:label="fragment_logbook"
tools:layout="@layout/fragment_logbook" />
<fragment
android:id="@+id/motFragment"
android:name="h_mal.appttude.com.driver.ui.vehicleprofile.MotFragment"
android:label="MotFragment" />
<fragment
android:id="@+id/privateHireVehicleFragment"
android:name="h_mal.appttude.com.driver.ui.vehicleprofile.PrivateHireVehicleFragment"
android:label="fragment_private_hire_vehicle"
tools:layout="@layout/fragment_private_hire_vehicle" />
<fragment
android:id="@+id/vehicleProfileFragment"
android:name="h_mal.appttude.com.driver.ui.vehicleprofile.VehicleProfileFragment"
android:label="fragment_vehicle_setup"
tools:layout="@layout/fragment_vehicle_setup" />
</navigation> </navigation>

View File

@@ -1,3 +1,5 @@
<resources> <resources>
<string name="app_name">Driver Admin</string> <string name="app_name">Driver Admin</string>
<!-- TODO: Remove or change this placeholder text -->
<string name="hello_blank_fragment">Hello blank fragment</string>
</resources> </resources>

View File

@@ -60,7 +60,7 @@ open class BaseTestRobot {
} }
fun <VH : ViewHolder> scrollToRecyclerItem(recyclerId: Int, text: String): ViewInteraction? { fun <VH : ViewHolder> scrollToRecyclerItem(recyclerId: Int, text: String): ViewInteraction? {
return onView(withId(recyclerId)) return matchView(recyclerId)
.perform( .perform(
// scrollTo will fail the test if no item matches. // scrollTo will fail the test if no item matches.
RecyclerViewActions.scrollTo<VH>( RecyclerViewActions.scrollTo<VH>(
@@ -69,8 +69,38 @@ open class BaseTestRobot {
) )
} }
fun <VH : ViewHolder> scrollToRecyclerItem(recyclerId: Int, resIdForString: Int): ViewInteraction? {
return matchView(recyclerId)
.perform(
// scrollTo will fail the test if no item matches.
RecyclerViewActions.scrollTo<VH>(
hasDescendant(withText(resIdForString))
)
)
}
fun <VH : ViewHolder> scrollToRecyclerItemByPosition(recyclerId: Int, position: Int): ViewInteraction? {
return matchView(recyclerId)
.perform(
// scrollTo will fail the test if no item matches.
RecyclerViewActions.scrollToPosition<VH>(position)
)
}
fun <VH : ViewHolder> clickViewInRecycler(recyclerId: Int, text: String) { fun <VH : ViewHolder> clickViewInRecycler(recyclerId: Int, text: String) {
scrollToRecyclerItem<VH>(recyclerId, text)?.perform(click()) matchView(recyclerId)
.perform(
// scrollTo will fail the test if no item matches.
RecyclerViewActions.actionOnItem<VH>(hasDescendant(withText(text)), click())
)
}
fun <VH : ViewHolder> clickViewInRecycler(recyclerId: Int, resIdForString: Int) {
matchView(recyclerId)
.perform(
// scrollTo will fail the test if no item matches.
RecyclerViewActions.actionOnItem<VH>(hasDescendant(withText(resIdForString)), click())
)
} }
fun <VH : ViewHolder> clickSubViewInRecycler(recyclerId: Int, text: String, subView: Int) { fun <VH : ViewHolder> clickSubViewInRecycler(recyclerId: Int, text: String, subView: Int) {

View File

@@ -1,17 +1,26 @@
package h_mal.appttude.com.driver package h_mal.appttude.com.driver
import android.R
import android.app.Activity
import android.content.Context
import android.view.View import android.view.View
import android.view.WindowManager
import android.widget.Toast
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.test.core.app.ActivityScenario import androidx.test.core.app.ActivityScenario
import androidx.test.espresso.*
import androidx.test.espresso.Espresso.onView import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.IdlingRegistry import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.IdlingResource import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.UiController import androidx.test.espresso.matcher.ViewMatchers.*
import androidx.test.espresso.ViewAction import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
import androidx.test.espresso.matcher.ViewMatchers.isRoot
import androidx.test.platform.app.InstrumentationRegistry
import h_mal.appttude.com.driver.base.BaseActivity import h_mal.appttude.com.driver.base.BaseActivity
import h_mal.appttude.com.driver.helpers.BaseViewAction
import org.hamcrest.CoreMatchers
import org.hamcrest.Description
import org.hamcrest.Matcher import org.hamcrest.Matcher
import org.hamcrest.TypeSafeMatcher
import org.hamcrest.core.AllOf
import org.junit.After import org.junit.After
import org.junit.Before import org.junit.Before
@@ -23,6 +32,8 @@ open class BaseUiTest<T : BaseActivity<*, *>>(
private lateinit var mActivityScenarioRule: ActivityScenario<T> private lateinit var mActivityScenarioRule: ActivityScenario<T>
private var mIdlingResource: IdlingResource? = null private var mIdlingResource: IdlingResource? = null
private lateinit var currentActivity: Activity
@Before @Before
fun setup() { fun setup() {
beforeLaunch() beforeLaunch()
@@ -30,7 +41,7 @@ open class BaseUiTest<T : BaseActivity<*, *>>(
mActivityScenarioRule.onActivity { mActivityScenarioRule.onActivity {
mIdlingResource = it.getIdlingResource()!! mIdlingResource = it.getIdlingResource()!!
IdlingRegistry.getInstance().register(mIdlingResource) IdlingRegistry.getInstance().register(mIdlingResource)
afterLaunch() afterLaunch(it)
} }
} }
@@ -42,7 +53,7 @@ open class BaseUiTest<T : BaseActivity<*, *>>(
} }
fun getResourceString(@StringRes stringRes: Int): String { fun getResourceString(@StringRes stringRes: Int): String {
return InstrumentationRegistry.getInstrumentation().targetContext.resources.getString( return getInstrumentation().targetContext.resources.getString(
stringRes stringRes
) )
} }
@@ -58,5 +69,50 @@ open class BaseUiTest<T : BaseActivity<*, *>>(
} }
open fun beforeLaunch() {} open fun beforeLaunch() {}
open fun afterLaunch() {} open fun afterLaunch(context: Context) {}
fun checkToastMessage(message: String) {
onView(withText(message)).inRoot(object : TypeSafeMatcher<Root>() {
override fun describeTo(description: Description?) {
description?.appendText("is toast")
}
override fun matchesSafely(root: Root): Boolean {
root.run {
if (windowLayoutParams.get().type === WindowManager.LayoutParams.TYPE_TOAST) {
decorView.run {
if (windowToken === applicationWindowToken) {
// windowToken == appToken means this window isn't contained by any other windows.
// if it was a window for an activity, it would have TYPE_BASE_APPLICATION.
return true
}
}
}
}
return false
}
}
).check(matches(isDisplayed()))
}
fun checkSnackBarDisplayedByMessage(message: String) {
onView(
CoreMatchers.allOf(
withId(com.google.android.material.R.id.snackbar_text),
withText(message)
)
).check(matches(isDisplayed()))
}
private fun getCurrentActivity(): Activity {
onView(AllOf.allOf(withId(R.id.content), isDisplayed()))
.perform(object : BaseViewAction() {
override fun setPerform(uiController: UiController?, view: View?) {
if (view?.context is Activity) {
currentActivity = view.context as Activity
}
}
})
return currentActivity
}
} }

View File

@@ -5,6 +5,8 @@ import com.google.firebase.database.FirebaseDatabase
import com.google.firebase.storage.FirebaseStorage import com.google.firebase.storage.FirebaseStorage
import h_mal.appttude.com.driver.base.BaseActivity import h_mal.appttude.com.driver.base.BaseActivity
import h_mal.appttude.com.driver.data.FirebaseAuthSource import h_mal.appttude.com.driver.data.FirebaseAuthSource
import h_mal.appttude.com.driver.data.FirebaseDatabaseSource
import h_mal.appttude.com.driver.data.FirebaseStorageSource
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.tasks.await import kotlinx.coroutines.tasks.await
import org.junit.After import org.junit.After
@@ -16,7 +18,11 @@ open class FirebaseTest<T : BaseActivity<*, *>>(
private val signedIn: Boolean = false, private val signedIn: Boolean = false,
private val signOutAfterTest: Boolean = true private val signOutAfterTest: Boolean = true
) : BaseUiTest<T>(activity) { ) : BaseUiTest<T>(activity) {
private val firebaseAuthSource by lazy { FirebaseAuthSource() } private val firebaseAuthSource by lazy { FirebaseAuthSource() }
private val firebaseDatabaseSource by lazy { FirebaseDatabaseSource() }
private val firebaseStorageSource by lazy { FirebaseStorageSource() }
private var email: String? = null private var email: String? = null
companion object { companion object {
@@ -58,6 +64,14 @@ open class FirebaseTest<T : BaseActivity<*, *>>(
firebaseAuthSource.registerUser(signInEmail, password).await().user firebaseAuthSource.registerUser(signInEmail, password).await().user
} }
suspend fun login(
signInEmail: String,
password: String
) {
email = signInEmail
firebaseAuthSource.signIn(signInEmail, password).await()
}
// remove the user we created for testing // remove the user we created for testing
suspend fun removeUser() { suspend fun removeUser() {
try { try {

View File

@@ -0,0 +1,27 @@
package h_mal.appttude.com.driver.helpers
import android.view.View
import androidx.test.espresso.UiController
import androidx.test.espresso.ViewAction
import androidx.test.espresso.matcher.ViewMatchers.isAssignableFrom
import org.hamcrest.Matcher
open class BaseViewAction: ViewAction {
override fun getDescription(): String? = setDescription()
override fun getConstraints(): Matcher<View> = setConstraints()
override fun perform(uiController: UiController?, view: View?) {
setPerform(uiController, view)
}
open fun setDescription(): String? {
return null
}
open fun setConstraints(): Matcher<View> {
return isAssignableFrom(View::class.java)
}
open fun setPerform(uiController: UiController?, view: View?) { }
}

View File

@@ -0,0 +1,13 @@
package h_mal.appttude.com.driver.robots
import h_mal.appttude.com.driver.BaseTestRobot
import h_mal.appttude.com.driver.R
fun approver(func: ApproverRobot.() -> Unit) = ApproverRobot().apply { func() }
class ApproverRobot : BaseTestRobot() {
fun clickApprove() = clickButton(R.id.approve)
fun clickDecline() = clickButton(R.id.decline)
}

View File

@@ -0,0 +1,27 @@
package h_mal.appttude.com.driver.robots
import androidx.test.espresso.Espresso.onData
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.*
import h_mal.appttude.com.driver.BaseTestRobot
import h_mal.appttude.com.driver.R
import org.hamcrest.CoreMatchers.anything
fun driverOverview(func: DriverOverviewRobot.() -> Unit) = DriverOverviewRobot().apply { func() }
class DriverOverviewRobot : BaseTestRobot() {
fun clickOnItemAtPosition(index: Int) =
onData(anything())
.inAdapterView(withId(R.id.approvals_list))
.atPosition(index)
.perform(click())
fun matchView(position: Int, status: String) =
onData(anything())
.inAdapterView(withId(R.id.approvals_list))
.atPosition(position)
.onChildView(withText(status))
.check(matches(isDisplayed()))
}

View File

@@ -12,6 +12,10 @@ import h_mal.appttude.com.driver.base.CustomViewHolder
fun homeAdmin(func: HomeAdminRobot.() -> Unit) = HomeAdminRobot().apply { func() } fun homeAdmin(func: HomeAdminRobot.() -> Unit) = HomeAdminRobot().apply { func() }
class HomeAdminRobot : BaseTestRobot() { class HomeAdminRobot : BaseTestRobot() {
fun waitUntilDisplayed() {
matchViewWaitFor(R.id.recycler_view)
}
fun openDrawer() { fun openDrawer() {
onView(withId(R.id.drawer_layout)).perform(DrawerActions.open()) onView(withId(R.id.drawer_layout)).perform(DrawerActions.open())
} }

View File

@@ -0,0 +1,16 @@
package h_mal.appttude.com.driver.tests
import h_mal.appttude.com.driver.ADMIN_EMAIL
import h_mal.appttude.com.driver.FirebaseTest
import h_mal.appttude.com.driver.PASSWORD
import h_mal.appttude.com.driver.ui.MainActivity
import kotlinx.coroutines.runBlocking
open class AdminBaseTest: FirebaseTest<MainActivity>(MainActivity::class.java) {
override fun beforeLaunch() {
runBlocking {
login(ADMIN_EMAIL, PASSWORD)
}
}
}

View File

@@ -0,0 +1,72 @@
package h_mal.appttude.com.driver.tests
import androidx.test.espresso.Espresso
import h_mal.appttude.com.driver.robots.approver
import h_mal.appttude.com.driver.robots.driverOverview
import h_mal.appttude.com.driver.robots.homeAdmin
import h_mal.appttude.com.driver.R
import org.junit.Test
class DocumentApproverTest : AdminBaseTest() {
@Test
fun loginAsAdmin_approveDocumentForDriver_documentApproved() {
homeAdmin {
waitUntilDisplayed()
clickOnItem("kabirmhkhan@gmail.com")
}
// Approve check
driverOverview {
clickOnItemAtPosition(0)
}
approver {
clickApprove()
checkToastMessage("Document already approved")
Espresso.pressBack()
}
driverOverview {
clickOnItemAtPosition(2)
}
approver {
clickApprove()
Espresso.pressBack()
}
driverOverview {
matchView(2, getResourceString(R.string.approved))
}
// Decline check
driverOverview {
clickOnItemAtPosition(3)
}
approver {
clickDecline()
checkToastMessage("Document already declined")
Espresso.pressBack()
}
driverOverview {
clickOnItemAtPosition(1)
}
approver {
clickDecline()
Espresso.pressBack()
}
driverOverview {
matchView(1, getResourceString(R.string.denied))
}
}
@Test
fun loginAsAdmin_verifyNoDocumentForNewDriver() {
homeAdmin {
waitUntilDisplayed()
clickOnItem("fanasid@gmail.com")
}
driverOverview {
matchView(0, getResourceString(R.string.not_submitted))
clickOnItemAtPosition(0)
matchView(0, getResourceString(R.string.not_submitted))
}
}
}

View File

@@ -11,8 +11,7 @@ import h_mal.appttude.com.driver.viewmodels.*
class ApplicationViewModelFactory( class ApplicationViewModelFactory(
private val auth: FirebaseAuthSource, private val auth: FirebaseAuthSource,
private val database: FirebaseDatabaseSource, private val database: FirebaseDatabaseSource,
private val storage: FirebaseStorageSource, private val storage: FirebaseStorageSource
private val preferences: PreferenceProvider
) : ViewModelProvider.Factory { ) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")

View File

@@ -0,0 +1,28 @@
package h_mal.appttude.com.driver.application
import android.app.Application
import android.content.res.Resources
import h_mal.appttude.com.driver.data.FirebaseAuthSource
import h_mal.appttude.com.driver.data.FirebaseDatabaseSource
import h_mal.appttude.com.driver.data.FirebaseStorageSource
import h_mal.appttude.com.driver.data.prefs.PreferenceProvider
import org.kodein.di.Kodein
import org.kodein.di.KodeinAware
import org.kodein.di.android.x.androidXModule
import org.kodein.di.generic.bind
import org.kodein.di.generic.instance
import org.kodein.di.generic.provider
import org.kodein.di.generic.singleton
class DriverApplication : BaseApplication() {
override val flavourModule = super.flavourModule.copy {
bind() from provider {
ApplicationViewModelFactory(
instance(),
instance(),
instance()
)
}
}
}

View File

@@ -9,19 +9,24 @@ import org.kodein.di.Kodein
import org.kodein.di.KodeinAware import org.kodein.di.KodeinAware
import org.kodein.di.android.x.androidXModule import org.kodein.di.android.x.androidXModule
import org.kodein.di.generic.bind import org.kodein.di.generic.bind
import org.kodein.di.generic.instance
import org.kodein.di.generic.provider
import org.kodein.di.generic.singleton import org.kodein.di.generic.singleton
class DriverApplication : Application(), KodeinAware { open class BaseApplication : Application(), KodeinAware {
// Kodein aware to initialise the classes used for DI // Kodein aware to initialise the classes used for DI
override val kodein = Kodein.lazy { override val kodein = Kodein.lazy {
import(androidXModule(this@DriverApplication)) import(parentModule)
import(flavourModule)
}
val parentModule = Kodein.Module("Parent Module") {
import(androidXModule(this@BaseApplication))
bind() from singleton { FirebaseAuthSource() } bind() from singleton { FirebaseAuthSource() }
bind() from singleton { FirebaseDatabaseSource() } bind() from singleton { FirebaseDatabaseSource() }
bind() from singleton { FirebaseStorageSource() } bind() from singleton { FirebaseStorageSource() }
bind() from singleton { PreferenceProvider(this@DriverApplication) } }
bind() from provider { ApplicationViewModelFactory(instance(), instance(), instance(), instance()) }
open val flavourModule = Kodein.Module("Flavour") {
import(parentModule)
} }
} }

View File

@@ -1,27 +1,35 @@
package h_mal.appttude.com.driver.base package h_mal.appttude.com.driver.base
import android.content.Intent import android.content.Intent
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.view.View import android.view.View
import android.view.ViewGroup.LayoutParams import android.view.ViewGroup.LayoutParams
import android.view.ViewGroup.LayoutParams.MATCH_PARENT import android.view.ViewGroup.LayoutParams.MATCH_PARENT
import android.view.ViewGroup.inflate import android.view.ViewGroup.inflate
import android.widget.Toast
import androidx.annotation.VisibleForTesting import androidx.annotation.VisibleForTesting
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.ViewModelLazy import androidx.lifecycle.ViewModelLazy
import androidx.test.espresso.IdlingResource import androidx.test.espresso.IdlingResource
import androidx.viewbinding.ViewBinding import androidx.viewbinding.ViewBinding
import com.google.android.material.snackbar.BaseTransientBottomBar.BaseCallback
import com.google.android.material.snackbar.Snackbar
import h_mal.appttude.com.driver.R import h_mal.appttude.com.driver.R
import h_mal.appttude.com.driver.application.ApplicationViewModelFactory import h_mal.appttude.com.driver.application.ApplicationViewModelFactory
import h_mal.appttude.com.driver.data.ViewState import h_mal.appttude.com.driver.data.ViewState
import h_mal.appttude.com.driver.utils.* import h_mal.appttude.com.driver.utils.BasicIdlingResource
import h_mal.appttude.com.driver.utils.GenericsHelper.getGenericClassAt import h_mal.appttude.com.driver.utils.GenericsHelper.getGenericClassAt
import h_mal.appttude.com.driver.utils.GenericsHelper.inflateBindingByType import h_mal.appttude.com.driver.utils.GenericsHelper.inflateBindingByType
import h_mal.appttude.com.driver.utils.hide
import h_mal.appttude.com.driver.utils.show
import h_mal.appttude.com.driver.utils.triggerAnimation
import org.kodein.di.KodeinAware import org.kodein.di.KodeinAware
import org.kodein.di.android.kodein import org.kodein.di.android.kodein
import org.kodein.di.generic.instance import org.kodein.di.generic.instance
abstract class BaseActivity<V : BaseViewModel, VB : ViewBinding> : AppCompatActivity(), KodeinAware { abstract class BaseActivity<V : BaseViewModel, VB : ViewBinding> : AppCompatActivity(),
KodeinAware {
// The Idling Resource which will be null in production. // The Idling Resource which will be null in production.
private var mIdlingResource: BasicIdlingResource? = null private var mIdlingResource: BasicIdlingResource? = null
private lateinit var loadingView: View private lateinit var loadingView: View
@@ -108,7 +116,7 @@ abstract class BaseActivity<V : BaseViewModel, VB : ViewBinding> : AppCompatActi
* Called in case of failure or some error emitted from the liveData in viewModel * Called in case of failure or some error emitted from the liveData in viewModel
*/ */
open fun onFailure(error: String?) { open fun onFailure(error: String?) {
error?.let { displayToast(it) } error?.let { showToast(it) }
loadingView.fadeOut() loadingView.fadeOut()
mIdlingResource?.setIdleState(true) mIdlingResource?.setIdleState(true)
} }
@@ -139,6 +147,42 @@ abstract class BaseActivity<V : BaseViewModel, VB : ViewBinding> : AppCompatActi
super.onBackPressed() super.onBackPressed()
} }
fun showToast(message: String) {
val toast = Toast.makeText(this, message, Toast.LENGTH_LONG)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
toast.addCallback(object : Toast.Callback() {
override fun onToastHidden() {
super.onToastHidden()
mIdlingResource?.setIdleState(true)
}
override fun onToastShown() {
super.onToastShown()
mIdlingResource?.setIdleState(false)
}
})
}
toast.show()
}
fun showSnackBar(message: String) {
val snackbar = Snackbar.make(
window.decorView.findViewById(android.R.id.content),
message,
Snackbar.LENGTH_LONG
)
snackbar.addCallback(object : BaseCallback<Snackbar>() {
override fun onShown(transientBottomBar: Snackbar?) {
super.onShown(transientBottomBar)
mIdlingResource?.setIdleState(false)
}
override fun onDismissed(transientBottomBar: Snackbar?, event: Int) {
super.onDismissed(transientBottomBar, event)
mIdlingResource?.setIdleState(true)
}
})
snackbar.show()
}
/** /**
* Only called from test, creates and returns a new [BasicIdlingResource]. * Only called from test, creates and returns a new [BasicIdlingResource].
*/ */

View File

@@ -20,7 +20,6 @@ import h_mal.appttude.com.driver.utils.PermissionsUtils
import org.kodein.di.KodeinAware import org.kodein.di.KodeinAware
import org.kodein.di.android.x.kodein import org.kodein.di.android.x.kodein
import org.kodein.di.generic.instance import org.kodein.di.generic.instance
import java.io.File
abstract class BaseFragment<V : BaseViewModel, VB : ViewBinding> : Fragment(), KodeinAware { abstract class BaseFragment<V : BaseViewModel, VB : ViewBinding> : Fragment(), KodeinAware {
@@ -43,7 +42,6 @@ abstract class BaseFragment<V : BaseViewModel, VB : ViewBinding> : Fragment(), K
multipleImage = true multipleImage = true
} }
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, inflater: LayoutInflater,
container: ViewGroup?, container: ViewGroup?,
@@ -169,4 +167,7 @@ abstract class BaseFragment<V : BaseViewModel, VB : ViewBinding> : Fragment(), K
} }
} }
} }
fun showToast(message: String) = (activity as BaseActivity<*, *>).showToast(message)
fun showSnackBar(message: String) = (activity as BaseActivity<*, *>).showSnackBar(message)
} }

View File

@@ -36,7 +36,7 @@ class FirebaseDatabaseSource {
return data return data
} }
fun getDatabaseReferenceFromLink(link: String) = database.getReferenceFromUrl(link) fun getDatabaseRefFromPath(path: String) = database.getReference(path)
val users = database.getReference(USER_CONST) val users = database.getReference(USER_CONST)
@@ -46,6 +46,7 @@ class FirebaseDatabaseSource {
fun getVehicleRef(uid: String) = getUserRef(uid).child(VEHICLE_PROFILE) fun getVehicleRef(uid: String) = getUserRef(uid).child(VEHICLE_PROFILE)
fun getDriverRef(uid: String) = getUserRef(uid).child(DRIVER_PROFILE) fun getDriverRef(uid: String) = getUserRef(uid).child(DRIVER_PROFILE)
fun getApprovalsRef(uid: String) = getUserRef(uid).child(APPROVALS) fun getApprovalsRef(uid: String) = getUserRef(uid).child(APPROVALS)
fun getDocumentApprovalRef(uid: String, document: String) = getApprovalsRef(uid).child(document)
fun getArchiveRef(uid: String) = getUserRef(uid).child(ARCHIVE) fun getArchiveRef(uid: String) = getUserRef(uid).child(ARCHIVE)
fun getUserRoleRef(uid: String) = getUserRef(uid).child(PROFILE_ROLE) fun getUserRoleRef(uid: String) = getUserRef(uid).child(PROFILE_ROLE)
fun getDriverNumberRef(uid: String) = getUserRef(uid).child(DRIVER_NUMBER) fun getDriverNumberRef(uid: String) = getUserRef(uid).child(DRIVER_NUMBER)

View File

@@ -3,7 +3,6 @@ package h_mal.appttude.com.driver.ui.update
import h_mal.appttude.com.driver.base.BaseActivity import h_mal.appttude.com.driver.base.BaseActivity
import h_mal.appttude.com.driver.data.FirebaseCompletion import h_mal.appttude.com.driver.data.FirebaseCompletion
import h_mal.appttude.com.driver.databinding.UpdateActivityBinding import h_mal.appttude.com.driver.databinding.UpdateActivityBinding
import h_mal.appttude.com.driver.utils.displayToast
import h_mal.appttude.com.driver.viewmodels.UpdateUserViewModel import h_mal.appttude.com.driver.viewmodels.UpdateUserViewModel
class UpdateActivity : BaseActivity<UpdateUserViewModel, UpdateActivityBinding>() { class UpdateActivity : BaseActivity<UpdateUserViewModel, UpdateActivityBinding>() {
@@ -11,7 +10,7 @@ class UpdateActivity : BaseActivity<UpdateUserViewModel, UpdateActivityBinding>(
override fun onSuccess(data: Any?) { override fun onSuccess(data: Any?) {
super.onSuccess(data) super.onSuccess(data)
when (data) { when (data) {
is FirebaseCompletion.Changed -> displayToast(data.message) is FirebaseCompletion.Changed -> showToast(data.message)
} }
} }
} }

View File

@@ -33,14 +33,6 @@ fun View.hide() {
this.visibility = View.GONE this.visibility = View.GONE
} }
fun Context.displayToast(message: String) {
Toast.makeText(this, message, Toast.LENGTH_LONG).show()
}
fun Fragment.displayToast(message: String) {
requireContext().displayToast(message)
}
fun EditText.setEnterPressedListener(action: () -> Unit) { fun EditText.setEnterPressedListener(action: () -> Unit) {
setOnEditorActionListener(TextView.OnEditorActionListener { _, id, _ -> setOnEditorActionListener(TextView.OnEditorActionListener { _, id, _ ->
if (id == EditorInfo.IME_ACTION_DONE || id == EditorInfo.IME_NULL) { if (id == EditorInfo.IME_ACTION_DONE || id == EditorInfo.IME_NULL) {

View File

@@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#FFFFFF"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M9,16.17L4.83,12l-1.42,1.41L9,19 21,7l-1.41,-1.41z"/>
</vector>

View File

@@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#FFFFFF"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M19,6.41L17.59,5 12,10.59 6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 12,13.41 17.59,19 19,17.59 13.41,12z"/>
</vector>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 125 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 224 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 330 KiB

View File

@@ -102,4 +102,7 @@
<string name="not_submitted">Not submitted</string> <string name="not_submitted">Not submitted</string>
<string name="empty_view_message">You have no drivers</string> <string name="empty_view_message">You have no drivers</string>
<string name="approval_status">approval status</string> <string name="approval_status">approval status</string>
<string name="approve">Approve</string>
<string name="deny">Deny</string>
<string name="decline">Decline</string>
</resources> </resources>

View File

@@ -1,10 +1,6 @@
{ {
"rules": { "rules": {
"user": {
".read": true, ".read": true,
"$user_id": { ".write": true
".write": "$user_id === auth.uid"
}
}
} }
} }