Storage permission dispatcher (#33)

- storage permissions request updated
 - test suite expanded
This commit is contained in:
2023-08-18 21:26:21 +01:00
committed by GitHub
parent 4d00545fd3
commit 1e528617f2
117 changed files with 1188 additions and 823 deletions

View File

@@ -20,16 +20,10 @@ commands:
command: | command: |
echo "$GOOGLE_SERVICES_KEY" > "app/google-services.json" echo "$GOOGLE_SERVICES_KEY" > "app/google-services.json"
- android/restore-gradle-cache - android/restore-gradle-cache
build_gradle:
description: Build the gradle
steps:
- android/restore-gradle-cache
- run: - run:
name: Download Dependencies name: allow gradle
command: | command: |
sudo chmod +x ./gradlew sudo chmod +x ./gradlew
./gradlew androidDependencies
- android/save-gradle-cache
run_tests: run_tests:
description: run non-instrumentation tests for flavour specified description: run non-instrumentation tests for flavour specified
parameters: parameters:
@@ -38,11 +32,11 @@ commands:
default: "Driver" default: "Driver"
steps: steps:
# The next step will run the unit tests # The next step will run the unit tests
- build_gradle
- run: - run:
name: Run non-instrumentation unit tests name: Run non-instrumentation unit tests
command: | command: |
./gradlew test<< parameters.flavour >>DebugUnitTest --continue ./gradlew test<< parameters.flavour >>DebugUnitTest
- android/save-gradle-cache
- store_artifacts: - store_artifacts:
path: app/build/reports path: app/build/reports
destination: reports destination: reports
@@ -53,10 +47,9 @@ commands:
parameters: parameters:
flavour: flavour:
type: string type: string
default: "AtlasWeather" default: "Driver"
steps: steps:
# Download and cache dependencies # Download and cache dependencies
- build_gradle
- run: - run:
name: Setup subtree for test data name: Setup subtree for test data
command: | command: |
@@ -78,9 +71,7 @@ commands:
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
pull-data: true pre-test-command: adb push driver_app_data/images /sdcard/Camera
pull-data-path: /storage/emulated/0/Android/data/
pull-data-target: ~/app-data
pre-emulator-wait-steps: pre-emulator-wait-steps:
# Start firebase emulator in the background while waiting to start testing # Start firebase emulator in the background while waiting to start testing
- run: - run:
@@ -94,6 +85,13 @@ commands:
paths: paths:
- ~/.cache/firebase/emulators/ - ~/.cache/firebase/emulators/
key: emulator-cache-v1-{{ epoch }} key: emulator-cache-v1-{{ epoch }}
# store screenshots for failed ui tests
- when:
condition: on_fail
steps:
- store_artifacts:
path: app/build/outputs/connected_android_test_additional_output/
destination: connected_android_test
# store test reports # store test reports
- store_artifacts: - store_artifacts:
path: app/build/reports/androidTests/connected path: app/build/reports/androidTests/connected
@@ -128,7 +126,6 @@ commands:
name: Setup playstore key name: Setup playstore key
command: | command: |
echo "$GOOGLE_PLAY_KEY" > "google-play-key.json" echo "$GOOGLE_PLAY_KEY" > "google-play-key.json"
- build_gradle
- run: - run:
name: Run fastlane command to deploy to playstore name: Run fastlane command to deploy to playstore
command: | command: |
@@ -156,8 +153,23 @@ jobs:
steps: steps:
# Checkout the code and its submodule as the first step. # Checkout the code and its submodule as the first step.
- setup_repo - setup_repo
# - run_tests: - run_tests:
# flavour: << parameters.flavour >> flavour: << parameters.flavour >>
run_instrumentation_test:
# Parameters used for determining
parameters:
flavour:
type: string
default: "Driver"
# These next lines define the Android machine image executor.
# See: https://circleci.com/docs/2.0/executor-types/
executor:
name: android/android-machine
tag: 2023.05.1
# Add steps to the job
# See: https://circleci.com/docs/2.0/configuration-reference/#steps
steps:
- setup_repo
- run_ui_tests: - run_ui_tests:
flavour: << parameters.flavour >> flavour: << parameters.flavour >>
deploy-to-playstore: deploy-to-playstore:
@@ -187,6 +199,14 @@ workflows:
branches: branches:
ignore: ignore:
- main_admin - main_admin
- run_instrumentation_test:
context: appttude
flavour: "Driver"
filters:
branches:
only:
- master
- main_driver
- deploy-to-playstore: - deploy-to-playstore:
context: appttude context: appttude
flavour: "Driver" flavour: "Driver"
@@ -195,7 +215,7 @@ workflows:
only: only:
- main_driver - main_driver
requires: requires:
- build-and-test - run_instrumentation_test
build-release-admin: build-release-admin:
jobs: jobs:
- build-and-test: - build-and-test:
@@ -205,12 +225,20 @@ workflows:
branches: branches:
ignore: ignore:
- main_driver - main_driver
- deploy-to-playstore: - run_instrumentation_test:
context: appttude context: appttude
flavour: "Admin" flavour: "Admin"
filters:
branches:
only:
- master
- main_admin
- deploy-to-playstore:
context: appttude
flavour: "Driver"
filters: filters:
branches: branches:
only: only:
- main_admin - main_admin
requires: requires:
- build-and-test - run_instrumentation_test

2
.gitignore vendored
View File

@@ -27,3 +27,5 @@ local
# Firebase emulator # Firebase emulator
database-debug.log database-debug.log
firebase-debug.log firebase-debug.log
# Subtree
driver_app_data

View File

@@ -166,4 +166,10 @@ dependencies {
androidTestImplementation "com.squareup.retrofit2:converter-gson:$retrofit_version" androidTestImplementation "com.squareup.retrofit2:converter-gson:$retrofit_version"
/ * screenshot library */ / * screenshot library */
androidTestImplementation 'tools.fastlane:screengrab:2.1.1' androidTestImplementation 'tools.fastlane:screengrab:2.1.1'
/ * Permissions dispatcher * /
def dispatcher_ver = "4.9.2"
implementation "com.github.permissions-dispatcher:permissionsdispatcher:${dispatcher_ver}"
kapt "com.github.permissions-dispatcher:permissionsdispatcher-processor:${dispatcher_ver}"
/ * Date utils * /
implementation 'net.danlew:android.joda:2.12.5'
} }

View File

@@ -0,0 +1,9 @@
{
"address": "123 test street update",
"dateFirst": "26/01/2019",
"dob": "26/01/1979",
"forenames": "Alex Smith",
"driverPic": "driver_profile_pic.jpg",
"ni": "NI 12 34 56 A",
"postcode": "EC1V 2AL"
}

View File

@@ -0,0 +1,5 @@
{
"licenseExpiry": "27/04/2019",
"licenseImageString": "driver_license_driver.jpg",
"licenseNumber": "FARME100165AB5EW"
}

View File

@@ -0,0 +1,7 @@
{
"expiryDate": "03/02/2019",
"insurerName": "Insurer",
"photoStrings": [
"driver_insurance.jpg"
]
}

View File

@@ -0,0 +1,4 @@
{
"photoString": "driver_logbook.jpg",
"v5cnumber": "NJ59NTV"
}

View File

@@ -0,0 +1,4 @@
{
"motExpiry": "11/06/2019",
"motImageString": "driver_mot.jpg"
}

View File

@@ -0,0 +1,5 @@
{
"phExpiry": "27/04/2019",
"phImageString": "driver_license_private_hire.jpg",
"phNumber": "987651"
}

View File

@@ -0,0 +1,5 @@
{
"phCarExpiry": "28/01/2019",
"phCarImageString": "driver_license_private_hire_car.jpg",
"phCarNumber": "4602060501"
}

View File

@@ -0,0 +1,11 @@
{
"colour": "Black",
"keeperAddress": "483 Green lanes London",
"keeperName": "Adam Cars Ltd",
"keeperPostCode": "N13 4BS",
"make": "Toyota",
"model": "Prius",
"reg": "NG59ERY",
"seized": false,
"startDate": "04/02/2019"
}

View File

