Merge pull request #20 from hmalik144/admin_app_refactor

Admin app refactor
This commit is contained in:
2023-06-23 23:07:51 +01:00
committed by GitHub
67 changed files with 1017 additions and 296 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,13 +57,14 @@ 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:
post-emulator-launch-assemble-command: ./gradlew assemble<< parameters.flavour >>DebugAndroidTest post-emulator-launch-assemble-command: ./gradlew assemble<< parameters.flavour >>DebugAndroidTest
test-command: ./gradlew connected<< parameters.flavour >>DebugAndroidTest test-command: ./gradlew connected<< parameters.flavour >>DebugAndroidTest
system-image: system-images;android-25;google_apis;x86 system-image: system-images;android-25;google_apis;x86
max-tries: 1
# store test reports # store test reports
- store_artifacts: - store_artifacts:
path: app/build/reports/androidTests/connected path: app/build/reports/androidTests/connected

4
.gitignore vendored
View File

@@ -9,6 +9,7 @@
/.idea/navEditor.xml /.idea/navEditor.xml
/.idea/misc.xml /.idea/misc.xml
/.idea/assetWizardSettings.xml /.idea/assetWizardSettings.xml
/.idea/shelf/
.DS_Store .DS_Store
/build /build
/captures /captures
@@ -19,3 +20,6 @@ local
/.circleci/run_local.bash /.circleci/run_local.bash
/Gemfile.lock /Gemfile.lock
/playstore.json /playstore.json
database-debug.log
firebase-debug.log

6
.idea/compiler.xml generated
View File

@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<bytecodeTargetLevel target="17" />
</component>
</project>

View File

@@ -1,7 +0,0 @@
<component name="ProjectDictionaryState">
<dictionary name="h_mal">
<words>
<w>viewmodel</w>
</words>
</dictionary>
</component>

20
.idea/gradle.xml generated
View File

@@ -1,20 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GradleMigrationSettings" migrationVersion="1" />
<component name="GradleSettings">
<option name="linkedExternalProjectsSettings">
<GradleProjectSettings>
<option name="testRunner" value="GRADLE" />
<option name="distributionType" value="DEFAULT_WRAPPED" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="gradleJvm" value="Android Studio java home" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$" />
<option value="$PROJECT_DIR$/app" />
</set>
</option>
</GradleProjectSettings>
</option>
</component>
</project>

48
.idea/misc.xml generated
View File

@@ -1,48 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="NullableNotNullManager">
<option name="myDefaultNullable" value="android.support.annotation.Nullable" />
<option name="myDefaultNotNull" value="android.support.annotation.NonNull" />
<option name="myNullables">
<value>
<list size="12">
<item index="0" class="java.lang.String" itemvalue="org.jetbrains.annotations.Nullable" />
<item index="1" class="java.lang.String" itemvalue="javax.annotation.Nullable" />
<item index="2" class="java.lang.String" itemvalue="javax.annotation.CheckForNull" />
<item index="3" class="java.lang.String" itemvalue="edu.umd.cs.findbugs.annotations.Nullable" />
<item index="4" class="java.lang.String" itemvalue="android.support.annotation.Nullable" />
<item index="5" class="java.lang.String" itemvalue="androidx.annotation.Nullable" />
<item index="6" class="java.lang.String" itemvalue="androidx.annotation.RecentlyNullable" />
<item index="7" class="java.lang.String" itemvalue="android.annotation.Nullable" />
<item index="8" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.qual.Nullable" />
<item index="9" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.compatqual.NullableDecl" />
<item index="10" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.compatqual.NullableType" />
<item index="11" class="java.lang.String" itemvalue="com.android.annotations.Nullable" />
</list>
</value>
</option>
<option name="myNotNulls">
<value>
<list size="11">
<item index="0" class="java.lang.String" itemvalue="org.jetbrains.annotations.NotNull" />
<item index="1" class="java.lang.String" itemvalue="javax.annotation.Nonnull" />
<item index="2" class="java.lang.String" itemvalue="edu.umd.cs.findbugs.annotations.NonNull" />
<item index="3" class="java.lang.String" itemvalue="android.support.annotation.NonNull" />
<item index="4" class="java.lang.String" itemvalue="androidx.annotation.NonNull" />
<item index="5" class="java.lang.String" itemvalue="androidx.annotation.RecentlyNonNull" />
<item index="6" class="java.lang.String" itemvalue="android.annotation.NonNull" />
<item index="7" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.qual.NonNull" />
<item index="8" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.compatqual.NonNullDecl" />
<item index="9" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.compatqual.NonNullType" />
<item index="10" class="java.lang.String" itemvalue="com.android.annotations.NonNull" />
</list>
</value>
</option>
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_17_PREVIEW" project-jdk-name="1.8" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" />
</component>
<component name="ProjectType">
<option name="id" value="Android" />
</component>
</project>