@@ -7,7 +7,7 @@ import android.content.res.Resources
import android.net.Uri import android.net.Uri
import android.view.View import android.view.View
import android.widget.DatePicker import android.widget.DatePicker
import android.widget.ListView import androidx.annotation.IdRes
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.recyclerview.widget.RecyclerView.ViewHolder import androidx.recyclerview.widget.RecyclerView.ViewHolder
import androidx.test.espresso.Espresso.onData import androidx.test.espresso.Espresso.onData
@@ -17,13 +17,13 @@ import androidx.test.espresso.ViewAction
import androidx.test.espresso.ViewInteraction import androidx.test.espresso.ViewInteraction
import androidx.test.espresso.action.ViewActions import androidx.test.espresso.action.ViewActions
import androidx.test.espresso.action.ViewActions.click import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.action.ViewActions.scrollTo
import androidx.test.espresso.action.ViewActions.swipeDown import androidx.test.espresso.action.ViewActions.swipeDown
import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.contrib.PickerActions import androidx.test.espresso.contrib.PickerActions
import androidx.test.espresso.contrib.RecyclerViewActions import androidx.test.espresso.contrib.RecyclerViewActions
import androidx.test.espresso.intent.Intents 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.hasAction import androidx.test.espresso.intent.matcher.IntentMatchers.hasAction
import androidx.test.espresso.matcher.ViewMatchers.hasDescendant import androidx.test.espresso.matcher.ViewMatchers.hasDescendant
import androidx.test.espresso.matcher.ViewMatchers.isRoot import androidx.test.espresso.matcher.ViewMatchers.isRoot
@@ -41,34 +41,40 @@ import java.io.File
@SuppressWarnings("unused") @SuppressWarnings("unused")
open class BaseTestRobot { open class BaseTestRobot {
fun fillEditText(resId: Int, text: String?): ViewInteraction = fun fillEditText(@IdRes resId: Int, text: String?): ViewInteraction =
onView(withId(resId)).perform( onView(withId(resId)).perform(
ViewActions.replaceText(text), ViewActions.replaceText(text),
ViewActions.closeSoftKeyboard() ViewActions.closeSoftKeyboard()
) )
fun clickButton(resId: Int): ViewInteraction = fun scrollAndFillEditText(@IdRes resId: Int, text: String?): ViewInteraction =
onView(withId(resId)).perform(
scrollTo(),
ViewActions.replaceText(text),
ViewActions.closeSoftKeyboard()
)
fun clickButton(@IdRes resId: Int): ViewInteraction =
onView((withId(resId))).perform(click()) onView((withId(resId))).perform(click())
fun matchView(resId: Int): ViewInteraction = onView(withId(resId)) fun matchView(@IdRes resId: Int): ViewInteraction = onView(withId(resId))
fun matchViewWaitFor(resId: Int): ViewInteraction = waitForView(withId(resId)) fun matchViewWaitFor(@IdRes resId: Int): ViewInteraction = waitForView(withId(resId))
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)) fun matchText(@StringRes stringId:Int): ViewInteraction = onView(withText(stringId))
.check(matches(withText(textId)))
fun matchText(resId: Int, text: String): ViewInteraction = matchText(matchView(resId), text) fun matchText(@IdRes resId: Int, text: String): ViewInteraction = matchText(matchView(resId), text)
fun clickListItem(listRes: Int, position: Int) { fun clickListItem(@IdRes listRes: Int, position: Int) {
onData(anything()) onData(anything())
.inAdapterView(allOf(withId(listRes))) .inAdapterView(allOf(withId(listRes)))
.atPosition(position).perform(click()) .atPosition(position).perform(click())
} }
fun <VH : ViewHolder> scrollToRecyclerItem(recyclerId: Int, text: String): ViewInteraction? { fun <VH : ViewHolder> scrollToRecyclerItem(@IdRes recyclerId: Int, text: String): ViewInteraction? {
return matchView(recyclerId) return matchView(recyclerId)
.perform( .perform(
// scrollTo will fail the test if no item matches. // scrollTo will fail the test if no item matches.
@@ -78,7 +84,7 @@ open class BaseTestRobot {
) )
} }
fun <VH : ViewHolder> scrollToRecyclerItem(recyclerId: Int, resIdForString: Int): ViewInteraction? { fun <VH : ViewHolder> scrollToRecyclerItem(@IdRes recyclerId: Int, resIdForString: Int): ViewInteraction? {
return matchView(recyclerId) return matchView(recyclerId)
.perform( .perform(
// scrollTo will fail the test if no item matches. // scrollTo will fail the test if no item matches.
@@ -88,7 +94,7 @@ open class BaseTestRobot {
) )
} }
fun <VH : ViewHolder> scrollToRecyclerItemByPosition(recyclerId: Int, position: Int): ViewInteraction? { fun <VH : ViewHolder> scrollToRecyclerItemByPosition(@IdRes recyclerId: Int, position: Int): ViewInteraction? {
return matchView(recyclerId) return matchView(recyclerId)
.perform( .perform(
// scrollTo will fail the test if no item matches. // scrollTo will fail the test if no item matches.
@@ -96,7 +102,7 @@ open class BaseTestRobot {
) )
} }
fun <VH : ViewHolder> clickViewInRecycler(recyclerId: Int, text: String) { fun <VH : ViewHolder> clickViewInRecycler(@IdRes recyclerId: Int, text: String) {
matchView(recyclerId) matchView(recyclerId)
.perform( .perform(
// scrollTo will fail the test if no item matches. // scrollTo will fail the test if no item matches.
@@ -104,7 +110,7 @@ open class BaseTestRobot {
) )
} }
fun <VH : ViewHolder> clickViewInRecycler(recyclerId: Int, resIdForString: Int) { fun <VH : ViewHolder> clickViewInRecycler(@IdRes recyclerId: Int, resIdForString: Int) {
matchView(recyclerId) matchView(recyclerId)
.perform( .perform(
// scrollTo will fail the test if no item matches. // scrollTo will fail the test if no item matches.
@@ -112,7 +118,7 @@ open class BaseTestRobot {
) )
} }
fun <VH : ViewHolder> clickSubViewInRecycler(recyclerId: Int, text: String, subView: Int) { fun <VH : ViewHolder> clickSubViewInRecycler(@IdRes recyclerId: Int, text: String, subView: Int) {
scrollToRecyclerItem<VH>(recyclerId, text) scrollToRecyclerItem<VH>(recyclerId, text)
?.perform( ?.perform(
// scrollTo will fail the test if no item matches. // scrollTo will fail the test if no item matches.
@@ -128,13 +134,13 @@ open class BaseTestRobot {
) )
} }
fun checkErrorOnTextEntry(resId: Int, errorMessage: String): ViewInteraction = fun checkErrorOnTextEntry(@IdRes resId: Int, errorMessage: String): ViewInteraction =
onView(withId(resId)).check(matches(checkErrorMessage(errorMessage))) onView(withId(resId)).check(matches(checkErrorMessage(errorMessage)))
fun checkImageViewHasImage(resId: Int): ViewInteraction = fun checkImageViewDoesNotHaveDefaultImage(@IdRes resId: Int): ViewInteraction =
onView(withId(resId)).check(matches(checkImage())) onView(withId(resId)).check(matches(checkImage()))
fun swipeDown(resId: Int): ViewInteraction = fun swipeDown(@IdRes resId: Int): ViewInteraction =
onView(withId(resId)).perform(swipeDown()) onView(withId(resId)).perform(swipeDown())
fun getStringFromResource(@StringRes resId: Int): String = fun getStringFromResource(@StringRes resId: Int): String =
@@ -150,12 +156,12 @@ open class BaseTestRobot {
) )
} }
fun selectSingleImageFromGallery(filePath: FormRobot.FilePath, openSelector: () -> Unit) { fun selectSingleImageFromGallery(filePath: String, openSelector: () -> Unit) {
Intents.init() Intents.init()
// Build the result to return when the activity is launched. // Build the result to return when the activity is launched.
val resultData = Intent() val resultData = Intent()
resultData.flags = Intent.FLAG_GRANT_READ_URI_PERMISSION resultData.flags = Intent.FLAG_GRANT_READ_URI_PERMISSION
resultData.data = Uri.fromFile(File(FormRobot.FilePath.getFilePath(filePath))) resultData.data = Uri.fromFile(File("/sdcard/Camera/", filePath))
val result = Instrumentation.ActivityResult(Activity.RESULT_OK, resultData) val result = Instrumentation.ActivityResult(Activity.RESULT_OK, resultData)
// Set up result stubbing when an intent sent to image picker is seen. // Set up result stubbing when an intent sent to image picker is seen.
intending(hasAction(Intent.ACTION_GET_CONTENT)).respondWith(result) intending(hasAction(Intent.ACTION_GET_CONTENT)).respondWith(result)
@@ -164,7 +170,7 @@ open class BaseTestRobot {
Intents.release() Intents.release()
} }
fun selectMultipleImageFromGallery(filePaths: Array<String>, openSelector: () -> Unit) { fun selectMultipleImageFromGallery(filePaths: List<String>, openSelector: () -> Unit) {
Intents.init() Intents.init()
// Build the result to return when the activity is launched. // Build the result to return when the activity is launched.
val resultData = Intent() val resultData = Intent()
@@ -172,7 +178,7 @@ open class BaseTestRobot {
resultData.clipData = clipData resultData.clipData = clipData
val result = Instrumentation.ActivityResult(Activity.RESULT_OK, resultData) val result = Instrumentation.ActivityResult(Activity.RESULT_OK, resultData)
// Set up result stubbing when an intent sent to "contacts" is seen. // Set up result stubbing when an intent sent to "contacts" is seen.
intending(IntentMatchers.toPackage("android.intent.action.PICK")).respondWith(result) intending(hasAction(Intent.ACTION_GET_CONTENT)).respondWith(result)
openSelector() openSelector()
Intents.release() Intents.release()

View File

@@ -3,38 +3,41 @@ package h_mal.appttude.com.driver
import android.Manifest import android.Manifest
import android.R import android.R
import android.app.Activity 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.*
import androidx.test.espresso.Espresso.onView import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.IdlingRegistry
import androidx.test.espresso.IdlingResource
import androidx.test.espresso.UiController
import androidx.test.espresso.ViewAction
import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.* import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.isRoot
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
import androidx.test.rule.GrantPermissionRule import androidx.test.rule.GrantPermissionRule
import com.google.gson.Gson
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 h_mal.appttude.com.driver.helpers.BaseViewAction
import h_mal.appttude.com.driver.helpers.SnapshotRule import h_mal.appttude.com.driver.helpers.SnapshotRule
import org.hamcrest.CoreMatchers 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.hamcrest.core.AllOf
import org.junit.After import org.junit.After
import org.junit.Before import org.junit.Before
import org.junit.ClassRule
import org.junit.Rule import org.junit.Rule
import tools.fastlane.screengrab.Screengrab import tools.fastlane.screengrab.Screengrab
import tools.fastlane.screengrab.UiAutomatorScreenshotStrategy import tools.fastlane.screengrab.UiAutomatorScreenshotStrategy
import tools.fastlane.screengrab.locale.LocaleTestRule import tools.fastlane.screengrab.locale.LocaleTestRule
import java.io.BufferedReader
open class BaseUiTest<T : BaseActivity<*, *>>( open class BaseUiTest<T : BaseActivity<*, *>>(
private val activity: Class<T> private val activity: Class<T>
) { ) {
val gson by lazy { Gson() }
private lateinit var mActivityScenarioRule: ActivityScenario<T> private lateinit var mActivityScenarioRule: ActivityScenario<T>
private var mIdlingResource: IdlingResource? = null private var mIdlingResource: IdlingResource? = null
@@ -42,7 +45,7 @@ open class BaseUiTest<T : BaseActivity<*, *>>(
private lateinit var currentActivity: Activity private lateinit var currentActivity: Activity
@get:Rule @get:Rule
var permissionRule = GrantPermissionRule.grant(Manifest.permission.WRITE_EXTERNAL_STORAGE) var permissionRule = GrantPermissionRule.grant(Manifest.permission.READ_EXTERNAL_STORAGE)
@get:Rule @get:Rule
var snapshotRule: SnapshotRule = SnapshotRule() var snapshotRule: SnapshotRule = SnapshotRule()
@@ -59,8 +62,8 @@ open class BaseUiTest<T : BaseActivity<*, *>>(
mActivityScenarioRule.onActivity { mActivityScenarioRule.onActivity {
mIdlingResource = it.getIdlingResource()!! mIdlingResource = it.getIdlingResource()!!
IdlingRegistry.getInstance().register(mIdlingResource) IdlingRegistry.getInstance().register(mIdlingResource)
afterLaunch(it)
} }
afterLaunch()
} }
@After @After
@@ -87,36 +90,7 @@ open class BaseUiTest<T : BaseActivity<*, *>>(
} }
open fun beforeLaunch() {} open fun beforeLaunch() {}
open fun afterLaunch(context: Context) {} open fun afterLaunch() {}
@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) { fun checkSnackBarDisplayedByMessage(message: String) {
onView( onView(
@@ -138,4 +112,22 @@ open class BaseUiTest<T : BaseActivity<*, *>>(
}) })
return currentActivity return currentActivity
} }
fun <T: Any> readDataFromAsset(fileName: String, clazz: Class<T>): T {
val iStream =
getInstrumentation().context.assets.open("$fileName.json")
val data = iStream.bufferedReader().use(BufferedReader::readText)
return gson.fromJson(data, clazz)
}
inline fun <reified M: Any> readDataFromAsset(fileName: String): M {
val iStream =
getInstrumentation().context.assets.open("$fileName.json")
val data = iStream.bufferedReader().use(BufferedReader::readText)
return fromJson<M>(data)
}
inline fun <reified M> fromJson(json: String)
= gson.fromJson<M>(json, M::class.java)
} }

View File

@@ -5,41 +5,32 @@ import android.view.View
import android.widget.EditText import android.widget.EditText
import android.widget.ImageView import android.widget.ImageView
import com.google.android.material.textfield.TextInputLayout import com.google.android.material.textfield.TextInputLayout
import org.hamcrest.Description import h_mal.appttude.com.driver.helpers.BaseMatcher
import org.hamcrest.Matcher import org.hamcrest.Matcher
import org.hamcrest.TypeSafeMatcher
/** /**
* Matcher for testing error of TextInputLayout * Matcher for testing error of TextInputLayout
*/ */
fun checkErrorMessage(expectedErrorText: String): Matcher<View?> { fun checkErrorMessage(expectedErrorText: String): Matcher<View> {
return object : TypeSafeMatcher<View?>() { return object : BaseMatcher<View>() {
override fun matchesSafely(view: View?): Boolean { override fun match(item: View): Boolean {
if (view is EditText) { if (item is EditText) {
return view.error.toString() == expectedErrorText return item.error.toString() == expectedErrorText
} }
if (view !is TextInputLayout) return false if (item !is TextInputLayout) return false
val error = view.error ?: return false val error = item.error ?: return false
return expectedErrorText == error.toString() return expectedErrorText == error.toString()
} }
override fun describeTo(d: Description?) {}
} }
} }
fun checkImage(): Matcher<View?> { @Suppress("UNCHECKED_CAST")
return object : TypeSafeMatcher<View?>() { fun checkImage(): Matcher<View> {
override fun matchesSafely(view: View?): Boolean { return object: BaseMatcher<ImageView>() {
if (view is ImageView) { override fun match(item: ImageView): Boolean = hasImage(item)
return hasImage(view)
}
return false
}
override fun describeTo(d: Description?) {}
private fun hasImage(view: ImageView): Boolean { private fun hasImage(view: ImageView): Boolean {
val drawable = view.drawable val drawable = view.drawable
@@ -49,6 +40,6 @@ fun checkImage(): Matcher<View?> {
} }
return hasImage return hasImage
} }
} } as Matcher<View>
} }

View File

@@ -1,13 +1,27 @@
package h_mal.appttude.com.driver package h_mal.appttude.com.driver
import androidx.test.espresso.Espresso.onView import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.ViewInteraction
import androidx.test.espresso.action.ViewActions.click import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.action.ViewActions.scrollTo
import androidx.test.espresso.matcher.ViewMatchers.isAssignableFrom
import androidx.test.espresso.matcher.ViewMatchers.withId import androidx.test.espresso.matcher.ViewMatchers.withId
import h_mal.appttude.com.driver.helpers.getImagePath import h_mal.appttude.com.driver.helpers.EspressoHelper.trying
import h_mal.appttude.com.driver.model.Model
import org.hamcrest.CoreMatchers.allOf
import java.time.LocalDate
import java.time.format.DateTimeFormatter
open class FormRobot : BaseTestRobot() { open class FormRobot<T : Model> : BaseTestRobot() {
fun submit() = clickButton(R.id.submit)
fun submit() = onView(
allOf(
withId(R.id.submit),
isAssignableFrom(com.google.android.material.button.MaterialButton::class.java)
)
).perform(click())
fun setDate(datePickerLaunchViewId: Int, year: Int, monthOfYear: Int, dayOfMonth: Int) { fun setDate(datePickerLaunchViewId: Int, year: Int, monthOfYear: Int, dayOfMonth: Int) {
onView(withId(datePickerLaunchViewId)).perform(click()) onView(withId(datePickerLaunchViewId)).perform(click())
selectDateInPicker(year, monthOfYear, dayOfMonth) selectDateInPicker(year, monthOfYear, dayOfMonth)
@@ -15,32 +29,50 @@ open class FormRobot : BaseTestRobot() {
onView(withId(android.R.id.button1)).perform(click()) onView(withId(android.R.id.button1)).perform(click())
} }
fun selectSingleImage(imagePickerLauncherViewId: Int, filePath: FilePath) { fun setDate(datePickerLaunchViewId: Int, dateString: String) {
selectSingleImageFromGallery(filePath) { onView(withId(datePickerLaunchViewId)).perform(click())
onView(withId(imagePickerLauncherViewId)).perform(click()) val date = LocalDate.parse(dateString, DateTimeFormatter.ofPattern("dd/MM/yyyy"))
} selectDateInPicker(date.year, date.monthValue, date.dayOfMonth)
// click ok in date picker // click ok in date picker
onView(withId(android.R.id.button1)).perform(click())
} }
fun selectMultipleImage(imagePickerLauncherViewId: Int, filePaths: Array<String>) { fun scrollAndSetDate(datePickerLaunchViewId: Int, dateString: String) {
selectMultipleImageFromGallery(filePaths) { onView(withId(datePickerLaunchViewId)).perform(scrollTo(), click())
val date = LocalDate.parse(dateString, DateTimeFormatter.ofPattern("dd/MM/yyyy"))
selectDateInPicker(date.year, date.monthValue, date.dayOfMonth)
// click ok in date picker
onView(withId(android.R.id.button1)).perform(click())
}
fun selectSingleImage(imagePickerLauncherViewId: Int, fileName: String) {
selectSingleImageFromGallery(fileName) {
onView(withId(imagePickerLauncherViewId)).perform(click()) onView(withId(imagePickerLauncherViewId)).perform(click())
} }
} }
enum class FilePath(val path: String) { fun selectSingleImage(imagePickerViewInteraction: ViewInteraction, fileName: String) {
PROFILE_PIC("driver_profile_pic.jpg"), selectSingleImageFromGallery(fileName) {
INSURANCE("driver_insurance.jpg"), imagePickerViewInteraction.perform(click())
PRIVATE_HIRE("driver_license_private_hire.jpg"),
PRIVATE_HIRE_CAR("driver_license_private_hire_car.jpg"),
LOGBOOK("driver_logbook.jpg"),
MOT("driver_mot.jpg"),
LICENSE("driver_license_driver.jpg");
companion object {
fun getFilePath(filePath: FilePath): String {
return getImagePath(filePath.path)
}
} }
} }
fun selectMultipleImage(imagePickerLauncherViewId: Int, filePaths: List<String>) {
selectMultipleImageFromGallery(filePaths.map { "/sdcard/Camera/$it" }) {
onView(withId(imagePickerLauncherViewId)).perform(click())
}
}
open fun submitForm(data: T) {
(trying {
onView(withId(R.id.submit)).perform(scrollTo())
} ?: onView(withId(R.id.submit))).perform(click())
}
open fun validateSubmission(data: T) {}
open fun submitAndValidate(data: T) {
submitForm(data)
validateSubmission(data)
}
} }

View File

@@ -1,17 +1,22 @@
package h_mal.appttude.com.driver.helpers package h_mal.appttude.com.driver.helpers
import android.view.View
import org.hamcrest.BaseMatcher
import org.hamcrest.Description import org.hamcrest.Description
import org.hamcrest.TypeSafeMatcher
class BaseMatcher: BaseMatcher<View>() { open class BaseMatcher<T: Any>: TypeSafeMatcher<T>() {
override fun describeTo(description: Description?) { override fun describeTo(description: Description?) { }
TODO("Not yet implemented")
override fun describeMismatchSafely(item: T, mismatchDescription: Description?) {
describe(item, mismatchDescription)
} }
override fun matches(actual: Any?): Boolean {
TODO("Not yet implemented")
}
override fun matchesSafely(item: T): Boolean = match(item)
open fun match(item: T): Boolean { return false }
open fun describe(item: T, mismatchDescription: Description?) {
super.describeMismatchSafely(item, mismatchDescription)
}
} }

View File

@@ -1,14 +1 @@
package h_mal.appttude.com.driver.helpers package h_mal.appttude.com.driver.helpers
import android.os.Environment
import java.io.File
/**
* File paths for images on device
*/
fun getImagePath(imageConst: String): String {
return File(
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM),
"/Camera/images/$imageConst"
).absolutePath
}

View File

@@ -18,10 +18,9 @@ object DataHelper {
fun createClipData(filePath: String) = createClipData(createClipItem(filePath)) fun createClipData(filePath: String) = createClipData(createClipItem(filePath))
fun createClipData(filePaths: Array<String>): ClipData { fun createClipData(filePaths: List<String>): ClipData {
val clipData = createClipData(filePaths[0]) val clipData = createClipData(filePaths[0])
val remainingFiles = filePaths.copyOfRange(1, filePaths.size - 1) filePaths.filterIndexed { i, _ -> i > 0 }.let { clipData.addFilePaths(it.toTypedArray()) }
clipData.addFilePaths(remainingFiles)
return clipData return clipData
} }

View File

@@ -120,4 +120,50 @@ object EspressoHelper {
throw Exception("Error finding a view matching $viewMatcher") throw Exception("Error finding a view matching $viewMatcher")
} }
/**
* try and perform a view interaction for
* @param waitMillis at intervals of
* @param waitMillisPerTry,
* upon failure to locate an element, it will return null
*
*/
fun ViewInteraction.tryPerform(
vararg viewActions: ViewAction,
waitMillis: Int = 1000,
waitMillisPerTry: Long = 200,
): ViewInteraction? {
// Derive the max tries
val maxTries = waitMillis / waitMillisPerTry.toInt()
var tries = 0
for (i in 0..maxTries)
try {
// Track the amount of times we've tried
tries++
// Search the root for the view
return perform(*viewActions)
} catch (e: Exception) {
if (tries == maxTries) {
throw e
}
sleep(waitMillisPerTry)
}
return null
}
fun <T: Any> trying(action: () -> T): T? {
return try {
val result = action.invoke()
result
}catch (_: Exception) {
null
}
}
} }

View File

@@ -3,7 +3,6 @@ package h_mal.appttude.com.driver.helpers
import org.junit.rules.TestWatcher import org.junit.rules.TestWatcher
import org.junit.runner.Description import org.junit.runner.Description
import tools.fastlane.screengrab.Screengrab import tools.fastlane.screengrab.Screengrab
import tools.fastlane.screengrab.UiAutomatorScreenshotStrategy
/** /**
* Junit rule that takes a screenshot when a test fails. * Junit rule that takes a screenshot when a test fails.

View File

@@ -1,11 +1,24 @@
package h_mal.appttude.com.driver.utils package h_mal.appttude.com.driver.untiTests
import androidx.startup.AppInitializer
import androidx.test.platform.app.InstrumentationRegistry
import h_mal.appttude.com.driver.utils.DateUtils
import h_mal.appttude.com.driver.utils.DateUtils.convertDateStringDatePattern import h_mal.appttude.com.driver.utils.DateUtils.convertDateStringDatePattern
import org.junit.Assert.* import net.danlew.android.joda.JodaTimeInitializer
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test import org.junit.Test
class DateUtilsTest { class DateUtilsTest {
@Before
fun setup() {
AppInitializer.getInstance(InstrumentationRegistry.getInstrumentation().context.applicationContext)
.initializeComponent(JodaTimeInitializer::class.java)
}
@Test @Test
fun test_getDateTimeStamp() { fun test_getDateTimeStamp() {
val regex1 = "[0-9]{8}_[0-9]{6}".toRegex() val regex1 = "[0-9]{8}_[0-9]{6}".toRegex()
@@ -31,6 +44,8 @@ class DateUtilsTest {
} }
@Test @Test
fun test_parseCalenderIntoDateString() { fun test_getDateString() {
val date = DateUtils.getDateString(2019, 8, 1)
assertEquals(date, "01/08/2019")
} }
} }

View File

@@ -1,11 +1,12 @@
package h_mal.appttude.com.driver.robots package h_mal.appttude.com.driver.robots
import h_mal.appttude.com.driver.FormRobot import h_mal.appttude.com.driver.BaseTestRobot
import h_mal.appttude.com.driver.R import h_mal.appttude.com.driver.R
fun delete(func: DeleteRobot.() -> Unit) = DeleteRobot().apply { func() } fun delete(func: DeleteRobot.() -> Unit) = DeleteRobot().apply { func() }
class DeleteRobot : FormRobot() { class DeleteRobot : BaseTestRobot() {
fun submit() = clickButton(R.id.submit)
fun enterEmail(email: String) = fillEditText(R.id.email_update, email) fun enterEmail(email: String) = fillEditText(R.id.email_update, email)
fun enterPassword(password: String) = fillEditText(R.id.password_top, password) fun enterPassword(password: String) = fillEditText(R.id.password_top, password)

View File

@@ -1,11 +1,12 @@
package h_mal.appttude.com.driver.robots package h_mal.appttude.com.driver.robots
import h_mal.appttude.com.driver.FormRobot import h_mal.appttude.com.driver.BaseTestRobot
import h_mal.appttude.com.driver.R import h_mal.appttude.com.driver.R
fun updateEmail(func: UpdateEmailRobot.() -> Unit) = UpdateEmailRobot().apply { func() } fun updateEmail(func: UpdateEmailRobot.() -> Unit) = UpdateEmailRobot().apply { func() }
class UpdateEmailRobot : FormRobot() { class UpdateEmailRobot : BaseTestRobot() {
fun submit() = clickButton(R.id.submit)
fun enterEmail(email: String) = fillEditText(R.id.email_update, email) fun enterEmail(email: String) = fillEditText(R.id.email_update, email)
fun enterPassword(password: String) = fillEditText(R.id.password_top, password) fun enterPassword(password: String) = fillEditText(R.id.password_top, password)
fun enterNewEmail(email: String) = fillEditText(R.id.new_email, email) fun enterNewEmail(email: String) = fillEditText(R.id.new_email, email)

View File

@@ -1,11 +1,12 @@
package h_mal.appttude.com.driver.robots package h_mal.appttude.com.driver.robots
import h_mal.appttude.com.driver.FormRobot import h_mal.appttude.com.driver.BaseTestRobot
import h_mal.appttude.com.driver.R import h_mal.appttude.com.driver.R
fun updatePassword(func: UpdatePasswordRobot.() -> Unit) = UpdatePasswordRobot().apply { func() } fun updatePassword(func: UpdatePasswordRobot.() -> Unit) = UpdatePasswordRobot().apply { func() }
class UpdatePasswordRobot : FormRobot() { class UpdatePasswordRobot : BaseTestRobot() {
fun submit() = clickButton(R.id.submit)
fun enterEmail(email: String) = fillEditText(R.id.email_update, email) fun enterEmail(email: String) = fillEditText(R.id.email_update, email)
fun enterPassword(password: String) = fillEditText(R.id.password_top, password) fun enterPassword(password: String) = fillEditText(R.id.password_top, password)
fun enterNewPassword(email: String) = fillEditText(R.id.password_bottom, email) fun enterNewPassword(email: String) = fillEditText(R.id.password_bottom, email)

View File

@@ -1,16 +1,18 @@
package h_mal.appttude.com.driver.robots package h_mal.appttude.com.driver.robots
import h_mal.appttude.com.driver.FormRobot import h_mal.appttude.com.driver.BaseTestRobot
import h_mal.appttude.com.driver.R import h_mal.appttude.com.driver.R
fun updateProfile(func: UpdateProfileRobot.() -> Unit) = UpdateProfileRobot().apply { func() } fun updateProfile(func: UpdateProfileRobot.() -> Unit) = UpdateProfileRobot().apply { func() }
class UpdateProfileRobot : FormRobot() { class UpdateProfileRobot : BaseTestRobot() {
fun submit() = clickButton(R.id.submit)
fun enterName(name: String) = fillEditText(R.id.update_name, name) fun enterName(name: String) = fillEditText(R.id.update_name, name)
fun selectImage() = selectSingleImage(R.id.profile_img, FilePath.PROFILE_PIC)
fun submitForm(name: String) { fun submitForm(name: String) {
// selectImage() selectSingleImageFromGallery("driver_profile_pic") {
clickButton(R.id.profile_img)
}
enterName(name) enterName(name)
submit() submit()
} }

View File

@@ -2,26 +2,23 @@ package h_mal.appttude.com.driver.robots.driver
import h_mal.appttude.com.driver.FormRobot import h_mal.appttude.com.driver.FormRobot
import h_mal.appttude.com.driver.R import h_mal.appttude.com.driver.R
import h_mal.appttude.com.driver.model.DriversLicense
fun driversLicense(func: DriversLicenseRobot.() -> Unit) = DriversLicenseRobot().apply { func() } fun driversLicense(func: DriversLicenseRobot.() -> Unit) = DriversLicenseRobot().apply { func() }
class DriversLicenseRobot : FormRobot() { class DriversLicenseRobot : FormRobot<DriversLicense>() {
fun enterLicenseNumber(text: String) = fillEditText(R.id.lic_no, text) fun enterLicenseNumber(text: String) = fillEditText(R.id.lic_no, text)
fun enterLicenseExpiry(year: Int, monthOfYear: Int, dayOfMonth: Int) = fun enterLicenseExpiry(data: String) = setDate(R.id.lic_expiry, data)
setDate(R.id.lic_expiry, year, monthOfYear, dayOfMonth)
fun selectImage() = selectSingleImage(R.id.search_image, FilePath.LICENSE) override fun submitForm(data: DriversLicense) {
selectSingleImage(R.id.search_image, data.licenseImageString!!)
fun submitForm(licenseNumber: String, year: Int, monthOfYear: Int, dayOfMonth: Int) { enterLicenseExpiry(data.licenseExpiry!!)
selectImage() enterLicenseNumber(data.licenseNumber!!)
enterLicenseNumber(licenseNumber) super.submitForm(data)
enterLicenseExpiry(year, monthOfYear, dayOfMonth)
submit()
} }
fun validate() { override fun validateSubmission(data: DriversLicense) {
checkImageViewHasImage(R.id.driversli_img) checkImageViewDoesNotHaveDefaultImage(R.id.driversli_img)
} }
} }

View File

@@ -1,38 +1,39 @@
package h_mal.appttude.com.driver.robots.driver package h_mal.appttude.com.driver.robots.driver
import androidx.test.espresso.action.ViewActions.closeSoftKeyboard
import androidx.test.espresso.action.ViewActions.scrollTo
import h_mal.appttude.com.driver.FormRobot import h_mal.appttude.com.driver.FormRobot
import h_mal.appttude.com.driver.R import h_mal.appttude.com.driver.R
import h_mal.appttude.com.driver.model.DriverProfile
fun driversProfile(func: DriversProfileRobot.() -> Unit) = DriversProfileRobot().apply { func() } fun driversProfile(func: DriversProfileRobot.() -> Unit) = DriversProfileRobot().apply { func() }
class DriversProfileRobot : FormRobot() { class DriversProfileRobot : FormRobot<DriverProfile>() {
fun enterName(name: String) = fillEditText(R.id.names_input, name) fun enterName(name: String) = fillEditText(R.id.names_input, name)
fun enterAddress(address: String) = fillEditText(R.id.address_input, address) fun enterAddress(address: String) = fillEditText(R.id.address_input, address)
fun enterPostcode(postcode: String) = fillEditText(R.id.postcode_input, postcode) fun enterPostcode(postcode: String) = fillEditText(R.id.postcode_input, postcode)
fun enterDateOfBirth(dob: String) = fillEditText(R.id.dob_input, dob) fun enterDateOfBirth(date: String) = setDate(R.id.dob_input, date)
fun enterNINumber(niNumber: String) = fillEditText(R.id.ni_number, niNumber) fun enterNINumber(niNumber: String) = fillEditText(R.id.ni_number, niNumber)
fun enterDateFirstAvailable(year: Int, monthOfYear: Int, dayOfMonth: Int) = fun enterDateFirstAvailable(date: String) {
setDate(R.id.date_first, year, monthOfYear, dayOfMonth) closeSoftKeyboard()
matchView(R.id.date_first).perform(scrollTo())
setDate(R.id.date_first, date)
}
fun selectImage() = selectSingleImage(R.id.add_photo, FilePath.PROFILE_PIC) override fun validateSubmission(data: DriverProfile) {
checkImageViewDoesNotHaveDefaultImage(R.id.driver_pic)
matchText(R.id.names_input, data.forenames!!)
}
fun submitForm( override fun submitForm(data: DriverProfile) = data.run {
name: String, selectSingleImage(R.id.add_photo, driverPic!!)
address: String, enterName(forenames!!)
postcode: String, enterAddress(address!!)
dob: String, enterPostcode(postcode!!)
niNumber: String, enterDateOfBirth(dob!!)
year: Int, enterNINumber(ni!!)
monthOfYear: Int, enterDateFirstAvailable(dateFirst!!)
dayOfMonth: Int super.submitForm(data)
) {
selectImage()
enterName(name)
enterAddress(address)
enterPostcode(postcode)
enterDateOfBirth(dob)
enterNINumber(niNumber)
enterDateFirstAvailable(year, monthOfYear, dayOfMonth)
submit()
} }
} }

View File

@@ -2,22 +2,29 @@ package h_mal.appttude.com.driver.robots.driver
import h_mal.appttude.com.driver.FormRobot import h_mal.appttude.com.driver.FormRobot
import h_mal.appttude.com.driver.R import h_mal.appttude.com.driver.R
import h_mal.appttude.com.driver.model.PrivateHireLicense
fun privateHireLicenseRobot(func: PrivateHireLicenseRobot.() -> Unit) = fun privateHireLicenseRobot(func: PrivateHireLicenseRobot.() -> Unit) =
PrivateHireLicenseRobot().apply { func() } PrivateHireLicenseRobot().apply { func() }
class PrivateHireLicenseRobot : FormRobot() { class PrivateHireLicenseRobot : FormRobot<PrivateHireLicense>() {
fun enterLicenseNumber(text: String) = fillEditText(R.id.ph_no, text) fun enterLicenseNumber(text: String) = fillEditText(R.id.ph_no, text)
fun enterLicenseExpiry(year: Int, monthOfYear: Int, dayOfMonth: Int) = fun enterLicenseExpiry(date: String) = setDate(R.id.ph_expiry, date)
setDate(R.id.ph_expiry, year, monthOfYear, dayOfMonth)
fun selectImage() = selectSingleImage(R.id.uploadphlic, FilePath.PRIVATE_HIRE) fun selectImage(fileName: String) = selectSingleImage(R.id.uploadphlic, fileName)
fun submitForm(licenseNumber: String, year: Int, monthOfYear: Int, dayOfMonth: Int) { override fun submitForm(data: PrivateHireLicense) {
selectImage() selectImage(data.phImageString!!)
enterLicenseNumber(licenseNumber) enterLicenseNumber(data.phNumber!!)
enterLicenseExpiry(year, monthOfYear, dayOfMonth) enterLicenseExpiry(data.phExpiry!!)
submit() super.submitForm(data)
} }
fun validate(data: PrivateHireLicense) {
checkImageViewDoesNotHaveDefaultImage(R.id.imageView2)
matchText(R.id.ph_expiry, data.phExpiry!!)
matchText(R.id.ph_no, data.phNumber!!)
}
} }

View File

@@ -1,23 +1,24 @@
package h_mal.appttude.com.driver.robots.vehicle package h_mal.appttude.com.driver.robots.vehicle
import h_mal.appttude.com.driver.FormRobot import h_mal.appttude.com.driver.FormRobot
import h_mal.appttude.com.driver.FormRobot.FilePath.Companion.getFilePath
import h_mal.appttude.com.driver.R import h_mal.appttude.com.driver.R
import h_mal.appttude.com.driver.model.Insurance
fun insurance(func: InsuranceRobot.() -> Unit) = InsuranceRobot().apply { func() } fun insurance(func: InsuranceRobot.() -> Unit) = InsuranceRobot().apply { func() }
class InsuranceRobot : FormRobot() { class InsuranceRobot : FormRobot<Insurance>() {
fun enterInsurance(text: String) = fillEditText(R.id.insurer, text) fun enterInsurance(text: String) = fillEditText(R.id.insurer, text)
fun enterInsuranceExpiry(year: Int, monthOfYear: Int, dayOfMonth: Int) = fun enterInsuranceExpiry(date: String) = setDate(R.id.insurance_exp, date)
setDate(R.id.insurance_exp, year, monthOfYear, dayOfMonth)
fun selectImages() = override fun submitForm(data: Insurance) {
selectMultipleImage(R.id.uploadInsurance, arrayOf(getFilePath(FilePath.INSURANCE))) selectMultipleImage(R.id.uploadInsurance, data.photoStrings!!.map { it!! })
enterInsurance(data.insurerName!!)
enterInsuranceExpiry(data.expiryDate!!)
super.submitForm(data)
}
fun submitForm(insurer: String, year: Int, monthOfYear: Int, dayOfMonth: Int) { override fun validateSubmission(data: Insurance) {
selectImages() matchText(R.id.insurer, data.insurerName!!)
enterInsurance(insurer) matchText(R.id.insurance_exp, data.expiryDate!!)
enterInsuranceExpiry(year, monthOfYear, dayOfMonth)
submit()
} }
} }

View File

@@ -2,17 +2,21 @@ package h_mal.appttude.com.driver.robots.vehicle
import h_mal.appttude.com.driver.FormRobot import h_mal.appttude.com.driver.FormRobot
import h_mal.appttude.com.driver.R import h_mal.appttude.com.driver.R
import h_mal.appttude.com.driver.model.Logbook
fun logbook(func: LogbookRobot.() -> Unit) = LogbookRobot().apply { func() } fun logbook(func: LogbookRobot.() -> Unit) = LogbookRobot().apply { func() }
class LogbookRobot : FormRobot() { class LogbookRobot : FormRobot<Logbook>() {
fun selectImages() = selectSingleImage(R.id.uploadmot, FilePath.MOT) fun enterV5c(v5c: String) = fillEditText(R.id.v5c_no, v5c)
fun enterExpiryDate(year: Int, monthOfYear: Int, dayOfMonth: Int) =
setDate(R.id.mot_expiry, year, monthOfYear, dayOfMonth)
fun submitForm(year: Int, monthOfYear: Int, dayOfMonth: Int) { override fun submitForm(data: Logbook) {
selectImages() selectSingleImage(R.id.upload_lb, data.photoString!!)
enterExpiryDate(year, monthOfYear, dayOfMonth) enterV5c(data.v5cnumber!!)
submit() super.submitForm(data)
}
override fun validateSubmission(data: Logbook) {
checkImageViewDoesNotHaveDefaultImage(R.id.log_book_img)
matchText(R.id.v5c_no, data.v5cnumber!!)
} }
} }

View File

@@ -2,16 +2,21 @@ package h_mal.appttude.com.driver.robots.vehicle
import h_mal.appttude.com.driver.FormRobot import h_mal.appttude.com.driver.FormRobot
import h_mal.appttude.com.driver.R import h_mal.appttude.com.driver.R
import h_mal.appttude.com.driver.model.Mot
fun mot(func: MOTRobot.() -> Unit) = MOTRobot().apply { func() } fun mot(func: MOTRobot.() -> Unit) = MOTRobot().apply { func() }
class MOTRobot : FormRobot() { class MOTRobot : FormRobot<Mot>() {
fun enterV5cNumber(v5c: String) = fillEditText(R.id.mot_expiry, v5c) fun enterMotExpiry(expiry: String) = setDate(R.id.mot_expiry, expiry)
fun selectImages() = selectSingleImage(R.id.mot_expiry, FilePath.LOGBOOK)
fun submitForm(v5c: String) { override fun submitForm(data: Mot) {
selectImages() selectSingleImage(R.id.uploadmot, data.motImageString!!)
enterV5cNumber(v5c) enterMotExpiry(data.motExpiry!!)
submit() super.submitForm(data)
}
override fun validateSubmission(data: Mot) {
checkImageViewDoesNotHaveDefaultImage(R.id.mot_img)
matchText(R.id.mot_expiry, data.motExpiry!!)
} }
} }

View File

@@ -2,22 +2,28 @@ package h_mal.appttude.com.driver.robots.vehicle
import h_mal.appttude.com.driver.FormRobot import h_mal.appttude.com.driver.FormRobot
import h_mal.appttude.com.driver.R import h_mal.appttude.com.driver.R
import h_mal.appttude.com.driver.model.PrivateHireVehicle
fun privateHireVehicleLicense(func: PrivateHireVehicleLicenseRobot.() -> Unit) = fun privateHireVehicleLicense(func: PrivateHireVehicleLicenseRobot.() -> Unit) =
PrivateHireVehicleLicenseRobot().apply { func() } PrivateHireVehicleLicenseRobot().apply { func() }
class PrivateHireVehicleLicenseRobot : FormRobot<PrivateHireVehicle>() {
class PrivateHireVehicleLicenseRobot : FormRobot() {
fun enterLicenseNumber(text: String) = fillEditText(R.id.ph_no, text) fun enterLicenseNumber(text: String) = fillEditText(R.id.ph_no, text)
fun enterLicenseExpiry(year: Int, monthOfYear: Int, dayOfMonth: Int) = fun enterLicenseExpiry(date: String) = setDate(R.id.ph_expiry, date)
setDate(R.id.ph_expiry, year, monthOfYear, dayOfMonth)
fun selectImage() = selectSingleImage(R.id.uploadphlic, FilePath.PRIVATE_HIRE) override fun submitForm(data: PrivateHireVehicle) {
selectSingleImage(
matchText(R.string.upload_private_hire_photo),
data.phCarImageString!!
)
enterLicenseNumber(data.phCarNumber!!)
enterLicenseExpiry(data.phCarExpiry!!)
super.submitForm(data)
}
fun submitForm(licenseNumber: String, year: Int, monthOfYear: Int, dayOfMonth: Int) { override fun validateSubmission(data: PrivateHireVehicle) {
selectImage() checkImageViewDoesNotHaveDefaultImage(R.id.imageView2)
enterLicenseNumber(licenseNumber) matchText(R.id.ph_no, data.phCarNumber!!)
enterLicenseExpiry(year, monthOfYear, dayOfMonth) matchText(R.id.ph_expiry, data.phCarExpiry!!)
submit()
} }
} }

View File

@@ -1,46 +1,51 @@
package h_mal.appttude.com.driver.robots.vehicle package h_mal.appttude.com.driver.robots.vehicle
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.isChecked
import androidx.test.espresso.matcher.ViewMatchers.isNotChecked
import h_mal.appttude.com.driver.FormRobot import h_mal.appttude.com.driver.FormRobot
import h_mal.appttude.com.driver.R import h_mal.appttude.com.driver.R
import h_mal.appttude.com.driver.helpers.EspressoHelper.setChecked import h_mal.appttude.com.driver.helpers.EspressoHelper.setChecked
import h_mal.appttude.com.driver.model.VehicleProfile
fun vehicleProfile(func: VehicleProfileRobot.() -> Unit) = VehicleProfileRobot().apply { func() } fun vehicleProfile(func: VehicleProfileRobot.() -> Unit) = VehicleProfileRobot().apply { func() }
class VehicleProfileRobot : FormRobot() { class VehicleProfileRobot : FormRobot<VehicleProfile>() {
fun enterRegistration(reg: String) = fillEditText(R.id.reg, reg)
fun enterMake(make: String) = fillEditText(R.id.make, make)
fun enterModel(model: String) = fillEditText(R.id.car_model, model)
fun enterColour(colour: String) = fillEditText(R.id.colour, colour)
fun enterAddress(address: String) = fillEditText(R.id.address, address)
fun enterPostcode(postCode: String) = fillEditText(R.id.postcode, postCode)
fun enterKeeperName(name: String) = fillEditText(R.id.keeper_name, name)
fun enterDateFirstAvailable(year: Int, monthOfYear: Int, dayOfMonth: Int) =
setDate(R.id.start_date, year, monthOfYear, dayOfMonth)
fun enterRegistration(reg: String) = scrollAndFillEditText(R.id.reg, reg)
fun enterMake(make: String) = scrollAndFillEditText(R.id.make, make)
fun enterModel(model: String) = scrollAndFillEditText(R.id.car_model, model)
fun enterColour(colour: String) = scrollAndFillEditText(R.id.colour, colour)
fun enterAddress(address: String) = scrollAndFillEditText(R.id.address, address)
fun enterPostcode(postCode: String) = scrollAndFillEditText(R.id.postcode, postCode)
fun enterKeeperName(name: String) = scrollAndFillEditText(R.id.keeper_name, name)
fun enterDateFirstAvailable(date: String) = scrollAndSetDate(R.id.start_date, date)
fun isSeized(seized: Boolean) = matchView(R.id.seized_checkbox).perform(setChecked(seized)) fun isSeized(seized: Boolean) = matchView(R.id.seized_checkbox).perform(setChecked(seized))
fun submitForm(
reg: String, override fun submitForm(data: VehicleProfile) {
make: String, enterRegistration(data.reg!!)
model: String, enterMake(data.make!!)
colour: String, enterModel(data.model!!)
address: String, enterColour(data.colour!!)
postCode: String, enterAddress(data.keeperAddress!!)
name: String, enterPostcode(data.keeperPostCode!!)
year: Int, enterKeeperName(data.keeperName!!)
monthOfYear: Int, enterDateFirstAvailable(data.startDate!!)
dayOfMonth: Int, isSeized(data.isSeized)
seized: Boolean = false super.submitForm(data)
) { }
enterRegistration(reg)
enterMake(make) override fun validateSubmission(data: VehicleProfile) {
enterModel(model) matchText(R.id.reg, data.reg!!)
enterColour(colour) matchText(R.id.make, data.make!!)
enterAddress(address) matchText(R.id.car_model, data.model!!)
enterPostcode(postCode) matchText(R.id.colour, data.colour!!)
enterKeeperName(name) matchText(R.id.address, data.keeperAddress!!)
enterDateFirstAvailable(year, monthOfYear, dayOfMonth) matchText(R.id.postcode, data.keeperPostCode!!)
isSeized(seized) matchText(R.id.keeper_name, data.keeperName!!)
submit() matchText(R.id.start_date, data.startDate!!)
val checking = if (data.isSeized) isChecked() else isNotChecked()
matchView(R.id.seized_checkbox).check(matches(checking))
super.validateSubmission(data)
} }
} }

View File

@@ -0,0 +1,52 @@
package h_mal.appttude.com.driver.tests.newUser
import androidx.test.espresso.matcher.ViewMatchers.withText
import h_mal.appttude.com.driver.FirebaseTest
import h_mal.appttude.com.driver.R
import h_mal.appttude.com.driver.helpers.EspressoHelper.trying
import h_mal.appttude.com.driver.helpers.EspressoHelper.waitForView
import h_mal.appttude.com.driver.model.DriverProfile
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.Model
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
import h_mal.appttude.com.driver.robots.home
import h_mal.appttude.com.driver.ui.MainActivity
import java.io.IOException
open class DataSubmissionTest :
FirebaseTest<MainActivity>(MainActivity::class.java, registered = true, signedIn = true) {
override fun afterLaunch() {
super.afterLaunch()
home {
waitForView(withText(getResourceString(R.string.welcome_title)), waitMillis = 10000)
trying {
requestProfile()
}
}
}
inline fun <reified T : Model> getAssetData(): T {
val file = when (T::class) {
DriverProfile::class -> "driver_details"
DriversLicense::class -> "drivers_license"
Insurance::class -> "insurance_details"
Logbook::class -> "log_book"
Mot::class -> "mot_details"
PrivateHireLicense::class -> "private_hire_license"
PrivateHireVehicle::class -> "private_hire_vehicle"
VehicleProfile::class -> "vehicle_details"
else -> {
throw IOException("No file for ${T::class}")
}
}
return readDataFromAsset(file)
}
}

View File

@@ -0,0 +1,13 @@
package h_mal.appttude.com.driver.tests.newUser
import h_mal.appttude.com.driver.robots.home
open class DriverProfileTest : DataSubmissionTest() {
override fun afterLaunch() {
super.afterLaunch()
home {
openDriverProfile()
}
}
}

View File

@@ -1,37 +0,0 @@
package h_mal.appttude.com.driver.tests.newUser
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
import androidx.test.rule.GrantPermissionRule
import h_mal.appttude.com.driver.FirebaseTest
import h_mal.appttude.com.driver.R
import h_mal.appttude.com.driver.robots.*
import h_mal.appttude.com.driver.robots.driver.driversLicense
import h_mal.appttude.com.driver.ui.MainActivity
import org.junit.*
import org.junit.runner.RunWith
@LargeTest
@RunWith(AndroidJUnit4::class)
class SubmitNewDataActivityTest :
FirebaseTest<MainActivity>(MainActivity::class.java, registered = true, signedIn = true) {
@Test
fun verifyUserRegistration_validUsernameAndPassword_loggedIn() {
home {
waitFor(2500)
checkTitleExists(getResourceString(R.string.welcome_title))
requestProfile()
openDriverProfile()
}
driverScreen {
driverLicense()
}
driversLicense {
submitForm("SAMPLE8456310LTU", 2022, 10, 2)
}
}
}

View File

@@ -0,0 +1,54 @@
package h_mal.appttude.com.driver.tests.newUser
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
import h_mal.appttude.com.driver.model.DriverProfile
import h_mal.appttude.com.driver.model.DriversLicense
import h_mal.appttude.com.driver.model.PrivateHireLicense
import h_mal.appttude.com.driver.robots.*
import h_mal.appttude.com.driver.robots.driver.driversLicense
import h_mal.appttude.com.driver.robots.driver.driversProfile
import h_mal.appttude.com.driver.robots.driver.privateHireLicenseRobot
import org.junit.*
import org.junit.runner.RunWith
@LargeTest
@RunWith(AndroidJUnit4::class)
class SubmitNewDriverDataTest : DriverProfileTest() {
@Test
fun signedInUser_uploadsValidLicenseDetails_uploadSuccessful() {
driverScreen {
driverLicense()
}
driversLicense {
val data = getAssetData<DriversLicense>()
submitAndValidate(data)
}
}
@Test
fun signedInUser_uploadsValidDriverDetails_uploadSuccessful() {
driverScreen {
driverProfile()
}
driversProfile {
val data = getAssetData<DriverProfile>()
submitAndValidate(data)
}
}
@Test
fun signedInUser_uploadsValidPrivateHireDetails_uploadSuccessful() {
driverScreen {
privateHireLicense()
}
privateHireLicenseRobot {
val data = getAssetData<PrivateHireLicense>()
submitAndValidate(data)
}
}
}

View File

@@ -0,0 +1,80 @@
package h_mal.appttude.com.driver.tests.newUser
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
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.robots.*
import h_mal.appttude.com.driver.robots.vehicle.insurance
import h_mal.appttude.com.driver.robots.vehicle.logbook
import h_mal.appttude.com.driver.robots.vehicle.mot
import h_mal.appttude.com.driver.robots.vehicle.privateHireVehicleLicense
import h_mal.appttude.com.driver.robots.vehicle.vehicleProfile
import org.junit.*
import org.junit.runner.RunWith
@LargeTest
@RunWith(AndroidJUnit4::class)
class SubmitNewVehicleDataTest : VehicleProfileTest() {
@Test
fun signedInUser_uploadsValidVehicleProfile_uploadSuccessful() {
vehicleScreen {
vehicleProfile()
}
vehicleProfile {
val data = getAssetData<VehicleProfile>()
submitAndValidate(data)
}
}
@Test
fun signedInUser_uploadsValidInsurance_uploadSuccessful() {
vehicleScreen {
insurance()
}
insurance {
val data = getAssetData<Insurance>()
submitAndValidate(data)
}
}
@Test
fun signedInUser_uploadsValidMot_uploadSuccessful() {
vehicleScreen {
mot()
}
mot {
val data = getAssetData<Mot>()
submitAndValidate(data)
}
}
@Test
fun signedInUser_uploadsValidLogbook_uploadSuccessful() {
vehicleScreen {
logbook()
}
logbook {
val data = getAssetData<Logbook>()
submitAndValidate(data)
}
}
@Test
fun signedInUser_uploadsValidPrivateHireVehicleLicense_uploadSuccessful() {
vehicleScreen {
privateHireVehicleLicense()
}
privateHireVehicleLicense {
val data = getAssetData<PrivateHireVehicle>()
submitAndValidate(data)
}
}
}

View File

@@ -0,0 +1,13 @@
package h_mal.appttude.com.driver.tests.newUser
import h_mal.appttude.com.driver.robots.home
open class VehicleProfileTest : DataSubmissionTest() {
override fun afterLaunch() {
super.afterLaunch()
home {
openVehicleProfile()
}
}
}

View File

@@ -5,7 +5,6 @@ import androidx.lifecycle.ViewModelProvider
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 h_mal.appttude.com.driver.viewmodels.* import h_mal.appttude.com.driver.viewmodels.*
class ApplicationViewModelFactory( class ApplicationViewModelFactory(

View File

@@ -1,18 +1,8 @@
package h_mal.appttude.com.driver.application 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.bind
import org.kodein.di.generic.instance import org.kodein.di.generic.instance
import org.kodein.di.generic.provider import org.kodein.di.generic.provider
import org.kodein.di.generic.singleton
class DriverApplication : BaseApplication() { class DriverApplication : BaseApplication() {

View File

@@ -3,7 +3,7 @@ package h_mal.appttude.com.driver.ui
import android.os.Bundle import android.os.Bundle
import android.view.View import android.view.View
import h_mal.appttude.com.driver.R import h_mal.appttude.com.driver.R
import h_mal.appttude.com.driver.base.DataSubmissionBaseFragment import h_mal.appttude.com.driver.base.BaseFragment
import h_mal.appttude.com.driver.data.DRIVER import h_mal.appttude.com.driver.data.DRIVER
import h_mal.appttude.com.driver.databinding.FragmentHomeDriverBinding import h_mal.appttude.com.driver.databinding.FragmentHomeDriverBinding
import h_mal.appttude.com.driver.utils.hide import h_mal.appttude.com.driver.utils.hide
@@ -13,9 +13,7 @@ import h_mal.appttude.com.driver.viewmodels.RoleViewModel
class HomeFragment : class HomeFragment :
DataSubmissionBaseFragment<RoleViewModel, FragmentHomeDriverBinding, String>() { BaseFragment<RoleViewModel, FragmentHomeDriverBinding>() {
override var model = String()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)

View File

@@ -12,8 +12,6 @@ import h_mal.appttude.com.driver.viewmodels.DriverLicenseViewModel
class DriverLicenseFragment : class DriverLicenseFragment :
DataSubmissionBaseFragment<DriverLicenseViewModel, FragmentDriverLicenseBinding, DriversLicense>() { DataSubmissionBaseFragment<DriverLicenseViewModel, FragmentDriverLicenseBinding, DriversLicense>() {
override var model = DriversLicense()
override fun setupView(binding: FragmentDriverLicenseBinding) { override fun setupView(binding: FragmentDriverLicenseBinding) {
binding.apply { binding.apply {
licExpiry.apply { licExpiry.apply {

View File

@@ -1,8 +1,8 @@
package h_mal.appttude.com.driver.ui.driverprofile package h_mal.appttude.com.driver.ui.driverprofile
import android.net.Uri import android.net.Uri
import h_mal.appttude.com.driver.databinding.FragmentDriverProfileBinding
import h_mal.appttude.com.driver.base.DataSubmissionBaseFragment import h_mal.appttude.com.driver.base.DataSubmissionBaseFragment
import h_mal.appttude.com.driver.databinding.FragmentDriverProfileBinding
import h_mal.appttude.com.driver.dialogs.DateDialog import h_mal.appttude.com.driver.dialogs.DateDialog
import h_mal.appttude.com.driver.model.DriverProfile import h_mal.appttude.com.driver.model.DriverProfile
import h_mal.appttude.com.driver.utils.isTrue import h_mal.appttude.com.driver.utils.isTrue
@@ -13,8 +13,6 @@ import h_mal.appttude.com.driver.viewmodels.DriverProfileViewModel
class DriverProfileFragment : class DriverProfileFragment :
DataSubmissionBaseFragment<DriverProfileViewModel, FragmentDriverProfileBinding, DriverProfile>() { DataSubmissionBaseFragment<DriverProfileViewModel, FragmentDriverProfileBinding, DriverProfile>() {
override var model = DriverProfile()
override fun setupView(binding: FragmentDriverProfileBinding) = binding.run { override fun setupView(binding: FragmentDriverProfileBinding) = binding.run {
namesInput.setTextOnChange { model.forenames = it } namesInput.setTextOnChange { model.forenames = it }
addressInput.setTextOnChange { model.address = it } addressInput.setTextOnChange { model.address = it }

View File

@@ -13,8 +13,6 @@ import h_mal.appttude.com.driver.viewmodels.PrivateHireLicenseViewModel
class PrivateHireLicenseFragment : DataSubmissionBaseFragment class PrivateHireLicenseFragment : DataSubmissionBaseFragment
<PrivateHireLicenseViewModel, FragmentPrivateHireLicenseBinding, PrivateHireLicense>() { <PrivateHireLicenseViewModel, FragmentPrivateHireLicenseBinding, PrivateHireLicense>() {
override var model = PrivateHireLicense()
override fun setupView(binding: FragmentPrivateHireLicenseBinding) = binding.run { override fun setupView(binding: FragmentPrivateHireLicenseBinding) = binding.run {
phNo.setTextOnChange { model.phNumber = it } phNo.setTextOnChange { model.phNumber = it }
phExpiry.apply { phExpiry.apply {

View File

@@ -18,8 +18,6 @@ class InsuranceFragment :
private var selectedImages: List<Uri>? = listOf() private var selectedImages: List<Uri>? = listOf()
override var model = Insurance()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
setImageSelectionAsMultiple() setImageSelectionAsMultiple()

View File

@@ -12,8 +12,6 @@ import h_mal.appttude.com.driver.viewmodels.LogbookViewModel
class LogbookFragment : class LogbookFragment :
DataSubmissionBaseFragment<LogbookViewModel, FragmentLogbookBinding, Logbook>() { DataSubmissionBaseFragment<LogbookViewModel, FragmentLogbookBinding, Logbook>() {
override var model = Logbook()
override fun setupView(binding: FragmentLogbookBinding) = binding.run { override fun setupView(binding: FragmentLogbookBinding) = binding.run {
v5cNo.setTextOnChange { model.v5cnumber = it } v5cNo.setTextOnChange { model.v5cnumber = it }
uploadLb.setOnClickListener { openGalleryWithPermissionRequest() } uploadLb.setOnClickListener { openGalleryWithPermissionRequest() }

View File

@@ -12,8 +12,6 @@ import h_mal.appttude.com.driver.viewmodels.MotViewModel
class MotFragment : DataSubmissionBaseFragment<MotViewModel, FragmentMotBinding, Mot>() { class MotFragment : DataSubmissionBaseFragment<MotViewModel, FragmentMotBinding, Mot>() {
override var model = Mot()
override fun setupView(binding: FragmentMotBinding) = binding.run { override fun setupView(binding: FragmentMotBinding) = binding.run {
motExpiry.apply { motExpiry.apply {
setOnClickListener { setOnClickListener {

View File

@@ -13,8 +13,6 @@ import h_mal.appttude.com.driver.viewmodels.PrivateHireVehicleViewModel
class PrivateHireVehicleFragment : class PrivateHireVehicleFragment :
DataSubmissionBaseFragment<PrivateHireVehicleViewModel, FragmentPrivateHireLicenseBinding, PrivateHireVehicle>() { DataSubmissionBaseFragment<PrivateHireVehicleViewModel, FragmentPrivateHireLicenseBinding, PrivateHireVehicle>() {
override var model = PrivateHireVehicle()
override fun setupView(binding: FragmentPrivateHireLicenseBinding) = binding.run { override fun setupView(binding: FragmentPrivateHireLicenseBinding) = binding.run {
phNo.setTextOnChange { model.phCarNumber = it } phNo.setTextOnChange { model.phCarNumber = it }
phExpiry.apply { phExpiry.apply {

View File

@@ -11,8 +11,6 @@ import h_mal.appttude.com.driver.viewmodels.VehicleProfileViewModel
class VehicleProfileFragment : DataSubmissionBaseFragment class VehicleProfileFragment : DataSubmissionBaseFragment
<VehicleProfileViewModel, FragmentVehicleSetupBinding, VehicleProfile>() { <VehicleProfileViewModel, FragmentVehicleSetupBinding, VehicleProfile>() {
override var model = VehicleProfile()
override fun setupView(binding: FragmentVehicleSetupBinding) = binding.run { override fun setupView(binding: FragmentVehicleSetupBinding) = binding.run {
reg.setTextOnChange { model.reg = it } reg.setTextOnChange { model.reg = it }
make.setTextOnChange { model.make = it } make.setTextOnChange { model.make = it }

View File

@@ -14,8 +14,8 @@
android:theme="@style/AppTheme"> android:theme="@style/AppTheme">
<activity <activity
android:name=".ui.user.LoginActivity" android:name=".ui.user.LoginActivity"
android:theme="@style/AppTheme.NoActionBar.User" android:exported="true"
android:exported="true"> android:theme="@style/AppTheme.NoActionBar.User">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
<action android:name="android.intent.action.VIEW" /> <action android:name="android.intent.action.VIEW" />

View File

@@ -1,17 +1,25 @@
package h_mal.appttude.com.driver.application package h_mal.appttude.com.driver.application
import android.app.Application import android.app.Application
import androidx.startup.AppInitializer
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 net.danlew.android.joda.JodaTimeInitializer
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.singleton import org.kodein.di.generic.singleton
const val GLOBAL_FORMAT = "dd/MM/yyyy"
open class BaseApplication : Application(), KodeinAware { open class BaseApplication : Application(), KodeinAware {
override fun onCreate() {
super.onCreate()
AppInitializer.getInstance(this).initializeComponent(JodaTimeInitializer::class.java)
}
// 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(parentModule) import(parentModule)

View File

@@ -4,7 +4,6 @@ import android.content.Intent
import android.os.Build 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
@@ -156,15 +155,18 @@ abstract class BaseActivity<V : BaseViewModel, VB : ViewBinding> : AppCompatActi
super.onToastHidden() super.onToastHidden()
mIdlingResource?.setIdleState(true) mIdlingResource?.setIdleState(true)
} }
override fun onToastShown() { override fun onToastShown() {
super.onToastShown() super.onToastShown()
mIdlingResource?.setIdleState(false) mIdlingResource?.setIdleState(false)
} }
}) })
toast.show()
} else { } else {
mIdlingResource?.setIdleState(true)
toast.show()
mIdlingResource?.setIdleState(false)
} }
toast.show()
} }
fun showSnackBar(message: String) { fun showSnackBar(message: String) {
@@ -178,6 +180,7 @@ abstract class BaseActivity<V : BaseViewModel, VB : ViewBinding> : AppCompatActi
super.onShown(transientBottomBar) super.onShown(transientBottomBar)
mIdlingResource?.setIdleState(false) mIdlingResource?.setIdleState(false)
} }
override fun onDismissed(transientBottomBar: Snackbar?, event: Int) { override fun onDismissed(transientBottomBar: Snackbar?, event: Int) {
super.onDismissed(transientBottomBar, event) super.onDismissed(transientBottomBar, event)
mIdlingResource?.setIdleState(true) mIdlingResource?.setIdleState(true)

View File

@@ -59,6 +59,7 @@ open class BaseFirebaseAdapter<T : Any, VB : ViewBinding>(
super.onDataChanged() super.onDataChanged()
if (itemCount == 0) emptyList() if (itemCount == 0) emptyList()
} }
override fun onError(error: DatabaseError) { override fun onError(error: DatabaseError) {
super.onError(error) super.onError(error)
when (error.code) { when (error.code) {

View File

@@ -1,14 +1,9 @@
package h_mal.appttude.com.driver.base package h_mal.appttude.com.driver.base
import android.content.ClipData
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Bundle import android.os.Bundle
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 androidx.activity.result.contract.ActivityResultContract
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.createViewModelLazy import androidx.fragment.app.createViewModelLazy
import androidx.viewbinding.ViewBinding import androidx.viewbinding.ViewBinding
@@ -16,7 +11,6 @@ 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.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.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
@@ -36,12 +30,6 @@ abstract class BaseFragment<V : BaseViewModel, VB : ViewBinding> : Fragment(), K
private fun getFragmentViewModel(): Lazy<V> = private fun getFragmentViewModel(): Lazy<V> =
createViewModelLazy(getGenericClassAt(0), { viewModelStore }, factoryProducer = { factory }) createViewModelLazy(getGenericClassAt(0), { viewModelStore }, factoryProducer = { factory })
private var multipleImage: Boolean = false
fun setImageSelectionAsMultiple() {
multipleImage = true
}
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, inflater: LayoutInflater,
container: ViewGroup?, container: ViewGroup?,
@@ -101,73 +89,6 @@ abstract class BaseFragment<V : BaseViewModel, VB : ViewBinding> : Fragment(), K
} }
} }
private fun ClipData.convertToList(): List<Uri> = 0.rangeTo(itemCount).map { getItemAt(it).uri }
/**
* Pair with {@link #Fragment.onRequestPermissionsResult}
* @param ourRequestCode
* @param requestCode
* checks that ourRequestCode was granted
* sends callback with
* @param permissionGranted
*/
fun onPermissionRequest(
requestCode: Int, ourRequestCode: Int, grantResults: IntArray,
permissionGranted: () -> Unit
) {
when (requestCode) {
ourRequestCode -> {
if (PermissionsUtils.isGranted(grantResults)) {
permissionGranted.invoke()
}
return
}
}
}
/**
* Called on the result of image selection
*/
open fun onImageGalleryResult(imageUri: Uri?) {}
/**
* Called on the result of multiple image selection
*/
open fun onImageGalleryResult(imageUris: List<Uri>?) {}
fun openGalleryForImage() {
permissionRequest.launch(multipleImage)
}
private val permissionRequest = registerForActivityResult(getResultsContract()) { result ->
@Suppress("UNCHECKED_CAST")
when (result) {
is Uri -> onImageGalleryResult(result)
is List<*> -> onImageGalleryResult(result as List<Uri>)
}
}
private fun getResultsContract(): ActivityResultContract<Boolean, Any?> {
return object : ActivityResultContract<Boolean, Any?>() {
override fun createIntent(context: Context, input: Boolean): Intent {
return Intent(Intent.ACTION_GET_CONTENT)
.addCategory(Intent.CATEGORY_OPENABLE)
.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, input)
.setType("image/*")
}
override fun parseResult(resultCode: Int, intent: Intent?): Any? {
intent?.clipData?.takeIf { it.itemCount > 1 }?.convertToList()?.let { clip ->
val list = clip.takeIf { it.size > 10 }?.let {
clip.subList(0, 9)
} ?: clip
return list
}
return intent?.data
}
}
}
fun showToast(message: String) = (activity as BaseActivity<*, *>).showToast(message) fun showToast(message: String) = (activity as BaseActivity<*, *>).showToast(message)
fun showSnackBar(message: String) = (activity as BaseActivity<*, *>).showSnackBar(message) fun showSnackBar(message: String) = (activity as BaseActivity<*, *>).showSnackBar(message)
} }

View File

@@ -1,26 +1,22 @@
package h_mal.appttude.com.driver.base package h_mal.appttude.com.driver.base
import android.Manifest
import android.content.Intent import android.content.Intent
import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.view.View import android.view.View
import android.widget.EditText import android.widget.EditText
import androidx.core.widget.doAfterTextChanged import androidx.core.widget.doAfterTextChanged
import androidx.viewbinding.ViewBinding import androidx.viewbinding.ViewBinding
import h_mal.appttude.com.driver.data.UserAuthState import h_mal.appttude.com.driver.data.UserAuthState
import h_mal.appttude.com.driver.model.Model
import h_mal.appttude.com.driver.ui.user.LoginActivity import h_mal.appttude.com.driver.ui.user.LoginActivity
import h_mal.appttude.com.driver.utils.PermissionsUtils.askForPermissions import h_mal.appttude.com.driver.utils.GenericsHelper.getGenericClassAt
import h_mal.appttude.com.driver.utils.TextValidationUtils.validateEditText import h_mal.appttude.com.driver.utils.TextValidationUtils.validateEditText
import kotlin.reflect.full.createInstance
private const val IMAGE_PERMISSION_RESULT = 402 abstract class DataSubmissionBaseFragment<V : DataSubmissionBaseViewModel<T>, VB : ViewBinding, T : Model> :
ImageSelectorFragment<V, VB>() {
abstract class DataSubmissionBaseFragment<V : DataSubmissionBaseViewModel<T>, VB : ViewBinding, T : Any> : var model: T = getGenericClassAt<T>(2).createInstance()
BaseFragment<V, VB>() {
var picUri: Uri? = null
abstract var model: T
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
@@ -52,17 +48,7 @@ abstract class DataSubmissionBaseFragment<V : DataSubmissionBaseViewModel<T>, VB
open fun submit() {} open fun submit() {}
fun openGalleryWithPermissionRequest() { fun openGalleryWithPermissionRequest() {
if (askForPermissions(Manifest.permission.READ_EXTERNAL_STORAGE, IMAGE_PERMISSION_RESULT)) { showStorageWithPermissionCheck()
openGalleryForImage()
}
}
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<String>,
grantResults: IntArray
) = onPermissionRequest(requestCode, IMAGE_PERMISSION_RESULT, grantResults) {
openGalleryForImage()
} }
fun validateEditTexts(vararg editTexts: EditText): Boolean { fun validateEditTexts(vararg editTexts: EditText): Boolean {
@@ -81,8 +67,4 @@ abstract class DataSubmissionBaseFragment<V : DataSubmissionBaseViewModel<T>, VB
} }
} }
override fun onImageGalleryResult(imageUri: Uri?) {
super.onImageGalleryResult(imageUri)
picUri = imageUri
}
} }