6
.idea/vcs.xml generated
View File

@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

View File

@@ -150,7 +150,7 @@ dependencies {
implementation "org.kodein.di:kodein-di-generic-jvm:$kodein_version" implementation "org.kodein.di:kodein-di-generic-jvm:$kodein_version"
implementation "org.kodein.di:kodein-di-framework-android-x:$kodein_version" implementation "org.kodein.di:kodein-di-framework-android-x:$kodein_version"
/ * Image Carousal */ / * Image Carousal */
implementation 'com.synnapps:carouselview:0.1.5' implementation 'com.synnapps:carouselview:0.1.6'
/ * Glide */ / * Glide */
implementation 'com.github.bumptech.glide:glide:4.12.0' implementation 'com.github.bumptech.glide:glide:4.12.0'
annotationProcessor 'com.github.bumptech.glide:compiler:4.12.0' annotationProcessor 'com.github.bumptech.glide:compiler:4.12.0'

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,23 @@
package h_mal.appttude.com.driver.application
import h_mal.appttude.com.driver.data.prefs.PreferenceProvider
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

@@ -0,0 +1,27 @@
package h_mal.appttude.com.driver.model
import h_mal.appttude.com.driver.R
enum class DatabaseStatus(val drawable: Int, val header: Int, val subtext: Int) {
NO_CONNECTION(R.drawable.baseline_inbox_24, R.string.no_connection, R.string.no_connection_subtext),
NO_PERMISSION(
R.drawable.baseline_inbox_24,
R.string.no_permission,
R.string.no_permission_subtext
),
CANNOT_RETRIEVE(
R.drawable.baseline_inbox_24,
R.string.cannot_retrieve,
R.string.cannot_retrieve_subtext
),
NO_AUTHORIZATION(
R.drawable.baseline_inbox_24,
R.string.no_authorization,
R.string.no_authorization_subtext
),
EMPTY_RESULTS(
R.drawable.baseline_inbox_24,
R.string.no_drivers_to_show,
R.string.no_drivers_subtext
)
}

View File

@@ -1,6 +1,12 @@
package h_mal.appttude.com.driver.objects package h_mal.appttude.com.driver.objects
import h_mal.appttude.com.driver.model.* import h_mal.appttude.com.driver.model.DriversLicense
import h_mal.appttude.com.driver.model.Insurance
import h_mal.appttude.com.driver.model.Logbook
import h_mal.appttude.com.driver.model.Mot
import h_mal.appttude.com.driver.model.PrivateHireLicense
import h_mal.appttude.com.driver.model.PrivateHireVehicle
import h_mal.appttude.com.driver.model.VehicleProfile
data class ArchiveObject( data class ArchiveObject(

View File

@@ -1,6 +1,9 @@
package h_mal.appttude.com.driver.objects.wholeObject package h_mal.appttude.com.driver.objects.wholeObject
import h_mal.appttude.com.driver.model.* import h_mal.appttude.com.driver.model.Insurance
import h_mal.appttude.com.driver.model.Logbook
import h_mal.appttude.com.driver.model.Mot
import h_mal.appttude.com.driver.model.PrivateHireVehicle
import h_mal.appttude.com.driver.model.VehicleProfile import h_mal.appttude.com.driver.model.VehicleProfile
data class VehicleProfile ( data class VehicleProfile (

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 ->
binding.root.setOnClickListener { callback.invoke(key) } item.score.takeIf { it != 0 }?.let {
} binding.root.setOnClickListener { callback.invoke(key) }
binding.approvalIv.setImageResource(getImageResourceBasedOnApproval(itemValue)) }
binding.approvalStatus.text = listItemView.context.getString(getStringResourceBasedOnApproval(itemValue)) binding.approvalIv.setImageResource(item.drawableId)
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,39 @@
package h_mal.appttude.com.driver.ui
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

@@ -19,6 +19,8 @@ import h_mal.appttude.com.driver.base.CustomViewHolder
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.FragmentHomeSuperUserBinding import h_mal.appttude.com.driver.databinding.FragmentHomeSuperUserBinding
import h_mal.appttude.com.driver.databinding.ListItemLayoutBinding import h_mal.appttude.com.driver.databinding.ListItemLayoutBinding
import h_mal.appttude.com.driver.model.DatabaseStatus
import h_mal.appttude.com.driver.model.DatabaseStatus.*
import h_mal.appttude.com.driver.model.SortOption import h_mal.appttude.com.driver.model.SortOption
import h_mal.appttude.com.driver.objects.UserObject import h_mal.appttude.com.driver.objects.UserObject
import h_mal.appttude.com.driver.objects.WholeDriverObject import h_mal.appttude.com.driver.objects.WholeDriverObject
@@ -27,7 +29,8 @@ import h_mal.appttude.com.driver.viewmodels.SuperUserViewModel
import java.util.* import java.util.*
class HomeSuperUserFragment : BaseFragment<SuperUserViewModel, FragmentHomeSuperUserBinding>(), MenuProvider { class HomeSuperUserFragment : BaseFragment<SuperUserViewModel, FragmentHomeSuperUserBinding>(),
MenuProvider {
private lateinit var adapter: FirebaseRecyclerAdapter<WholeDriverObject, CustomViewHolder<ListItemLayoutBinding>> private lateinit var adapter: FirebaseRecyclerAdapter<WholeDriverObject, CustomViewHolder<ListItemLayoutBinding>>
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
@@ -42,6 +45,17 @@ class HomeSuperUserFragment : BaseFragment<SuperUserViewModel, FragmentHomeSuper
is FirebaseRecyclerOptions<*> -> setAdapterToRecyclerView(data) is FirebaseRecyclerOptions<*> -> setAdapterToRecyclerView(data)
} }
} }
private fun setNonView(status: DatabaseStatus) {
applyBinding {
emptyView.run {
root.setOnClickListener(null)
root.visibility = View.VISIBLE
icon.setImageResource(status.drawable)
header.setText(status.header)
subtext.setText(status.subtext)
}
}
}
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
private fun setAdapterToRecyclerView(options: FirebaseRecyclerOptions<*>) { private fun setAdapterToRecyclerView(options: FirebaseRecyclerOptions<*>) {
@@ -74,8 +88,8 @@ class HomeSuperUserFragment : BaseFragment<SuperUserViewModel, FragmentHomeSuper
} }
private fun createAdapter(options: FirebaseRecyclerOptions<WholeDriverObject>): BaseFirebaseAdapter<WholeDriverObject, ListItemLayoutBinding> { private fun createAdapter(options: FirebaseRecyclerOptions<WholeDriverObject>): BaseFirebaseAdapter<WholeDriverObject, ListItemLayoutBinding> {
return object : BaseFirebaseAdapter<WholeDriverObject, ListItemLayoutBinding>(options, layoutInflater) { return object :
BaseFirebaseAdapter<WholeDriverObject, ListItemLayoutBinding>(options, layoutInflater) {
override fun onBindViewHolder( override fun onBindViewHolder(
holder: CustomViewHolder<ListItemLayoutBinding>, holder: CustomViewHolder<ListItemLayoutBinding>,
position: Int, position: Int,
@@ -87,7 +101,8 @@ class HomeSuperUserFragment : BaseFragment<SuperUserViewModel, FragmentHomeSuper
usernameText.text = userDetails?.profileName usernameText.text = userDetails?.profileName
emailaddressText.text = userDetails?.profileEmail emailaddressText.text = userDetails?.profileEmail
driverNo.run { driverNo.run {
val number = if (model.driver_number.isNullOrBlank()) "#N/A" else model.driver_number val number =
if (model.driver_number.isNullOrBlank()) "#N/A" else model.driver_number
text = number text = number
setOnClickListener { setOnClickListener {
getKeyAtPosition(position)?.let { showChangeNumberDialog(number!!, it) } getKeyAtPosition(position)?.let { showChangeNumberDialog(number!!, it) }
@@ -105,10 +120,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()
} }
} }
@@ -123,8 +136,24 @@ class HomeSuperUserFragment : BaseFragment<SuperUserViewModel, FragmentHomeSuper
applyBinding { progressCircular.hide() } applyBinding { progressCircular.hide() }
} }
override fun connectionLost() { override fun authorizationError() {
requireContext().displayToast("No connection available") setNonView(NO_AUTHORIZATION)
}
override fun cannotRetrieve() {
setNonView(CANNOT_RETRIEVE)
}
override fun noConnection() {
setNonView(NO_CONNECTION)
}
override fun permissionsDenied() {
setNonView(NO_PERMISSION)
}
override fun emptyList() {
setNonView(EMPTY_RESULTS)
} }
} }
} }
@@ -134,7 +163,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,104 @@
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.InsuranceFragment
import h_mal.appttude.com.driver.ui.vehicleprofile.LogbookFragment
import h_mal.appttude.com.driver.ui.vehicleprofile.MotFragment
import h_mal.appttude.com.driver.ui.vehicleprofile.PrivateHireVehicleFragment
import h_mal.appttude.com.driver.ui.vehicleprofile.VehicleProfileFragment
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

@@ -22,7 +22,7 @@ class SuperUserViewModel(
} }
fun createFirebaseOptions(sort: SortOption? = null) { fun createFirebaseOptions(sort: SortOption? = null) {
val ref = firebaseDatabaseSource.getUsersRef() val ref = firebaseDatabaseSource.getUsersRef().orderByChild("role").startAt("driver").endAt("driver")
sort?.isNotNull { preferenceProvider.setSortOption(it.label) } sort?.isNotNull { preferenceProvider.setSortOption(it.label) }
@@ -33,7 +33,7 @@ class SuperUserViewModel(
// } // }
val options = FirebaseRecyclerOptions.Builder<WholeDriverObject>() val options = FirebaseRecyclerOptions.Builder<WholeDriverObject>()
.setQuery(ref.orderByKey(), WholeDriverObject::class.java) .setQuery(ref, WholeDriverObject::class.java)
.build() .build()
onSuccess(options) onSuccess(options)
@@ -47,7 +47,7 @@ class SuperUserViewModel(
onError("No driver identifier provided") onError("No driver identifier provided")
return@doTryOperation return@doTryOperation
} }
val text = if (input.length > 6) input.substring(0,7) else input val text = if (input.length > 6) input.substring(0, 7) else input
firebaseDatabaseSource.run { firebaseDatabaseSource.run {
postToDatabaseRed(getDriverNumberRef(uid), text) postToDatabaseRed(getDriverNumberRef(uid), text)

View File

@@ -0,0 +1,54 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/background_with_curve"
xmlns:app="http://schemas.android.com/apk/res-auto">
<ImageView
android:layout_width="match_parent"
android:layout_height="0dp"
android:src="@drawable/splash_screen"
android:scaleType="centerCrop"
android:paddingTop="@dimen/default_indicator_margin_horizontal"
android:layout_marginTop="@dimen/default_indicator_margin_horizontal"
app:layout_constraintHeight_percent="0.5"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
android:importantForAccessibility="no" />
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
style="@style/constraint_container">
<ImageView
android:id="@+id/icon"
android:layout_width="80dp"
android:layout_height="80dp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintBottom_toTopOf="@id/header"
android:src="@drawable/baseline_inbox_24"
android:contentDescription="@string/image_icon_for_feedback_view" />
<TextView
android:id="@+id/header"
style="@style/headerStyle"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
android:gravity="center"
android:text="@string/no_drivers_to_show"/>
<TextView
android:id="@+id/subtext"
style="@style/subheader"
app:layout_constraintTop_toBottomOf="@id/header"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
android:gravity="center"
android:text="@string/no_drivers_subtext"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

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

@@ -3,6 +3,7 @@
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:id="@+id/container"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
tools:context=".ui.HomeSuperUserFragment"> tools:context=".ui.HomeSuperUserFragment">
@@ -28,9 +29,9 @@
<include <include
android:id="@+id/empty_view" android:id="@+id/empty_view"
layout="@layout/empty_layout" layout="@layout/empty_users_view"
android:layout_width="wrap_content" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="match_parent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintLeft_toLeftOf="parent"
@@ -38,5 +39,4 @@
android:visibility="gone" android:visibility="gone"
tools:visibility="visible"/> tools:visibility="visible"/>
</androidx.constraintlayout.widget.ConstraintLayout> </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

@@ -24,7 +24,11 @@ import androidx.test.espresso.intent.Intents
import androidx.test.espresso.intent.Intents.intending import androidx.test.espresso.intent.Intents.intending
import androidx.test.espresso.intent.matcher.IntentMatchers import androidx.test.espresso.intent.matcher.IntentMatchers
import androidx.test.espresso.intent.matcher.IntentMatchers.hasAction import androidx.test.espresso.intent.matcher.IntentMatchers.hasAction
import androidx.test.espresso.matcher.ViewMatchers.* import androidx.test.espresso.matcher.ViewMatchers.hasDescendant
import androidx.test.espresso.matcher.ViewMatchers.isRoot
import androidx.test.espresso.matcher.ViewMatchers.withClassName
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
import h_mal.appttude.com.driver.helpers.DataHelper import h_mal.appttude.com.driver.helpers.DataHelper
import h_mal.appttude.com.driver.helpers.EspressoHelper.waitForView import h_mal.appttude.com.driver.helpers.EspressoHelper.waitForView
import org.hamcrest.CoreMatchers.allOf import org.hamcrest.CoreMatchers.allOf
@@ -33,6 +37,7 @@ import org.hamcrest.Matcher
import org.hamcrest.Matchers import org.hamcrest.Matchers
import java.io.File import java.io.File
@SuppressWarnings("unused")
open class BaseTestRobot { open class BaseTestRobot {
fun fillEditText(resId: Int, text: String?): ViewInteraction = fun fillEditText(resId: Int, text: String?): ViewInteraction =
@@ -51,6 +56,9 @@ open class BaseTestRobot {
fun matchText(viewInteraction: ViewInteraction, text: String): ViewInteraction = viewInteraction fun matchText(viewInteraction: ViewInteraction, text: String): ViewInteraction = viewInteraction
.check(matches(withText(text))) .check(matches(withText(text)))
fun matchText(viewId: Int, textId: Int): ViewInteraction = onView(withId(viewId))
.check(matches(withText(textId)))
fun matchText(resId: Int, text: String): ViewInteraction = matchText(matchView(resId), text) fun matchText(resId: Int, text: String): ViewInteraction = matchText(matchView(resId), text)
fun clickListItem(listRes: Int, position: Int) { fun clickListItem(listRes: Int, position: Int) {
@@ -60,7 +68,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 +77,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,32 @@
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.os.Build
import android.view.View import android.view.View
import android.view.WindowManager
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.test.core.app.ActivityScenario import androidx.test.core.app.ActivityScenario
import androidx.test.espresso.Espresso.onView import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.IdlingRegistry import androidx.test.espresso.IdlingRegistry
import androidx.test.espresso.IdlingResource import androidx.test.espresso.IdlingResource
import androidx.test.espresso.Root
import androidx.test.espresso.UiController import androidx.test.espresso.UiController
import androidx.test.espresso.ViewAction import androidx.test.espresso.ViewAction
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.isRoot import androidx.test.espresso.matcher.ViewMatchers.isRoot
import androidx.test.platform.app.InstrumentationRegistry import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
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 +38,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 +47,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 +59,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
) )
} }
@@ -50,7 +67,7 @@ open class BaseUiTest<T : BaseActivity<*, *>>(
fun waitFor(delay: Long) { fun waitFor(delay: Long) {
onView(isRoot()).perform(object : ViewAction { onView(isRoot()).perform(object : ViewAction {
override fun getConstraints(): Matcher<View> = isRoot() override fun getConstraints(): Matcher<View> = isRoot()
override fun getDescription(): String? = "wait for $delay milliseconds" override fun getDescription(): String = "wait for $delay milliseconds"
override fun perform(uiController: UiController, v: View?) { override fun perform(uiController: UiController, v: View?) {
uiController.loopMainThreadForAtLeast(delay) uiController.loopMainThreadForAtLeast(delay)
} }
@@ -58,5 +75,55 @@ open class BaseUiTest<T : BaseActivity<*, *>>(
} }
open fun beforeLaunch() {} open fun beforeLaunch() {}
open fun afterLaunch() {} open fun afterLaunch(context: Context) {}
@Suppress("DEPRECATION")
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()))
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
waitFor(3500)
}
}
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,29 @@
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.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
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

@@ -8,10 +8,15 @@ import androidx.test.espresso.matcher.ViewMatchers.withTagKey
import h_mal.appttude.com.driver.BaseTestRobot import h_mal.appttude.com.driver.BaseTestRobot
import h_mal.appttude.com.driver.R import h_mal.appttude.com.driver.R
import h_mal.appttude.com.driver.base.CustomViewHolder import h_mal.appttude.com.driver.base.CustomViewHolder
import h_mal.appttude.com.driver.model.DatabaseStatus
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())
} }
@@ -39,4 +44,10 @@ class HomeAdminRobot : BaseTestRobot() {
// Click OK // Click OK
onView(withId(android.R.id.button1)).perform(ViewActions.click()) onView(withId(android.R.id.button1)).perform(ViewActions.click())
} }
fun showNoPermissionsDisplay() {
matchViewWaitFor(R.id.header)
matchText(R.id.header, DatabaseStatus.NO_PERMISSION.header)
matchText(R.id.subtext, DatabaseStatus.NO_PERMISSION.subtext)
}
} }

View File

@@ -0,0 +1,15 @@
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.R
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 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

@@ -1,6 +1,7 @@
package h_mal.appttude.com.driver.tests package h_mal.appttude.com.driver.tests
import h_mal.appttude.com.driver.ADMIN_EMAIL import h_mal.appttude.com.driver.ADMIN_EMAIL
import h_mal.appttude.com.driver.DRIVER_EMAIL
import h_mal.appttude.com.driver.FirebaseTest import h_mal.appttude.com.driver.FirebaseTest
import h_mal.appttude.com.driver.robots.homeAdmin import h_mal.appttude.com.driver.robots.homeAdmin
import h_mal.appttude.com.driver.robots.login import h_mal.appttude.com.driver.robots.login
@@ -18,7 +19,17 @@ class UserListTest : FirebaseTest<LoginActivity>(LoginActivity::class.java) {
homeAdmin { homeAdmin {
clickOnDriverIdentifier("rsaif660@gmail.com") clickOnDriverIdentifier("rsaif660@gmail.com")
submitDialog("ID45") submitDialog("ID45")
waitFor(5000) }
}
@Test
fun loginAsUser_unableToSeeDrivers_loggedIn() {
login {
waitFor(1100)
attemptLogin(DRIVER_EMAIL)
}
homeAdmin {
showNoPermissionsDisplay()
} }
} }

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

@@ -4,24 +4,28 @@ import android.app.Application
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.FirebaseDatabaseSource
import h_mal.appttude.com.driver.data.FirebaseStorageSource 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.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,36 @@
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.View.OnAttachStateChangeListener
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 +117,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 +148,44 @@ 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)
}
})
} else {
}
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