View File

@@ -25,7 +25,7 @@ abstract class DrawerActivity<V : BaseViewModel, VB : ViewBinding> : BaseActivit
abstract val containerId: Int abstract val containerId: Int
abstract val drawerLayoutId: Int abstract val drawerLayoutId: Int
abstract val toolbarId: Int abstract val toolbarId: Int
abstract val navViewId:Int abstract val navViewId: Int
lateinit var navController: NavController lateinit var navController: NavController
lateinit var appBarConfiguration: AppBarConfiguration lateinit var appBarConfiguration: AppBarConfiguration

View File

@@ -0,0 +1,107 @@
package h_mal.appttude.com.driver.base
import android.Manifest
import android.content.ClipData
import android.content.Context
import android.content.Intent
import android.net.Uri
import androidx.activity.result.contract.ActivityResultContract
import androidx.viewbinding.ViewBinding
import h_mal.appttude.com.driver.ui.permission.PermissionsDeclarationDialog
import permissions.dispatcher.NeedsPermission
import permissions.dispatcher.OnNeverAskAgain
import permissions.dispatcher.OnPermissionDenied
import permissions.dispatcher.OnShowRationale
import permissions.dispatcher.PermissionRequest
import permissions.dispatcher.RuntimePermissions
@RuntimePermissions
open class ImageSelectorFragment<V : BaseViewModel, VB : ViewBinding> : BaseFragment<V, VB>() {
private var multipleImage: Boolean = false
var picUri: Uri? = null
fun setImageSelectionAsMultiple() {
multipleImage = true
}
fun openGalleryForImage() {
permissionRequest.launch(multipleImage)
}
private val permissionRequest = registerForActivityResult(getResultsContract()) { result ->
@Suppress("UNCHECKED_CAST")
when (result) {
is Uri -> onImageGalleryResult(result)
is List<*> -> onImageGalleryResult(result as List<Uri>)
}
}
private fun getResultsContract(): ActivityResultContract<Boolean, Any?> {
return object : ActivityResultContract<Boolean, Any?>() {
override fun createIntent(context: Context, input: Boolean): Intent {
return Intent(Intent.ACTION_GET_CONTENT)
.addCategory(Intent.CATEGORY_OPENABLE)
.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, input)
.setType("image/*")
}
override fun parseResult(resultCode: Int, intent: Intent?): Any? {
intent?.clipData?.takeIf { it.itemCount > 1 }?.convertToList()?.let { clip ->
val list = clip.takeIf { it.size > 10 }?.let {
clip.subList(0, 9)
} ?: clip
return list
}
return intent?.data
}
}
}
private fun ClipData.convertToList(): List<Uri> = 0.rangeTo(itemCount).map { getItemAt(it).uri }
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<String>,
grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
// NOTE: delegate the permission handling to generated method
onRequestPermissionsResult(requestCode, grantResults)
}
@NeedsPermission(Manifest.permission.READ_EXTERNAL_STORAGE)
fun showStorage() {
openGalleryForImage()
}
@OnShowRationale(Manifest.permission.READ_EXTERNAL_STORAGE)
fun showRationaleForStorage(request: PermissionRequest) {
PermissionsDeclarationDialog(requireContext()).showDialog({
request.proceed()
}, {
request.cancel()
})
}
@OnPermissionDenied(Manifest.permission.READ_EXTERNAL_STORAGE)
fun onStorageDenied() {
showToast("Storage permissions have been denied")
}
@OnNeverAskAgain(Manifest.permission.READ_EXTERNAL_STORAGE)
fun onStorageNeverAskAgain() {
showToast("Storage permissions have been to never ask again")
}
/**
* Called on the result of image selection
*/
open fun onImageGalleryResult(imageUri: Uri?) {
picUri = imageUri
}
/**
* Called on the result of multiple image selection
*/
open fun onImageGalleryResult(imageUris: List<Uri>?) {}
}

View File

@@ -10,7 +10,8 @@ import androidx.preference.PreferenceManager
* *
*/ */
const val SORT_OPTION = "SORT_OPTION" const val SORT_OPTION = "SORT_OPTION"
class PreferenceProvider (context: Context) {
class PreferenceProvider(context: Context) {
private val appContext = context.applicationContext private val appContext = context.applicationContext

View File

@@ -3,14 +3,11 @@ package h_mal.appttude.com.driver.dialogs
import android.app.DatePickerDialog import android.app.DatePickerDialog
import android.app.DatePickerDialog.OnDateSetListener import android.app.DatePickerDialog.OnDateSetListener
import android.icu.util.Calendar
import android.widget.EditText import android.widget.EditText
import h_mal.appttude.com.driver.R import h_mal.appttude.com.driver.R
import h_mal.appttude.com.driver.utils.DateUtils import h_mal.appttude.com.driver.utils.DateUtils
private const val DATE_FORMAT = "dd/MM/yyyy"
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
class DateDialog( class DateDialog(
private val editText: EditText, private val editText: EditText,
@@ -19,10 +16,7 @@ class DateDialog(
private val dateSetListener: OnDateSetListener = private val dateSetListener: OnDateSetListener =
OnDateSetListener { _, year, month, dayOfMonth -> OnDateSetListener { _, year, month, dayOfMonth ->
val cal = Calendar.getInstance() val date = DateUtils.getDateString(year, month, dayOfMonth)
cal.set(year, month + 1, dayOfMonth)
val date = DateUtils.parseCalenderIntoDateString(cal, DATE_FORMAT)
dateSelected(date) dateSelected(date)
editText.setText(date) editText.setText(date)
editText.error = null editText.error = null
@@ -33,27 +27,17 @@ class DateDialog(
spinnersShown = true spinnersShown = true
calendarViewShown = false calendarViewShown = false
} }
val dateString = editText.text?.toString() val dateString = editText.text.toString()
val date = if (dateString.isNullOrBlank()) { val date = DateUtils.parseDateStringIntoCalender(dateString)
// Set time to now
Calendar.getInstance()
} else {
// Parse current edit text string and set value
DateUtils.parseDateStringIntoCalender(dateString, DATE_FORMAT)
?: Calendar.getInstance()
}
setDateFromCalender(date) setDateFromCalender(date)
setOnDateSetListener(dateSetListener) setOnDateSetListener(dateSetListener)
setTitle(context.getString(R.string.set_date)) setTitle(context.getString(R.string.set_date))
show() show()
} }
private fun setDateFromCalender(calendar: Calendar) { private fun setDateFromCalender(calendar: org.joda.time.LocalDate) {
val mYear = calendar.get(Calendar.YEAR) updateDate(calendar.year, calendar.monthOfYear, calendar.dayOfMonth)
val mMonth = calendar.get(Calendar.MONTH)
val mDay = calendar.get(Calendar.DAY_OF_MONTH)
updateDate(mYear, mMonth, mDay)
} }
} }

View File

@@ -8,4 +8,4 @@ data class DriverProfile(
var dob: String? = null, var dob: String? = null,
var ni: String? = null, var ni: String? = null,
var dateFirst: String? = null var dateFirst: String? = null
) ) : Model

View File

@@ -5,4 +5,4 @@ data class DriversLicense(
var licenseImageString: String? = null, var licenseImageString: String? = null,
var licenseNumber: String? = null, var licenseNumber: String? = null,
var licenseExpiry: String? = null var licenseExpiry: String? = null
) ) : Model