@@ -8,15 +8,20 @@ import androidx.recyclerview.widget.RecyclerView.ViewHolder
import androidx.viewbinding.ViewBinding import androidx.viewbinding.ViewBinding
import com.firebase.ui.database.FirebaseRecyclerAdapter import com.firebase.ui.database.FirebaseRecyclerAdapter
import com.firebase.ui.database.FirebaseRecyclerOptions import com.firebase.ui.database.FirebaseRecyclerOptions
import com.google.firebase.database.DatabaseError
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 java.nio.ByteBuffer import java.nio.ByteBuffer
open class BaseFirebaseAdapter<T: Any, VB : ViewBinding>(options: FirebaseRecyclerOptions<T>, private val layoutInflater: LayoutInflater): open class BaseFirebaseAdapter<T : Any, VB : ViewBinding>(
options: FirebaseRecyclerOptions<T>,
private val layoutInflater: LayoutInflater
) :
FirebaseRecyclerAdapter<T, CustomViewHolder<VB>>(options) { FirebaseRecyclerAdapter<T, CustomViewHolder<VB>>(options) {
private val connectivityManager = layoutInflater.context.getSystemService(ConnectivityManager::class.java) as ConnectivityManager private val connectivityManager =
layoutInflater.context.getSystemService(ConnectivityManager::class.java) as ConnectivityManager
private var _binding: VB? = null private var _binding: VB? = null
val binding: VB val binding: VB
@@ -32,7 +37,7 @@ open class BaseFirebaseAdapter<T: Any, VB : ViewBinding>(options: FirebaseRecycl
return CustomViewHolder(requireNotNull(_binding)) return CustomViewHolder(requireNotNull(_binding))
} }
override fun onBindViewHolder(holder: CustomViewHolder<VB>, position: Int, model: T) { } override fun onBindViewHolder(holder: CustomViewHolder<VB>, position: Int, model: T) {}
override fun getItemId(position: Int): Long { override fun getItemId(position: Int): Long {
return snapshots.getSnapshot(position).key?.toByteArray() return snapshots.getSnapshot(position).key?.toByteArray()
@@ -50,6 +55,26 @@ open class BaseFirebaseAdapter<T: Any, VB : ViewBinding>(options: FirebaseRecycl
} }
open fun connectionLost() {} open fun connectionLost() {}
override fun onDataChanged() {
super.onDataChanged()
if (itemCount == 0) emptyList()
}
override fun onError(error: DatabaseError) {
super.onError(error)
when (error.code) {
DatabaseError.PERMISSION_DENIED -> permissionsDenied()
DatabaseError.DISCONNECTED, DatabaseError.UNAVAILABLE, DatabaseError.NETWORK_ERROR -> noConnection()
DatabaseError.EXPIRED_TOKEN, DatabaseError.OPERATION_FAILED, DatabaseError.INVALID_TOKEN, DatabaseError.MAX_RETRIES -> authorizationError()
else -> cannotRetrieve()
}
}
open fun permissionsDenied() {}
open fun noConnection() {}
open fun cannotRetrieve() {}
open fun authorizationError() {}
open fun emptyList() {}
} }
class CustomViewHolder<VB : ViewBinding>(val viewBinding: VB): ViewHolder(viewBinding.root) class CustomViewHolder<VB : ViewBinding>(val viewBinding: VB) : ViewHolder(viewBinding.root)

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

@@ -20,6 +20,9 @@ abstract class BaseViewModel : ViewModel() {
uiState.postValue(ViewState.HasError(Event(error))) uiState.postValue(ViewState.HasError(Event(error)))
} }
/*
* All in one function for trying an operation and handling its start and failure
*/
suspend fun doTryOperation( suspend fun doTryOperation(
defaultErrorMessage: String?, defaultErrorMessage: String?,
operation: suspend () -> Unit operation: suspend () -> Unit

View File

@@ -2,7 +2,11 @@ package h_mal.appttude.com.driver.data
import android.net.Uri import android.net.Uri
import com.google.android.gms.tasks.Task import com.google.android.gms.tasks.Task
import com.google.firebase.auth.* import com.google.firebase.auth.AuthResult
import com.google.firebase.auth.EmailAuthProvider
import com.google.firebase.auth.FirebaseAuth
import com.google.firebase.auth.FirebaseUser
import com.google.firebase.auth.UserProfileChangeRequest
import java.io.IOException import java.io.IOException
class FirebaseAuthSource : FirebaseAuthentication { class FirebaseAuthSource : FirebaseAuthentication {

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

@@ -0,0 +1,55 @@
package h_mal.appttude.com.driver.utils
import com.google.firebase.database.DatabaseError
class FirebaseException(
private val databaseError: DatabaseError
) : RuntimeException(databaseError.message, databaseError.toException()) {
fun getCode() = databaseError.code
fun getDetails() = databaseError.details
fun getErrorStatus(): Status {
return Status.getByScore(getCode()) ?: Status.UNKNOWN_ERROR
}
enum class Status(private val code: Int) {
DATA_STALE(-1),
/** The server indicated that this operation failed */
OPERATION_FAILED(-2),
/** This client does not have permission to perform this operation */
PERMISSION_DENIED(-3),
/** The operation had to be aborted due to a network disconnect */
DISCONNECTED(-4),
/** The supplied auth token has expired */
EXPIRED_TOKEN (-6),
/**
* The specified authentication token is invalid. This can occur when the token is malformed,
* expired, or the secret that was used to generate it has been revoked.
*/
INVALID_TOKEN(-7),
/** The transaction had too many retries */
MAX_RETRIES(-8),
/** The transaction was overridden by a subsequent set */
OVERRIDDEN_BY_SET(-9),
/** The service is unavailable */
UNAVAILABLE(-10),
/** An exception occurred in user code */
USER_CODE_EXCEPTION(-11),
/** The operation could not be performed due to a network error. */
NETWORK_ERROR(-24),
/** The write was canceled locally */
WRITE_CANCELED(-25),
/**
* An unknown error occurred. Please refer to the error message and error details for more
* information.
*/
UNKNOWN_ERROR(-999);
companion object {
infix fun getByScore(value: Int): Status? =
Status.values().firstOrNull { it.code == value }
}
}
}

View File

@@ -30,7 +30,6 @@ suspend fun DatabaseReference.singleValueEvent(): EventResponse = suspendCorouti
/** /**
* Read database reference once {@link #DatabaseReference.addListenerForSingleValueEvent} * Read database reference once {@link #DatabaseReference.addListenerForSingleValueEvent}
* *
*
* @return T * @return T
*/ */
suspend inline fun <reified T : Any> DatabaseReference.getDataFromDatabaseRef(): T? { suspend inline fun <reified T : Any> DatabaseReference.getDataFromDatabaseRef(): T? {
@@ -39,7 +38,23 @@ suspend inline fun <reified T : Any> DatabaseReference.getDataFromDatabaseRef():
response.snapshot.getValue(T::class.java) response.snapshot.getValue(T::class.java)
} }
is EventResponse.Cancelled -> { is EventResponse.Cancelled -> {
throw response.error.toException() throw FirebaseException(response.error)
}
}
}
/**
* Read database reference once {@link #DatabaseReference.addListenerForSingleValueEvent}
*
* @return T
*/
suspend inline fun <reified T : Any> DatabaseReference.getListDataFromDatabaseRef(): List<T?> {
return when (val response: EventResponse = singleValueEvent()) {
is EventResponse.Changed -> {
response.snapshot.children.map { it.getValue(T::class.java) }
}
is EventResponse.Cancelled -> {
throw FirebaseException(response.error)
} }
} }
} }
@@ -50,7 +65,7 @@ suspend fun <T: Any> DatabaseReference.getDataFromDatabaseRef(clazz : Class<T>):
response.snapshot.getValue(clazz) response.snapshot.getValue(clazz)
} }
is EventResponse.Cancelled -> { is EventResponse.Cancelled -> {
throw response.error.toException() throw FirebaseException(response.error)
} }
} }
} }