View File

@@ -4,4 +4,4 @@ data class Insurance(
var photoStrings: MutableList<String?>? = null, var photoStrings: MutableList<String?>? = null,
var insurerName: String? = null, var insurerName: String? = null,
var expiryDate: String? = null var expiryDate: String? = null
) ) : Model

View File

@@ -4,4 +4,4 @@ package h_mal.appttude.com.driver.model
data class Logbook( data class Logbook(
var photoString: String? = null, var photoString: String? = null,
var v5cnumber: String? = null var v5cnumber: String? = null
) ) : Model

View File

@@ -0,0 +1,3 @@
package h_mal.appttude.com.driver.model
interface Model

View File

@@ -4,4 +4,4 @@ package h_mal.appttude.com.driver.model
data class Mot( data class Mot(
var motImageString: String? = null, var motImageString: String? = null,
var motExpiry: String? = null var motExpiry: String? = null
) ) : Model

View File

@@ -5,4 +5,4 @@ data class PrivateHireLicense(
var phImageString: String? = null, var phImageString: String? = null,
var phNumber: String? = null, var phNumber: String? = null,
var phExpiry: String? = null var phExpiry: String? = null
) ) : Model

View File

@@ -1,8 +1,8 @@
package h_mal.appttude.com.driver.model package h_mal.appttude.com.driver.model
class PrivateHireVehicle( data class PrivateHireVehicle(
var phCarImageString: String? = null, var phCarImageString: String? = null,
var phCarNumber: String? = null, var phCarNumber: String? = null,
var phCarExpiry: String? = null var phCarExpiry: String? = null
) ) : Model

View File

@@ -11,4 +11,4 @@ data class VehicleProfile(
var keeperPostCode: String? = null, var keeperPostCode: String? = null,
var startDate: String? = null, var startDate: String? = null,
var isSeized: Boolean = false var isSeized: Boolean = false
) ) : Model

View File

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

View File

@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectDictionaryState">
<dictionary name="h_mal" />
</component>
</project>

View File

@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Kotlin2JsCompilerArguments">
<option name="sourceMapEmbedSources" />
</component>
</project>

View File

@@ -1,11 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CMakeSettings">
<configurations>
<configuration PROFILE_NAME="Debug" CONFIG_NAME="Debug" />
</configurations>
</component>
<component name="ProjectRootManager">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>

View File

@@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/ui.iml" filepath="$PROJECT_DIR$/ui.iml" />
</modules>
</component>
</project>

View File

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

View File

@@ -0,0 +1,18 @@
package h_mal.appttude.com.driver.ui.permission
import android.content.Context
import android.text.Html
import androidx.annotation.StringRes
interface DeclarationBuilder {
val link: String
val message: String
fun Context.readFromResources(@StringRes id: Int) = resources.getString(id)
fun buildMessage(): CharSequence? {
val link1 = "<font color='blue'><a href=\"$link\">here</a></font>"
val message = "$message See my privacy policy: $link1"
return Html.fromHtml(message, Html.FROM_HTML_MODE_LEGACY)
}
}

View File

@@ -0,0 +1,46 @@
package h_mal.appttude.com.driver.ui.permission
import android.content.Context
import android.text.method.LinkMovementMethod
import android.view.View
import android.widget.TextView
import androidx.appcompat.app.AlertDialog
class PermissionsDeclarationDialog(context: Context) : BaseDeclarationDialog(context) {
override val link: String = "https://sites.google.com/view/hmaldev/home/choice-cars"
override val message: String =
"Storage is required to access images on the devices"
}
abstract class BaseDeclarationDialog(val context: Context) : DeclarationBuilder {
abstract override val link: String
abstract override val message: String
lateinit var dialog: AlertDialog
fun showDialog(agreeCallback: () -> Unit = { }, disagreeCallback: () -> Unit = { }) {
val myMessage = buildMessage()
val builder = AlertDialog.Builder(context)
.setPositiveButton("agree") { _, _ ->
agreeCallback()
}
.setNegativeButton("disagree") { _, _ ->
disagreeCallback()
}
.setMessage(myMessage)
.setCancelable(false)
dialog = builder.create()
dialog.show()
// Make the textview clickable. Must be called after show()
val msgTxt = dialog.findViewById<View>(android.R.id.message) as TextView?
msgTxt?.movementMethod = LinkMovementMethod.getInstance()
}
fun dismiss() = dialog.dismiss()
}

View File

@@ -1,7 +1,8 @@
package h_mal.appttude.com.driver.ui.update 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.Changed
import h_mal.appttude.com.driver.data.FirebaseCompletion.ProfileDeleted
import h_mal.appttude.com.driver.databinding.UpdateActivityBinding import h_mal.appttude.com.driver.databinding.UpdateActivityBinding
import h_mal.appttude.com.driver.viewmodels.UpdateUserViewModel import h_mal.appttude.com.driver.viewmodels.UpdateUserViewModel
@@ -10,7 +11,8 @@ 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 -> showToast(data.message) is Changed -> showSnackBar(data.message)
is ProfileDeleted -> showToast(data.message)
} }
} }
} }

View File

@@ -1,20 +1,18 @@
package h_mal.appttude.com.driver.ui.update package h_mal.appttude.com.driver.ui.update
import android.Manifest.permission.READ_EXTERNAL_STORAGE
import android.net.Uri import android.net.Uri
import androidx.core.widget.doAfterTextChanged import androidx.core.widget.doAfterTextChanged
import com.google.firebase.auth.FirebaseUser import com.google.firebase.auth.FirebaseUser
import h_mal.appttude.com.driver.base.BaseFragment import h_mal.appttude.com.driver.base.ImageSelectorFragment
import h_mal.appttude.com.driver.databinding.FragmentUpdateProfileBinding import h_mal.appttude.com.driver.databinding.FragmentUpdateProfileBinding
import h_mal.appttude.com.driver.utils.PermissionsUtils.askForPermissions
import h_mal.appttude.com.driver.utils.setEnterPressedListener import h_mal.appttude.com.driver.utils.setEnterPressedListener
import h_mal.appttude.com.driver.utils.setGlideImage import h_mal.appttude.com.driver.utils.setGlideImage
import h_mal.appttude.com.driver.viewmodels.UpdateUserViewModel import h_mal.appttude.com.driver.viewmodels.UpdateUserViewModel
const val TAG_CONST = "non-user" const val TAG_CONST = "non-user"
private const val IMAGE_PERMISSION_RESULT = 402
class UpdateProfileFragment : BaseFragment<UpdateUserViewModel, FragmentUpdateProfileBinding>() { class UpdateProfileFragment :
ImageSelectorFragment<UpdateUserViewModel, FragmentUpdateProfileBinding>() {
private var imageChangeListener: Boolean = false private var imageChangeListener: Boolean = false
private var nameChangeListener: Boolean = false private var nameChangeListener: Boolean = false
@@ -35,11 +33,7 @@ class UpdateProfileFragment : BaseFragment<UpdateUserViewModel, FragmentUpdatePr
setEnterPressedListener { submitProfileUpdate() } setEnterPressedListener { submitProfileUpdate() }
} }
profileImg.setOnClickListener { profileImg.setOnClickListener { openGalleryForImage() }
if (askForPermissions(READ_EXTERNAL_STORAGE, IMAGE_PERMISSION_RESULT)) {
openGalleryForImage()
}
}
submit.setOnClickListener { submitProfileUpdate() } submit.setOnClickListener { submitProfileUpdate() }
} }
@@ -53,15 +47,6 @@ class UpdateProfileFragment : BaseFragment<UpdateUserViewModel, FragmentUpdatePr
} }
} }
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray
) = onPermissionRequest(requestCode, IMAGE_PERMISSION_RESULT, grantResults) {
openGalleryForImage()
}
override fun onSuccess(data: Any?) { override fun onSuccess(data: Any?) {
super.onSuccess(data) super.onSuccess(data)
if (data is FirebaseUser) setFields(data) if (data is FirebaseUser) setFields(data)

View File

@@ -1,10 +1,13 @@
package h_mal.appttude.com.driver.utils package h_mal.appttude.com.driver.utils
import android.icu.util.Calendar import h_mal.appttude.com.driver.application.GLOBAL_FORMAT
import org.joda.time.LocalDate
import org.joda.time.format.DateTimeFormat
import java.text.ParseException import java.text.ParseException
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.* import java.util.*
object DateUtils { object DateUtils {
fun getDateTimeStamp(): String { fun getDateTimeStamp(): String {
@@ -27,26 +30,19 @@ object DateUtils {
private fun getSimpleDateFormat(format: String) = SimpleDateFormat(format, Locale.getDefault()) private fun getSimpleDateFormat(format: String) = SimpleDateFormat(format, Locale.getDefault())
fun parseDateStringIntoCalender(dateString: String, format: String): Calendar? { fun parseDateStringIntoCalender(dateString: String, format: String = GLOBAL_FORMAT): LocalDate {
val dateFormat = getSimpleDateFormat(format) if (dateString.isBlank()) {
val calendar = Calendar.getInstance() return LocalDate.now()
return try {
calendar.time = dateFormat.parse(dateString)
calendar
} catch (e: Exception) {
null
} }
val dtf = DateTimeFormat.forPattern(format)
return dtf.parseLocalDate(dateString)
} }
fun parseCalenderIntoDateString(calendar: Calendar, format: String): String? { fun getDateString(year: Int, month: Int, dayOfMonth: Int): String {
val date = calendar.time val date = LocalDate.now()
val dateFormat = getSimpleDateFormat(format) .withYear(year)
.withMonthOfYear(month + 1)
return try { .withDayOfMonth(dayOfMonth)
dateFormat.format(date) return date.toString(GLOBAL_FORMAT)
} catch (e: ParseException) {
e.printStackTrace()
null
}
} }
} }

View File

@@ -5,7 +5,7 @@ package h_mal.appttude.com.driver.utils
* *
* @sample #boolean.isTrue{ #Do something when its true } * @sample #boolean.isTrue{ #Do something when its true }
*/ */
inline fun Boolean.isTrue(block: () -> Unit){ inline fun Boolean.isTrue(block: () -> Unit) {
if (this) { if (this) {
block() block()
} }
@@ -16,7 +16,7 @@ inline fun Boolean.isTrue(block: () -> Unit){
* *
* @sample #nullable.isNotNull{i -> i.doSomethingSinceItsNotNull() } * @sample #nullable.isNotNull{i -> i.doSomethingSinceItsNotNull() }
*/ */
inline fun <T, R> T?.isNotNull(block: (T) -> R): R?{ inline fun <T, R> T?.isNotNull(block: (T) -> R): R? {
return if (this != null) { return if (this != null) {
block(this) block(this)
} else { } else {

View File

@@ -15,31 +15,43 @@ class FirebaseException(
enum class Status(private val code: Int) { enum class Status(private val code: Int) {
DATA_STALE(-1), DATA_STALE(-1),
/** The server indicated that this operation failed */ /** The server indicated that this operation failed */
OPERATION_FAILED(-2), OPERATION_FAILED(-2),
/** This client does not have permission to perform this operation */ /** This client does not have permission to perform this operation */
PERMISSION_DENIED(-3), PERMISSION_DENIED(-3),
/** The operation had to be aborted due to a network disconnect */ /** The operation had to be aborted due to a network disconnect */
DISCONNECTED(-4), DISCONNECTED(-4),
/** The supplied auth token has expired */ /** The supplied auth token has expired */
EXPIRED_TOKEN (-6), EXPIRED_TOKEN(-6),
/** /**
* The specified authentication token is invalid. This can occur when the token is malformed, * 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. * expired, or the secret that was used to generate it has been revoked.
*/ */
INVALID_TOKEN(-7), INVALID_TOKEN(-7),
/** The transaction had too many retries */ /** The transaction had too many retries */
MAX_RETRIES(-8), MAX_RETRIES(-8),
/** The transaction was overridden by a subsequent set */ /** The transaction was overridden by a subsequent set */
OVERRIDDEN_BY_SET(-9), OVERRIDDEN_BY_SET(-9),
/** The service is unavailable */ /** The service is unavailable */
UNAVAILABLE(-10), UNAVAILABLE(-10),
/** An exception occurred in user code */ /** An exception occurred in user code */
USER_CODE_EXCEPTION(-11), USER_CODE_EXCEPTION(-11),
/** The operation could not be performed due to a network error. */ /** The operation could not be performed due to a network error. */
NETWORK_ERROR(-24), NETWORK_ERROR(-24),
/** The write was canceled locally */ /** The write was canceled locally */
WRITE_CANCELED(-25), WRITE_CANCELED(-25),
/** /**
* An unknown error occurred. Please refer to the error message and error details for more * An unknown error occurred. Please refer to the error message and error details for more
* information. * information.

View File

@@ -37,6 +37,7 @@ suspend inline fun <reified T : Any> DatabaseReference.getDataFromDatabaseRef():
is EventResponse.Changed -> { is EventResponse.Changed -> {
response.snapshot.getValue(T::class.java) response.snapshot.getValue(T::class.java)
} }
is EventResponse.Cancelled -> { is EventResponse.Cancelled -> {
throw FirebaseException(response.error) throw FirebaseException(response.error)
} }
@@ -53,17 +54,19 @@ suspend inline fun <reified T : Any> DatabaseReference.getListDataFromDatabaseRe
is EventResponse.Changed -> { is EventResponse.Changed -> {
response.snapshot.children.map { it.getValue(T::class.java) } response.snapshot.children.map { it.getValue(T::class.java) }
} }
is EventResponse.Cancelled -> { is EventResponse.Cancelled -> {
throw FirebaseException(response.error) throw FirebaseException(response.error)
} }
} }
} }
suspend fun <T: Any> DatabaseReference.getDataFromDatabaseRef(clazz : Class<T>): T? { suspend fun <T : Any> DatabaseReference.getDataFromDatabaseRef(clazz: Class<T>): T? {
return when (val response: EventResponse = singleValueEvent()) { return when (val response: EventResponse = singleValueEvent()) {
is EventResponse.Changed -> { is EventResponse.Changed -> {
response.snapshot.getValue(clazz) response.snapshot.getValue(clazz)
} }
is EventResponse.Cancelled -> { is EventResponse.Cancelled -> {
throw FirebaseException(response.error) throw FirebaseException(response.error)
} }

View File

@@ -19,7 +19,7 @@ object GenericsHelper {
* *
* @sample inflateBindingByType(getGenericClassAt(0), layoutInflater) * @sample inflateBindingByType(getGenericClassAt(0), layoutInflater)
*/ */
fun <VB: ViewBinding> inflateBindingByType( fun <VB : ViewBinding> inflateBindingByType(
genericClassAt: KClass<VB>, genericClassAt: KClass<VB>,
layoutInflater: LayoutInflater layoutInflater: LayoutInflater
): VB = try { ): VB = try {
@@ -30,12 +30,12 @@ object GenericsHelper {
&& viewBinding.parameterTypes.getOrNull(0) == LayoutInflater::class.java && viewBinding.parameterTypes.getOrNull(0) == LayoutInflater::class.java
}.invoke(null, layoutInflater) as VB }.invoke(null, layoutInflater) as VB
} catch (exception: Exception) { } catch (exception: Exception) {
println ("generic class failed at = $genericClassAt") println("generic class failed at = $genericClassAt")
exception.printStackTrace() exception.printStackTrace()
throw IllegalStateException("Can not inflate binding from generic") throw IllegalStateException("Can not inflate binding from generic")
} }
fun <VB: ViewBinding> LayoutInflater.inflateBindingByType( fun <VB : ViewBinding> LayoutInflater.inflateBindingByType(
container: ViewGroup?, container: ViewGroup?,
genericClassAt: KClass<VB> genericClassAt: KClass<VB>
): VB = try { ): VB = try {

View File

@@ -9,7 +9,6 @@ import android.provider.Settings
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.core.app.ActivityCompat import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment
object PermissionsUtils { object PermissionsUtils {
@@ -39,12 +38,6 @@ object PermissionsUtils {
return true return true
} }
fun Fragment.askForPermissions(permission: String, requestCode: Int): Boolean =
requireActivity().askForPermissions(permission, requestCode)
fun isGranted(grantResults: IntArray): Boolean =
grantResults.getOrNull(0)?.equals(PERMISSION_GRANTED) ?: false
private fun Context.showPermissionDeniedDialog() { private fun Context.showPermissionDeniedDialog() {
AlertDialog.Builder(this) AlertDialog.Builder(this)
.setTitle("Permission Denied") .setTitle("Permission Denied")

View File

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

View File

@@ -1,5 +1,10 @@
<vector android:height="24dp" android:tint="#FFFFFF" <vector xmlns:android="http://schemas.android.com/apk/res/android"
android:viewportHeight="24" android:viewportWidth="24" android:width="24dp"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android"> android:height="24dp"
<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"/> android:tint="#FFFFFF"
android:viewportWidth="24"
android:viewportHeight="24">
<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> </vector>

View File

@@ -1,5 +1,10 @@
<vector android:height="72dp" android:tint="#FFFFFF" <vector xmlns:android="http://schemas.android.com/apk/res/android"
android:viewportHeight="24" android:viewportWidth="24" android:width="72dp"
android:width="72dp" xmlns:android="http://schemas.android.com/apk/res/android"> android:height="72dp"
<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"/> android:tint="#FFFFFF"
android:viewportWidth="24"
android:viewportHeight="24">
<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> </vector>

View File

@@ -1,9 +1,9 @@
<vector android:height="24dp" <vector xmlns:android="http://schemas.android.com/apk/res/android"
android:tint="#FFFFFF"
android:viewportHeight="24"
android:viewportWidth="24"
android:width="24dp" android:width="24dp"
xmlns:android="http://schemas.android.com/apk/res/android"> android:height="24dp"
android:tint="#FFFFFF"
android:viewportWidth="24"
android:viewportHeight="24">
<path <path
android:fillColor="@android:color/white" android:fillColor="@android:color/white"
android:pathData="M12,4l-1.41,1.41L16.17,11H4v2h12.17l-5.58,5.59L12,20l8,-8z" /> android:pathData="M12,4l-1.41,1.41L16.17,11H4v2h12.17l-5.58,5.59L12,20l8,-8z" />

View File

@@ -1,9 +1,9 @@
<vector android:height="24dp" <vector xmlns:android="http://schemas.android.com/apk/res/android"
android:tint="#FFFFFF"
android:viewportHeight="24"
android:viewportWidth="24"
android:width="24dp" android:width="24dp"
xmlns:android="http://schemas.android.com/apk/res/android"> android:height="24dp"
android:tint="#FFFFFF"
android:viewportWidth="24"
android:viewportHeight="24">
<path <path
android:fillColor="@android:color/white" android:fillColor="@android:color/white"
android:pathData="M19,3h-4.18C14.4,1.84 13.3,1 12,1c-1.3,0 -2.4,0.84 -2.82,2L5,3c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2L21,5c0,-1.1 -0.9,-2 -2,-2zM12,3c0.55,0 1,0.45 1,1s-0.45,1 -1,1 -1,-0.45 -1,-1 0.45,-1 1,-1zM12,7c1.66,0 3,1.34 3,3s-1.34,3 -3,3 -3,-1.34 -3,-3 1.34,-3 3,-3zM18,19L6,19v-1.4c0,-2 4,-3.1 6,-3.1s6,1.1 6,3.1L18,19z" /> android:pathData="M19,3h-4.18C14.4,1.84 13.3,1 12,1c-1.3,0 -2.4,0.84 -2.82,2L5,3c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2L21,5c0,-1.1 -0.9,-2 -2,-2zM12,3c0.55,0 1,0.45 1,1s-0.45,1 -1,1 -1,-0.45 -1,-1 0.45,-1 1,-1zM12,7c1.66,0 3,1.34 3,3s-1.34,3 -3,3 -3,-1.34 -3,-3 1.34,-3 3,-3zM18,19L6,19v-1.4c0,-2 4,-3.1 6,-3.1s6,1.1 6,3.1L18,19z" />

View File

@@ -1,9 +1,9 @@
<vector android:height="24dp" <vector xmlns:android="http://schemas.android.com/apk/res/android"
android:tint="#FFFFFF"
android:viewportHeight="24"
android:viewportWidth="24"
android:width="24dp" android:width="24dp"
xmlns:android="http://schemas.android.com/apk/res/android"> android:height="24dp"
android:tint="#FFFFFF"
android:viewportWidth="24"
android:viewportHeight="24">
<path <path
android:fillColor="@android:color/white" android:fillColor="@android:color/white"
android:pathData="M22,16L22,4c0,-1.1 -0.9,-2 -2,-2L8,2c-1.1,0 -2,0.9 -2,2v12c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2zM11,12l2.03,2.71L16,11l4,5L8,16l3,-4zM2,6v14c0,1.1 0.9,2 2,2h14v-2L4,20L4,6L2,6z" /> android:pathData="M22,16L22,4c0,-1.1 -0.9,-2 -2,-2L8,2c-1.1,0 -2,0.9 -2,2v12c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2zM11,12l2.03,2.71L16,11l4,5L8,16l3,-4zM2,6v14c0,1.1 0.9,2 2,2h14v-2L4,20L4,6L2,6z" />

View File

@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
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:background="@drawable/background_with_curve" android:background="@drawable/background_with_curve"

View File

@@ -11,9 +11,9 @@
android:id="@+id/appbar" android:id="@+id/appbar"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:elevation="0dp"
android:background="@android:color/transparent" android:background="@android:color/transparent"
android:theme="@style/AppTheme.AppBarOverlay"> android:theme="@style/AppTheme.AppBarOverlay"
app:elevation="0dp">
<androidx.appcompat.widget.Toolbar <androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar" android:id="@+id/toolbar"

View File

@@ -37,9 +37,9 @@
<EditText <EditText
android:id="@+id/email_update" android:id="@+id/email_update"
style="@style/EditTextStyle" style="@style/EditTextStyle"
android:autofillHints="emailAddress"
android:hint="@string/prompt_email" android:hint="@string/prompt_email"
android:inputType="textEmailAddress" android:inputType="textEmailAddress" />
android:autofillHints="emailAddress" />
</com.google.android.material.textfield.TextInputLayout> </com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout <com.google.android.material.textfield.TextInputLayout
@@ -52,9 +52,9 @@
<EditText <EditText
android:id="@+id/password_top" android:id="@+id/password_top"
style="@style/EditTextStyle" style="@style/EditTextStyle"
android:autofillHints="password"
android:hint="@string/prompt_password" android:hint="@string/prompt_password"
android:inputType="textPassword" android:inputType="textPassword" />
android:autofillHints="password" />
</com.google.android.material.textfield.TextInputLayout> </com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.button.MaterialButton <com.google.android.material.button.MaterialButton

View File

@@ -11,9 +11,9 @@
android:layout_marginBottom="12dp" android:layout_marginBottom="12dp"
app:cardCornerRadius="28dp" app:cardCornerRadius="28dp"
app:cardElevation="0dp" app:cardElevation="0dp"
app:layout_constraintBottom_toTopOf="@id/til_lic_no"
app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent" app:layout_constraintRight_toRightOf="parent">
app:layout_constraintBottom_toTopOf="@id/til_lic_no">
<ImageView <ImageView
android:id="@+id/driversli_img" android:id="@+id/driversli_img"
@@ -21,23 +21,22 @@
android:layout_height="200dp" android:layout_height="200dp"
android:layout_alignParentStart="true" android:layout_alignParentStart="true"
android:adjustViewBounds="true" android:adjustViewBounds="true"
android:scaleType="centerCrop" android:contentDescription="@string/image_description"
tools:src="@drawable/choice_img_round" android:scaleType="centerCrop" />
android:contentDescription="@string/image_description" />
<com.mikhaellopez.circularimageview.CircularImageView <com.mikhaellopez.circularimageview.CircularImageView
android:id="@+id/search_image" android:id="@+id/search_image"
android:layout_width="40dp" android:layout_width="40dp"
android:layout_height="40dp" android:layout_height="40dp"
android:src="@drawable/ic_baseline_photo_library_24" android:layout_gravity="bottom|end"
android:scaleType="centerInside"
android:elevation="1dp"
app:civ_border_width="1dp"
app:civ_shadow_radius="0.5dp"
android:alpha="1"
app:civ_circle_color="@color/colour_one"
android:layout_margin="18dp" android:layout_margin="18dp"
android:layout_gravity="bottom|end" /> android:alpha="1"
android:elevation="1dp"
android:scaleType="centerInside"
android:src="@drawable/ic_baseline_photo_library_24"
app:civ_border_width="1dp"
app:civ_circle_color="@color/colour_one"
app:civ_shadow_radius="0.5dp" />
</androidx.cardview.widget.CardView> </androidx.cardview.widget.CardView>
@@ -53,24 +52,24 @@
android:id="@+id/lic_no" android:id="@+id/lic_no"
style="@style/EditTextStyle.Date" style="@style/EditTextStyle.Date"
android:hint="@string/drivers_license_no" android:hint="@string/drivers_license_no"
android:importantForAutofill="no"
android:inputType="none" android:inputType="none"
android:maxLines="1" android:maxLines="1" />
android:importantForAutofill="no" />
</com.google.android.material.textfield.TextInputLayout> </com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout <com.google.android.material.textfield.TextInputLayout
android:id="@+id/til_submission" android:id="@+id/til_submission"
style="@style/text_input_layout" style="@style/text_input_layout"
app:layout_constraintTop_toBottomOf="@id/til_lic_no"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"> app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/til_lic_no">
<EditText <EditText
android:hint="@string/license_expiry_date"
android:id="@+id/lic_expiry" android:id="@+id/lic_expiry"
style="@style/EditTextStyle.Date" style="@style/EditTextStyle.Date"
android:autofillHints="date" /> android:autofillHints="date"
android:hint="@string/license_expiry_date" />
</com.google.android.material.textfield.TextInputLayout> </com.google.android.material.textfield.TextInputLayout>
@@ -78,10 +77,10 @@
android:id="@+id/submit" android:id="@+id/submit"
style="@style/TextButton.WithIcon" style="@style/TextButton.WithIcon"
android:text="@string/submit" android:text="@string/submit"
app:layout_constraintTop_toBottomOf="@+id/til_submission"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/til_submission"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.8" /> app:layout_constraintVertical_bias="0.8" />

View File

@@ -2,13 +2,19 @@
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android" <ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
style="@style/parent_constraint_layout" android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginEnd="@dimen/activity_vertical_margin"
android:layout_marginStart="@dimen/activity_vertical_margin"
android:layout_marginRight="@dimen/activity_horizontal_margin"
android:layout_marginLeft="@dimen/activity_horizontal_margin"
android:orientation="vertical" android:orientation="vertical"
tools:context=".ui.driverprofile.DriverProfileFragment"> tools:context=".ui.driverprofile.DriverProfileFragment">
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center"
android:orientation="vertical"> android:orientation="vertical">
<androidx.constraintlayout.widget.ConstraintLayout <androidx.constraintlayout.widget.ConstraintLayout
@@ -46,18 +52,17 @@
tools:src="@drawable/choice_img_round" /> tools:src="@drawable/choice_img_round" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>
<com.google.android.material.textfield.TextInputLayout <com.google.android.material.textfield.TextInputLayout style="@style/text_input_layout">
style="@style/text_input_layout">
<EditText <EditText
android:id="@+id/names_input" android:id="@+id/names_input"
style="@style/EditTextStyle" style="@style/EditTextStyle"
android:layout_marginBottom="12dp" android:layout_marginBottom="12dp"
android:autofillHints="name"
android:ems="10" android:ems="10"
android:hint="@string/full_name" android:hint="@string/full_name"
android:inputType="textPersonName" android:inputType="textPersonName"
android:maxLines="1" android:maxLines="1" />
android:autofillHints="name" />
</com.google.android.material.textfield.TextInputLayout> </com.google.android.material.textfield.TextInputLayout>
@@ -67,6 +72,7 @@
android:id="@+id/address_input" android:id="@+id/address_input"
style="@style/EditTextStyle" style="@style/EditTextStyle"
android:layout_marginBottom="12dp" android:layout_marginBottom="12dp"
android:autofillHints="postalAddress"
android:ems="10" android:ems="10"
android:gravity="top|start" android:gravity="top|start"
android:hint="@string/address" android:hint="@string/address"
@@ -75,8 +81,7 @@
android:maxLines="7" android:maxLines="7"
android:minLines="4" android:minLines="4"
android:selectAllOnFocus="true" android:selectAllOnFocus="true"
android:singleLine="true" android:singleLine="true" />
android:autofillHints="postalAddress" />
</com.google.android.material.textfield.TextInputLayout> </com.google.android.material.textfield.TextInputLayout>
@@ -86,12 +91,12 @@
android:id="@+id/postcode_input" android:id="@+id/postcode_input"
style="@style/EditTextStyle" style="@style/EditTextStyle"
android:layout_marginBottom="12dp" android:layout_marginBottom="12dp"
android:autofillHints="postalCode"
android:hint="@string/postcode" android:hint="@string/postcode"
android:inputType="none" android:inputType="none"
android:maxLines="1" android:maxLines="1"
android:selectAllOnFocus="true" android:selectAllOnFocus="true"
android:singleLine="true" android:singleLine="true" />
android:autofillHints="postalCode" />
</com.google.android.material.textfield.TextInputLayout> </com.google.android.material.textfield.TextInputLayout>
@@ -101,8 +106,8 @@
android:id="@+id/dob_input" android:id="@+id/dob_input"
style="@style/EditTextStyle.Date" style="@style/EditTextStyle.Date"
android:layout_marginBottom="12dp" android:layout_marginBottom="12dp"
android:hint="@string/date_of_birth" android:autofillHints="date"
android:autofillHints="date" /> android:hint="@string/date_of_birth" />
</com.google.android.material.textfield.TextInputLayout> </com.google.android.material.textfield.TextInputLayout>
@@ -113,9 +118,9 @@
style="@style/EditTextStyle" style="@style/EditTextStyle"
android:layout_marginBottom="12dp" android:layout_marginBottom="12dp"
android:hint="@string/ni_number" android:hint="@string/ni_number"
android:maxLines="1"
android:importantForAutofill="no" android:importantForAutofill="no"
android:inputType="none" android:inputType="none"
android:maxLines="1"
tools:ignore="TextFields" /> tools:ignore="TextFields" />
</com.google.android.material.textfield.TextInputLayout> </com.google.android.material.textfield.TextInputLayout>
@@ -126,8 +131,8 @@
android:id="@+id/date_first" android:id="@+id/date_first"
style="@style/EditTextStyle.Date" style="@style/EditTextStyle.Date"
android:layout_marginBottom="12dp" android:layout_marginBottom="12dp"
android:hint="@string/date_first_available" android:autofillHints="date"
android:autofillHints="date" /> android:hint="@string/date_first_available" />
</com.google.android.material.textfield.TextInputLayout> </com.google.android.material.textfield.TextInputLayout>

View File

@@ -37,9 +37,9 @@
<EditText <EditText
android:id="@+id/submission_et" android:id="@+id/submission_et"
style="@style/EditTextStyle" style="@style/EditTextStyle"
android:autofillHints="emailAddress"
android:hint="@string/prompt_email" android:hint="@string/prompt_email"
android:inputType="textEmailAddress" android:inputType="textEmailAddress" />
android:autofillHints="emailAddress" />
</com.google.android.material.textfield.TextInputLayout> </com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.button.MaterialButton <com.google.android.material.button.MaterialButton

View File

@@ -22,7 +22,7 @@
android:layout_marginTop="16dp" android:layout_marginTop="16dp"
android:layout_marginEnd="16dp" android:layout_marginEnd="16dp"
android:layout_marginBottom="16dp" android:layout_marginBottom="16dp"
android:src="@drawable/ic_file_download_black_24dp" android:contentDescription="@string/floating_action_button"
android:contentDescription="@string/floating_action_button" /> android:src="@drawable/ic_file_download_black_24dp" />
</RelativeLayout> </RelativeLayout>

View File

@@ -10,12 +10,12 @@
android:id="@+id/carouselView" android:id="@+id/carouselView"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="200dp" android:layout_height="200dp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintBottom_toTopOf="@id/uploadInsurance"
android:layout_marginBottom="12dp" android:layout_marginBottom="12dp"
app:autoPlay="false" app:autoPlay="false"
app:fillColor="#FFFFFFFF" app:fillColor="#FFFFFFFF"
app:layout_constraintBottom_toTopOf="@id/uploadInsurance"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:pageColor="#00000000" app:pageColor="#00000000"
app:radius="6dp" app:radius="6dp"
app:strokeColor="#FF777777" app:strokeColor="#FF777777"
@@ -41,11 +41,11 @@
android:id="@+id/insurer" android:id="@+id/insurer"
style="@style/EditTextStyle" style="@style/EditTextStyle"
android:hint="@string/insurer" android:hint="@string/insurer"
android:importantForAutofill="no"
android:inputType="none" android:inputType="none"
android:maxLines="1" android:maxLines="1"
android:selectAllOnFocus="true" android:selectAllOnFocus="true"
android:singleLine="true" android:singleLine="true" />
android:importantForAutofill="no" />
</com.google.android.material.textfield.TextInputLayout> </com.google.android.material.textfield.TextInputLayout>
@@ -61,9 +61,9 @@
style="@style/EditTextStyle.Date" style="@style/EditTextStyle.Date"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:inputType="date"
android:hint="@string/insurance_expiry" android:hint="@string/insurance_expiry"
android:importantForAutofill="no" /> android:importantForAutofill="no"
android:inputType="date" />
</com.google.android.material.textfield.TextInputLayout> </com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.button.MaterialButton <com.google.android.material.button.MaterialButton

View File

@@ -1,10 +1,10 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
style="@style/parent_constraint_layout" style="@style/parent_constraint_layout"
xmlns:app="http://schemas.android.com/apk/res-auto"> android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.cardview.widget.CardView <androidx.cardview.widget.CardView
android:layout_width="wrap_content" android:layout_width="wrap_content"
@@ -13,9 +13,9 @@
android:layout_marginBottom="12dp" android:layout_marginBottom="12dp"
app:cardCornerRadius="28dp" app:cardCornerRadius="28dp"
app:cardElevation="0dp" app:cardElevation="0dp"
app:layout_constraintBottom_toTopOf="@id/upload_lb"
app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent" app:layout_constraintRight_toRightOf="parent">
app:layout_constraintBottom_toTopOf="@id/upload_lb">
<ImageView <ImageView
android:id="@+id/log_book_img" android:id="@+id/log_book_img"
@@ -23,47 +23,47 @@
android:layout_height="200dp" android:layout_height="200dp"
android:layout_alignParentStart="true" android:layout_alignParentStart="true"
android:adjustViewBounds="true" android:adjustViewBounds="true"
android:contentDescription="@string/image_description"
android:scaleType="centerCrop" android:scaleType="centerCrop"
tools:src="@drawable/choice_img_round" tools:src="@drawable/choice_img_round" />
android:contentDescription="@string/image_description" />
</androidx.cardview.widget.CardView> </androidx.cardview.widget.CardView>
<com.google.android.material.button.MaterialButton <com.google.android.material.button.MaterialButton
android:id="@+id/upload_lb" android:id="@+id/upload_lb"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintBottom_toTopOf="@id/til_v5c"
style="@style/TextButton.Rounded" style="@style/TextButton.Rounded"
android:selectAllOnFocus="true" android:selectAllOnFocus="true"
android:singleLine="true" android:singleLine="true"
android:text="@string/upload_logbook" /> android:text="@string/upload_logbook"
app:layout_constraintBottom_toTopOf="@id/til_v5c"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<com.google.android.material.textfield.TextInputLayout <com.google.android.material.textfield.TextInputLayout
android:id="@+id/til_v5c" android:id="@+id/til_v5c"
style="@style/text_input_layout" style="@style/text_input_layout"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent">
app:layout_constraintBottom_toBottomOf="parent">
<EditText <EditText
android:id="@+id/v5c_no" android:id="@+id/v5c_no"
style="@style/EditTextStyle" style="@style/EditTextStyle"
android:hint="@string/v5c_number" android:hint="@string/v5c_number"
android:inputType="none" android:importantForAutofill="no"
android:importantForAutofill="no" /> android:inputType="none" />
</com.google.android.material.textfield.TextInputLayout> </com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.button.MaterialButton <com.google.android.material.button.MaterialButton
android:id="@+id/submit" android:id="@+id/submit"
style="@style/TextButton.WithIcon" style="@style/TextButton.WithIcon"
android:text="@string/submit" android:text="@string/submit"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintVertical_bias="0.8"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/til_v5c"> app:layout_constraintTop_toBottomOf="@+id/til_v5c"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.8">
</com.google.android.material.button.MaterialButton> </com.google.android.material.button.MaterialButton>

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