View File

@@ -2,7 +2,6 @@ package h_mal.appttude.com.driver.utils
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.Activity import android.app.Activity
import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.res.Resources import android.content.res.Resources
import android.graphics.Bitmap import android.graphics.Bitmap
@@ -16,7 +15,6 @@ import android.view.inputmethod.InputMethodManager
import android.widget.EditText import android.widget.EditText
import android.widget.ImageView import android.widget.ImageView
import android.widget.TextView import android.widget.TextView
import android.widget.Toast
import androidx.annotation.DrawableRes import androidx.annotation.DrawableRes
import androidx.appcompat.widget.SearchView import androidx.appcompat.widget.SearchView
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
@@ -33,14 +31,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) {

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 127 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 185 KiB

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>

View File

@@ -0,0 +1,5 @@
<vector android:height="72dp" android:tint="#FFFFFF"
android:viewportHeight="24" android:viewportWidth="24"
android:width="72dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M19,3L4.99,3c-1.11,0 -1.98,0.89 -1.98,2L3,19c0,1.1 0.88,2 1.99,2L19,21c1.1,0 2,-0.9 2,-2L21,5c0,-1.11 -0.9,-2 -2,-2zM19,15h-4c0,1.66 -1.35,3 -3,3s-3,-1.34 -3,-3L4.99,15L4.99,5L19,5v10z"/>
</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,18 @@
<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>
<string name="no_drivers_to_show">No drivers to show</string>
<string name="no_drivers_subtext">There are no drivers present for your organisation.</string>
<string name="no_permission">You do not have permissions to view</string>
<string name="no_permission_subtext">You are not a super user. Contact us to get super user access.</string>
<string name="cannot_retrieve">Cannot retrieve data.</string>
<string name="cannot_retrieve_subtext">Check you are logged in correctly and have a working connection.</string>
<string name="no_connection">No connection</string>
<string name="no_connection_subtext">Make you have a valid internet connection.</string>
<string name="no_authorization">Authentication has failed</string>
<string name="no_authorization_subtext">There is a problem with authentication.</string>
<string name="image_icon_for_feedback_view">Image icon for feedback view.</string>
</resources> </resources>

View File

@@ -1,9 +1,17 @@
{ {
"rules": { "rules": {
"user": { "user": {
".read": true, ".read": "root.child('user').child(auth.uid).child('role').val() == 'admin'",
"$user_id": { "$user_id": {
".write": "$user_id === auth.uid" ".write": "$user_id === auth.uid",
".read": "$user_id === auth.uid",
"driver_number": {
".write": "root.child('user').child(auth.uid).child('role').val() == 'admin'",
".read": "root.child('user').child(auth.uid).child('role').val() == 'admin'"
},
"approvalsObject": {
".write": "root.child('user').child(auth.uid).child('role').val() == 'admin'"
}
} }
} }
} }