<|start_filename|>app/src/main/java/org/wildaid/ofish/ui/home/HomeFragmentViewModel.kt<|end_filename|>
package org.wildaid.ofish.ui.home

import android.net.Uri
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import org.wildaid.ofish.Event
import org.wildaid.ofish.data.Repository

class HomeFragmentViewModel(val repository: Repository) : ViewModel() {

    private val _locationLiveData = MutableLiveData<Pair<Double, Double>>()
    val locationLiveData: LiveData<Pair<Double, Double>>
        get() = _locationLiveData

    private val _userEventLiveData = MutableLiveData<Event<HomeFragmentUserEvent>>()
    val userEventLiveData: LiveData<Event<HomeFragmentUserEvent>>
        get() = _userEventLiveData

    private val _amountOfDrafts = MutableLiveData<Int>()
    val amountOfDrafts: LiveData<Int>
        get() = _amountOfDrafts

    lateinit var activityViewModel: HomeActivityViewModel

    fun onLocationAvailable(latitude: Double, longitude: Double) {
        _locationLiveData.value = Pair(latitude, longitude)
    }

    fun showDrafts(){
        _userEventLiveData.value = Event(HomeFragmentUserEvent.ShowDrafts)
    }

    fun updateDraftCount() {
        _amountOfDrafts.value = repository.getAmountOfDraftsByEmail()
    }

    fun boardVessel() {
        if (activityViewModel.onDutyStatusLiveData.value == true) {
            _userEventLiveData.value = Event(HomeFragmentUserEvent.BoardVessel)
        } else {
            activityViewModel.userEventLiveData.value =
                Event(HomeActivityViewModel.HomeActivityUserEvent.AskDutyConfirmationEvent)
        }
    }

    fun findRecords() {
        _userEventLiveData.value = Event(HomeFragmentUserEvent.FindRecords)
    }

    fun showUserStatus() {
        _userEventLiveData.value = Event(HomeFragmentUserEvent.ShowUserStatus)
    }

    fun saveProfileImage(uri: Uri) {
        repository.updateCurrentOfficerPhoto(uri)
    }

    sealed class HomeFragmentUserEvent {
        object FindRecords : HomeFragmentUserEvent()
        object ShowUserStatus : HomeFragmentUserEvent()
        object BoardVessel : HomeFragmentUserEvent()
        object ShowDrafts : HomeFragmentUserEvent()
    }
}

<|start_filename|>app/src/main/java/org/wildaid/ofish/ui/tabs/TabsFragmentHost.kt<|end_filename|>
package org.wildaid.ofish.ui.tabs

import android.annotation.SuppressLint
import android.os.Bundle
import android.util.Log
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import android.widget.CheckedTextView
import android.widget.LinearLayout
import androidx.appcompat.app.AppCompatActivity
import androidx.core.os.bundleOf
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.fragment.app.viewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.Observer
import androidx.navigation.fragment.findNavController
import androidx.viewpager2.widget.ViewPager2
import com.google.android.material.tabs.TabLayout
import com.google.android.material.tabs.TabLayoutMediator
import kotlinx.android.synthetic.main.fragment_tabs.*
import org.wildaid.ofish.EventObserver
import org.wildaid.ofish.R
import org.wildaid.ofish.data.OnSaveListener
import org.wildaid.ofish.data.report.Report
import org.wildaid.ofish.ui.base.*
import org.wildaid.ofish.ui.createreport.CreateReportBundle
import org.wildaid.ofish.ui.createreport.CreateReportViewModel
import org.wildaid.ofish.ui.createreport.KEY_CREATE_REPORT_ARGS
import org.wildaid.ofish.ui.crew.CrewFragment
import org.wildaid.ofish.ui.home.KEY_CREATE_REPORT_RESULT
import org.wildaid.ofish.ui.vessel.VesselFragment
import org.wildaid.ofish.util.getViewModelFactory
import org.wildaid.ofish.util.setVisible
import org.wildaid.ofish.util.showSnackMessage

const val BASIC_INFO_FRAGMENT_POSITION = 0
const val VESSEL_FRAGMENT_POSITION = 1
const val CREW_FRAGMENT_POSITION = 2
const val ACTIVITIES_FRAGMENT_POSITION = 3
const val CATCH_FRAGMENT_POSITION = 4
const val VIOLATION_FRAGMENT_POSITION = 5
const val RISK_FRAGMENT_POSITION = 6
const val NOTES_FRAGMENT_POSITION = 7

const val FRAGMENT_TAG_PREFIX = "f"
private const val ASK_PREFILL_VESSEL_DIALOG_ID = 13
private const val ASK_SKIP_TABS_DIALOG_ID = 14
private const val SUBMIT_DIALOG_ID = 15
private const val ASK_PREFILL_CREW_DIALOG_ID = 191232

class TabsFragmentHost : Fragment(R.layout.fragment_tabs), OnNextClickedListener {
    private val fragmentViewModel by viewModels<TabsViewModel> { getViewModelFactory() }
    private val activityViewModel by activityViewModels<CreateReportViewModel> { getViewModelFactory() }
    private val navigation by lazy { findNavController() }

    private lateinit var fragmentFactory: TabsFragmentFactory
    private lateinit var tabsFragmentAdapter: TabsFragmentAdapter
    private lateinit var currentReportFragment: BaseReportFragment
    private var pendingSkippingTabs: List<TabItem>? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val arguments: CreateReportBundle? =
            requireArguments().getParcelable(KEY_CREATE_REPORT_ARGS)

        fragmentViewModel.initReport(
            activityViewModel.report,
            activityViewModel.reportPhotos,
            arguments?.prefillVessel,
            arguments?.prefillCrew
        )
        subscribeToDialogEvents()
        setHasOptionsMenu(true)
    }

    override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
        inflater.inflate(R.menu.menu_create_report, menu)
    }

    override fun onOptionsItemSelected(item: MenuItem): Boolean {
        when (item.itemId) {
            android.R.id.home -> activityViewModel.onBackPressed()
            R.id.menu_report_submit -> showSubmitReportDialog()
        }

        return super.onOptionsItemSelected(item)
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        fragmentViewModel.reportLiveData.observe(viewLifecycleOwner, Observer {
            initUI(it.first, it.second)
        })

        fragmentViewModel.userEventLiveData.observe(viewLifecycleOwner, EventObserver {
            when (it) {
                is TabsViewModel.TabsUserEvent.AskSkipSectionsEvent -> showSkipSectionsDialog(it.skippedTabs)
                is TabsViewModel.TabsUserEvent.AskLeftEmptyFields -> showSkipSectionsDialog(it.skippedTabs)
                is TabsViewModel.TabsUserEvent.ChangeTabEvent -> selectTab(it.tabItem)
                TabsViewModel.TabsUserEvent.AskPrefillVesselEvent -> showAskPrefillBoatDialog()
                TabsViewModel.TabsUserEvent.AskPrefillCrewEvent -> showAskPrefillCrewDialog()
            }
        })

        fragmentViewModel.tabsStateLiveData.observe(viewLifecycleOwner, Observer {
            updateTabsDrawable(it)
        })
    }

    override fun onNextClicked(isSubmitted: Boolean) {
        if (isSubmitted) {
            showSubmitReportDialog()
        } else {
            tabs_pager.currentItem += 1
        }
    }

    private fun selectTab(tabItem: TabItem) {
        tabs_layout.getTabAt(tabItem.position)?.select()
    }

    private fun onTabChanged(previousTabIndex: Int, currentTabIndex: Int) {
        currentReportFragment =
            childFragmentManager.findFragmentByTag("$FRAGMENT_TAG_PREFIX${currentTabIndex}") as BaseReportFragment

        val previousReportFragment =
            childFragmentManager.findFragmentByTag("f$previousTabIndex") as BaseReportFragment?

        previousReportFragment?.let {
            fragmentViewModel.onTabChanged(previousTabIndex, currentTabIndex)
        }
    }

    private fun updateTabsDrawable(tabs: List<TabItem>) {
        tabs.forEach { tabState ->
            if (tabState.status == TabStatus.NOT_VISITED) {
                tabs_layout.getTabAt(tabState.position)?.customView?.also {
                    val textSelector = resources.getColorStateList(R.color.selector_tabs_text, null)
                    it.findViewById<CheckedTextView>(android.R.id.text1)?.setTextColor(textSelector)
                    it.findViewById<View>(R.id.tab_bottom_line)?.setVisible(false)
                }
            } else {
                val visitedStatusColorRes =
                    if (tabState.status == TabStatus.SKIPPED) R.color.orange else R.color.tab_text_color

                val bgVisitedStatusColorRes =
                    if (tabState.status == TabStatus.SKIPPED) R.color.orange else R.color.blue

                val visitedStatusColor = resources.getColor(visitedStatusColorRes, null)
                val bgVisitedStatusColor = resources.getColor(bgVisitedStatusColorRes, null)

                tabs_layout.getTabAt(tabState.position)?.customView?.also { view ->
                    view.findViewById<View>(R.id.tab_bottom_line)?.setVisible(true)
                    view.findViewById<View>(R.id.tab_bottom_line)
                        ?.setBackgroundColor(bgVisitedStatusColor)
                    view.findViewById<CheckedTextView>(android.R.id.text1)
                        ?.setTextColor(visitedStatusColor)
                }
            }
        }
    }

    private fun subscribeToDialogEvents() {
        val navStack = navigation.currentBackStackEntry!!
        navStack.lifecycle.addObserver(LifecycleEventObserver { _, event ->
            if (event != Lifecycle.Event.ON_RESUME) {
                return@LifecycleEventObserver
            }

            if (navStack.savedStateHandle.contains(DIALOG_CLICK_EVENT)) {
                val click = navStack.savedStateHandle.get<DialogClickEvent>(DIALOG_CLICK_EVENT)!!
                val handled = handleDialogClick(click)
                if (handled) {
                    navStack.savedStateHandle.remove<DialogClickEvent>(DIALOG_CLICK_EVENT)!!
                }
            }
        })
    }

    private fun showSkipSectionsDialog(skippedTabs: List<TabItem>) {
        pendingSkippingTabs = skippedTabs

        val skipMessage = skippedTabs.joinToString("\n") { "- ${it.title}" }

        val dialogBundle = ConfirmationDialogFragment.Bundler(
            ASK_SKIP_TABS_DIALOG_ID,
            getString(R.string.skip_sections_dialog_title),
            skipMessage,
            getString(R.string.skip_sections_dialog_yes),
            getString(R.string.keep_editing)
        ).bundle()

        navigation.navigate(R.id.confirmation_dialog, dialogBundle)
    }

    private fun showAskPrefillBoatDialog() {
        val dialogBundle = ConfirmationDialogFragment.Bundler(
            ASK_PREFILL_VESSEL_DIALOG_ID,
            getString(R.string.prefill_vessel_dialog_title),
            getString(R.string.prefill_vessel_dialog_description),
            getString(R.string.prefill_vessel_dialog_yes),
            getString(R.string.no)
        ).bundle()

        navigation.navigate(R.id.confirmation_dialog, dialogBundle)
    }

    private fun showAskPrefillCrewDialog() {
        val dialogBundle = ConfirmationDialogFragment.Bundler(
            ASK_PREFILL_CREW_DIALOG_ID,
            getString(R.string.prefill_crew_dialog_title),
            getString(R.string.prefill_crew_dialog_description),
            getString(R.string.prefill_crew_dialog_yes),
            getString(R.string.no)
        ).bundle()

        navigation.navigate(R.id.confirmation_dialog, dialogBundle)
    }

    private fun showSubmitReportDialog() {
        val tabs = fragmentViewModel.getSkippedAndNotVisitedTabs()
        val title: String
        val message: String
        if (tabs.isEmpty()) {
            title = getString(R.string.submit_boarding)
            message = getString(R.string.sure_to_submit)
        } else {
            title = getString(R.string.you_left_blank)
            message = getString(
                R.string.sure_to_submit_with_skipped,
                tabs.joinToString("\n") { "- ${it.title}" })
        }
        val dialogBundle = ConfirmationDialogFragment.Bundler(
            SUBMIT_DIALOG_ID,
            title,
            message,
            getString(R.string.submit),
            getString(R.string.keep_editing)
        ).apply {
            neutral = getString(R.string.save_and_finish_later)
        }.bundle()

        navigation.navigate(R.id.confirmation_dialog, dialogBundle)
    }

    private fun handleDialogClick(click: DialogClickEvent): Boolean {
        return when (click.dialogId) {
            ASK_PREFILL_VESSEL_DIALOG_ID -> {
                if (click.dialogBtn == DialogButton.POSITIVE) {
                    val vesselFragment =
                        childFragmentManager.findFragmentByTag("$FRAGMENT_TAG_PREFIX$VESSEL_FRAGMENT_POSITION") as VesselFragment
                    fragmentViewModel.vesselToPrefill?.let {
                        vesselFragment.fillVesselInfo(it)
                    }
                }
                true
            }
            ASK_PREFILL_CREW_DIALOG_ID -> {
                if (click.dialogBtn == DialogButton.POSITIVE) {
                    val crewFragment =
                        childFragmentManager.findFragmentByTag("$FRAGMENT_TAG_PREFIX$CREW_FRAGMENT_POSITION") as CrewFragment
                    crewFragment.fillCrewInfo(
                        fragmentViewModel.prefillCaptain!!,
                        fragmentViewModel.crewToPrefill!!
                    )
                }
                true
            }
            SUBMIT_DIALOG_ID -> {
                if (click.dialogBtn == DialogButton.POSITIVE || click.dialogBtn == DialogButton.NEUTRAL) {
                    val isDraft = click.dialogBtn == DialogButton.NEUTRAL
                    val successMessage = if (isDraft) getString(R.string.draft_saved) else getString(R.string.boarding_submitted)
                    activityViewModel.saveReport(
                        isDraft = isDraft,
                        listener = object : OnSaveListener {
                            override fun onSuccess() {
                                val args = bundleOf(KEY_CREATE_REPORT_RESULT to successMessage)
                                navigation.navigate(R.id.action_tabsFragment_to_home_navigation, args)
                                requireActivity().finish()
                            }

                            override fun onError(it: Throwable) {
                                Log.e("Save error", it.message ?: "")
                                showSnackMessage(requireView(), getString(R.string.saving_error))
                            }
                        })
                }
                true
            }

            ASK_SKIP_TABS_DIALOG_ID -> {
                if (click.dialogBtn == DialogButton.POSITIVE) {
                    if (pendingSkippingTabs != null) {
                        fragmentViewModel.onTabsSkipped(pendingSkippingTabs.orEmpty())
                        pendingSkippingTabs = null
                    }
                }
                true
            }
            else -> false
        }
    }

    @SuppressLint("ClickableViewAccessibility")
    private fun initUI(report: Report, reportPhotos: MutableList<PhotoItem>) {
        (requireActivity() as AppCompatActivity).setSupportActionBar(tabs_toolbar)
        tabs_toolbar.setNavigationIcon(R.drawable.ic_close_white)
        tabs_toolbar.title = getString(
            if (report.draft == true) R.string.edit_boarding else R.string.new_boarding
        )

        fragmentFactory = TabsFragmentFactory(requireContext(), this, report, reportPhotos)
        tabsFragmentAdapter = TabsFragmentAdapter(fragmentFactory, childFragmentManager, lifecycle)
        tabs_pager.apply {
            adapter = tabsFragmentAdapter
            offscreenPageLimit = 8
            isUserInputEnabled = false
            registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
                private var lastSelectedTabPosition = TabLayout.Tab.INVALID_POSITION

                override fun onPageSelected(position: Int) {
                    onTabChanged(lastSelectedTabPosition, position)
                    lastSelectedTabPosition = position
                }
            })
        }

        TabLayoutMediator(tabs_layout, tabs_pager, fragmentFactory.fragmentTitleConfigurator)
            .attach()

        // Unable to use tabs_layout.addOnTabSelectedListener() since we need to intercept click
        val tabTouchClickDetector = GestureTouchClickDetector(requireContext())
        val tabStrip = tabs_layout.getChildAt(0) as LinearLayout
        for (newPosition in 0 until tabStrip.childCount) {
            val currentTabPosition = tabs_layout.selectedTabPosition
            tabStrip.getChildAt(newPosition).setOnLongClickListener {
                return@setOnLongClickListener currentTabPosition != newPosition &&
                        fragmentViewModel.onTabClicked(
                            currentTabPosition,
                            newPosition,
                            currentReportFragment.isAllRequiredFieldsNotEmpty()
                        )
            }

            tabStrip.getChildAt(newPosition).setOnTouchListener { _, event ->
                val isClick = tabTouchClickDetector.onTouchEvent(event)
                val currentTabPosition = tabs_layout.selectedTabPosition
                return@setOnTouchListener currentTabPosition != newPosition &&
                        isClick &&
                        fragmentViewModel.onTabClicked(
                            currentTabPosition,
                            newPosition,
                            currentReportFragment.isAllRequiredFieldsNotEmpty()
                        )
            }
        }
    }
}

<|start_filename|>app/src/main/java/org/wildaid/ofish/ui/home/HomeActivityViewModel.kt<|end_filename|>
package org.wildaid.ofish.ui.home

import android.app.Application
import android.content.Context
import android.content.SharedPreferences
import android.content.res.Configuration
import android.util.Log
import androidx.appcompat.app.AppCompatDelegate
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import org.wildaid.ofish.Event
import org.wildaid.ofish.R
import org.wildaid.ofish.data.ON_DUTY
import org.wildaid.ofish.data.OfficerData
import org.wildaid.ofish.data.Repository
import org.wildaid.ofish.util.getString
import java.util.*

class HomeActivityViewModel(val repository: Repository, app: Application) : AndroidViewModel(app) {

    private val _onDutyStatusLiveData = MutableLiveData<Boolean>()
    val onDutyStatusLiveData: LiveData<Boolean>
        get() = _onDutyStatusLiveData

    private val _onDutyTextStatusLiveData = MutableLiveData<String>()
    val onDutyTextStatusLiveData: LiveData<String>
        get() = _onDutyTextStatusLiveData

    private val _darkModeLiveData = MutableLiveData<Boolean>()
    val darkModeLiveData: LiveData<Boolean>
        get() = _darkModeLiveData

    private val _currentOfficerLiveData = MutableLiveData<OfficerData>()
    val currentOfficerLiveData: LiveData<OfficerData>
        get() = _currentOfficerLiveData

    private val _timerLiveData = MutableLiveData<Event<Boolean>>()
    val timerLiveData: LiveData<Event<Boolean>>
        get() = _timerLiveData

    var userEventLiveData = MutableLiveData<Event<HomeActivityUserEvent>>()

    init {
        val officer = repository.getCurrentOfficer()
        if (!repository.isLoggedIn()) {
            logOutUser()
        } else {
            _currentOfficerLiveData.value = officer
        }

        val lastOnDutyStatus = repository.getRecentOnDutyChange()?.status == ON_DUTY
        _onDutyStatusLiveData.value = lastOnDutyStatus
        updateStringStatus(lastOnDutyStatus)
        updateDarkModeStatus()
    }

    fun changeStatus() {
        if (_onDutyStatusLiveData.value == true) {
            userEventLiveData.value = Event(HomeActivityUserEvent.BecomeNotAtSea)
        } else {
            userEventLiveData.value = Event(HomeActivityUserEvent.AskDutyConfirmationEvent)
        }
    }

    fun onDutyChanged(onDuty: Boolean, date: Date = Date()) {
        if (onDutyStatusLiveData.value == onDuty) {
            return
        }

        _onDutyStatusLiveData.value = onDuty
        _timerLiveData.value = Event(onDuty)
        repository.saveOnDutyChange(onDuty, date)

        updateStringStatus(onDuty)
    }

    fun onDarkModeStateChange() {
        if(_darkModeLiveData.value!!) {
            _darkModeLiveData.value = false
            saveDarkModeState(false)
            AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO)
        } else {
            _darkModeLiveData.value = true
            saveDarkModeState(true)
            AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES)
        }
    }

    fun logOutUser() {
        userEventLiveData.value = Event(HomeActivityUserEvent.AskUserLogoutEvent)
    }

    fun logoutConfirmed() {
        repository.logOut(
            logoutSuccess = {
                userEventLiveData.value = Event(HomeActivityUserEvent.HomeUserLogoutEvent)
            },
            logoutError = {
                // TODO add message here
                Log.d("Logout", "Cannot logout, error -> $it")
            }
        )
    }

    private fun updateStringStatus(onDuty: Boolean) {
        if (onDuty)
            _onDutyTextStatusLiveData.value = getString(R.string.at_sea)
        else
            _onDutyTextStatusLiveData.value = getString(R.string.not_at_sea)
    }

    private fun updateDarkModeStatus() {
        _darkModeLiveData.value = getDarkModeStatus()
    }

    private fun getDarkModeStatus(): Boolean {
        val defaultNightMode: Int = AppCompatDelegate.getDefaultNightMode()
        return defaultNightMode == AppCompatDelegate.MODE_NIGHT_YES
    }

    private fun saveDarkModeState(updatedState: Boolean) {
        val sharedPref: SharedPreferences = getApplication<Application>()
            .getSharedPreferences(getString(R.string.DARK_MODE_STATE), Context.MODE_PRIVATE) ?: return
        with (sharedPref.edit()) {
            putBoolean(getString(R.string.DARK_MODE_ENABLED), updatedState)
            apply()
        }
    }

    sealed class HomeActivityUserEvent {
        object AskDutyConfirmationEvent : HomeActivityUserEvent()
        object AskUserLogoutEvent : HomeActivityUserEvent()
        object HomeUserLogoutEvent : HomeActivityUserEvent()
        object BecomeNotAtSea : HomeActivityUserEvent()
    }
}


<|start_filename|>app/src/main/java/org/wildaid/ofish/app/OnDutyAlarmReminder.kt<|end_filename|>
package org.wildaid.ofish.app

import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.os.Bundle
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.navigation.NavDeepLinkBuilder
import org.wildaid.ofish.R
import org.wildaid.ofish.ui.home.HomeActivity

private const val NOTIFICATION_ID = 119

class OnDutyAlarmReminder : BroadcastReceiver() {

    override fun onReceive(context: Context, intent: Intent) {
        val pendingIntent = NavDeepLinkBuilder(context)
            .setComponentName(HomeActivity::class.java)
            .setGraph(R.navigation.home_navigation)
            .setDestination(R.id.profileFragment)
            .setArguments(Bundle())
            .createPendingIntent()

        val builder = NotificationCompat.Builder(context, CHANNEL_ID)
            .setSmallIcon(R.mipmap.ic_launcher)
            .setContentTitle(context.getString(R.string.app_name))
            .setContentText("Are you still on duty?")
            .setPriority(NotificationCompat.PRIORITY_DEFAULT)
            .setContentIntent(pendingIntent)

        with(NotificationManagerCompat.from(context)) {
            notify(NOTIFICATION_ID, builder.build())
        }
    }
}


<|start_filename|>app/src/main/java/org/wildaid/ofish/data/RealmDataSource.kt<|end_filename|>
package org.wildaid.ofish.data

import android.content.Context
import android.util.Log
import io.realm.Realm
import io.realm.Sort
import io.realm.kotlin.toFlow
import io.realm.kotlin.where
import io.realm.log.LogLevel
import io.realm.log.RealmLog
import io.realm.mongodb.App
import io.realm.mongodb.AppConfiguration
import io.realm.mongodb.AppException
import io.realm.mongodb.Credentials
import io.realm.mongodb.sync.SyncConfiguration
import kotlinx.coroutines.flow.Flow
import org.bson.Document
import org.bson.types.ObjectId
import org.wildaid.ofish.BuildConfig
import org.wildaid.ofish.data.report.*
import java.util.*

const val ON_DUTY = "On Duty"
const val OFF_DUTY = "Off Duty"
const val FIELD_ID = "_id"
const val FIELD_PERMIT_NUMBER = "permitNumber"
const val FIELD_NAME = "name"
const val FIELD_STATUS = "status"
const val DATE = "date"
const val BUSINESS = "business"
const val LOCATION = "location"
const val DRAFT = "draft"
const val _ID = "_id"
const val REPORTING_EMAIL_OFFICER = "reportingOfficer.email"
const val LAST_DELIVERY_DATE = "lastDelivery.date"
const val VESSEL_PERMIT_NUMBER = "vessel.permitNumber"
const val VESSEL_NAME = "vessel.name"
const val USER_EMAIL = "user.email"

const val OTHER = "Other"

private const val TAG = "Realm Setup"

class RealmDataSource(context: Context) {
    private val realmApp: App

    private lateinit var realm: Realm
    private lateinit var currentRealmUser: io.realm.mongodb.User
    private lateinit var currentOfficer: OfficerData

    init {
        Log.w("DataSource", "Created new instance of Realm data source")
        Realm.init(context)
        if (BuildConfig.DEBUG) {
            RealmLog.setLevel(LogLevel.ALL)
        }
        if (BuildConfig.REALM_APP_ID.isBlank()) {
            Log.e(
                TAG, "You need to specify properties realm_app_id in local.properties"
            )
        }

        val appConfigBuilder = AppConfiguration.Builder(BuildConfig.REALM_APP_ID)
            .appName(BuildConfig.VERSION_NAME)
            .appVersion(BuildConfig.VERSION_CODE.toString())

        if (!BuildConfig.REALM_URL.isBlank()) {
            appConfigBuilder.baseUrl(BuildConfig.REALM_URL)
        }

        realmApp = App(appConfigBuilder.build())
    }

    fun login(
        userName: String,
        password: <PASSWORD>,
        loginSuccess: (io.realm.mongodb.User) -> Unit,
        loginError: (AppException?) -> Unit
    ) {

        val appCredentials = Credentials.emailPassword(userName, password)
        realmApp.loginAsync(appCredentials) {
            if (it.error != null) {
                loginError.invoke(it.error)
            } else {
                val user = it.get()
                instantiateRealm(user)
                loginSuccess.invoke(user)
            }
        }
    }

    fun logOut(
        logoutSuccess: () -> Unit,
        logoutError: (Throwable?) -> Unit
    ) {
        realmApp.currentUser()?.logOutAsync {
            if (it.error != null) {
                logoutError.invoke(it.error.exception)
            } else {
                logoutSuccess.invoke()
            }
        }
    }

    fun restoreLoggedUser(): io.realm.mongodb.User? {
        return realmApp.currentUser()?.also {
            instantiateRealm(it)
        }
    }

    fun saveOnDutyChange(onDuty: Boolean, date: Date) {
        realm.executeTransactionAsync {
            it.copyToRealm(DutyChange().apply {
                this.user = User().apply {
                    name = Name().apply {
                        first = currentOfficer.firstName
                        last = currentOfficer.lastName
                    }
                    email = currentOfficer.email
                }
                this.date = date
                status = if (onDuty) ON_DUTY else OFF_DUTY
            })
        }
    }

    fun getRecentOnDutyChange(): DutyChange? {
        return realm.where<DutyChange>()
            .equalTo(USER_EMAIL, currentOfficer.email)
            .sort(DATE, Sort.DESCENDING)
            .findFirst()
    }

    fun getRecentStartCurrentDuty(): DutyChange? {
        return realm.where<DutyChange>()
            .equalTo(USER_EMAIL, currentOfficer.email)
            .equalTo(FIELD_STATUS, ON_DUTY)
            .sort(DATE, Sort.DESCENDING)
            .findFirst()
    }

    fun updateStartDateForCurrentDuty(date: Date) {
        val onDuty = getRecentStartCurrentDuty()!!
        realm.executeTransactionAsync {
            onDuty.date = date
        }
    }

    fun saveDraftWithTransaction(
        draft: Report,
        listener: OnSaveListener,
        photoIterator: Iterator<Photo>
    ) {
        realm.executeTransactionAsync(
            {
                while (photoIterator.hasNext()) {
                    val photo = photoIterator.next()
                    it.copyToRealmOrUpdate(photo)
                }
                it.copyToRealmOrUpdate(draft)
            },
            { listener.onSuccess() },
            { listener.onError(it) }
        )
    }

    fun saveReportWithTransaction(
        report: Report,
        listener: OnSaveListener,
        photoIterator: Iterator<Photo>
    ) {
        realm.executeTransactionAsync(
            {
                while (photoIterator.hasNext()) {
                    it.insertOrUpdate(photoIterator.next())
                }
                it.insertOrUpdate(report)
            },
            { listener.onSuccess() },
            { listener.onError(it) }
        )
    }

    fun deleteDraft(report: Report, photoIds: MutableList<String>) {
        realm.executeTransactionAsync {
            it.where<Report>()
                .equalTo(_ID, report._id)
                .findFirst()
                ?.deleteFromRealm()

            if (!photoIds.isNullOrEmpty()) {
                val query = realm.where<Photo>().equalTo(FIELD_ID, ObjectId(photoIds.first()))
                photoIds.forEach { id -> query.or().equalTo(FIELD_ID, ObjectId(id)) }
                query.findAll().deleteAllFromRealm()
            }
        }
    }

    fun getCurrentOfficer() = currentOfficer

    fun isLoggedIn() = realmApp.currentUser() != null

    fun findReportsGroupedByVesselNameAndPermitNumber(sort: Sort): Flow<List<Report>> {
        return realm.where<Report>()
            .isNotEmpty(VESSEL_NAME)
            .and()
            .beginGroup()
            .equalTo(DRAFT, false)
            .or()
            .equalTo(DRAFT, null as Boolean?)
            .endGroup()
            .sort(DATE, sort)
            .findAllAsync()
            .toFlow()
    }

    fun findDraftsGroupedByOfficerEmail(sort: Sort, email: String): Flow<List<Report>> {
        return realm.where<Report>()
            .equalTo(DRAFT, true)
            .and()
            .equalTo(REPORTING_EMAIL_OFFICER, email)
            .sort(DATE, sort)
            .findAllAsync()
            .toFlow()
    }

    fun findAllReports(sort: Sort): List<Report> {
        return realm.where<Report>()
            .sort(DATE, sort)
            .findAll()
    }

    fun findReport(reportId: ObjectId): Report? {
        return realm.where<Report>().equalTo(FIELD_ID, reportId).findFirst()
    }

    fun findDraft(draftId: ObjectId): Report? {
        val draft = realm.where<Report>().equalTo(FIELD_ID, draftId).findFirst()
        return draft?.let {
            realm.copyFromRealm(it)
        }
    }

    fun findReportsForBoat(boatPermitNumber: String, vesselName: String): Flow<List<Report>> {
        return realm.where<Report>()
            .equalTo(VESSEL_PERMIT_NUMBER, boatPermitNumber)
            .and()
            .equalTo(VESSEL_NAME, vesselName)
            .sort(DATE, Sort.DESCENDING)
            .findAllAsync()
            .toFlow()
    }

    fun getAmountOfDraftsByOfficerEmail(email: String): Int {
        return realm.where<Report>()
            .equalTo(DRAFT, true)
            .and()
            .equalTo(REPORTING_EMAIL_OFFICER, email)
            .findAll()
            .count()
    }

    fun getAmountOfDraftsForCurrentDuty(): Int {
        val dutyChange = getRecentStartCurrentDuty()
        dutyChange?.let {
            return realm.where<Report>()
                .sort(DATE, Sort.DESCENDING)
                .greaterThan(DATE, it.date)
                .equalTo(DRAFT, true)
                .count().toInt()
        }
        return 0
    }

    fun findReportsForCurrentDuty(): List<Report> {
        val dutyChange = getRecentStartCurrentDuty()
        dutyChange?.let {
            return realm.where<Report>()
                .sort(DATE, Sort.DESCENDING)
                .greaterThan(DATE, it.date)
                .findAll()
                .toList()
        }
        return emptyList()
    }

    fun findAllBoats(): List<Boat> {
        return realm.where<Boat>().findAll()
    }

    fun getPhotosWithIds(ids: List<String>): List<Photo> {
        if (ids.isNullOrEmpty()) return emptyList()

        val query = realm.where<Photo>().equalTo(FIELD_ID, ObjectId(ids.first()))
        ids.forEach { query.or().equalTo(FIELD_ID, ObjectId(it)) }
        return query.findAll()
    }

    fun getPhotoById(id: String): Photo? {
        if (id.isBlank()) return null
        return realm.where<Photo>().equalTo(FIELD_ID, ObjectId(id)).findFirst()
    }

    fun findBoat(boatPermitNumber: String, vesselName: String): Flow<Boat?> {
        return realm.where<Boat>()
            .equalTo(FIELD_PERMIT_NUMBER, boatPermitNumber)
            .and()
            .equalTo(FIELD_NAME, vesselName)
            .sort(LAST_DELIVERY_DATE, Sort.DESCENDING)
            .findFirstAsync()
            .toFlow()
    }

    fun getMenuData(): MenuData? {
        return realm.where<MenuData>().findFirst()
    }

    private fun instantiateRealm(user: io.realm.mongodb.User) {
        currentRealmUser = user

        val userData = user.customData
        val agencyName = (userData["agency"] as Document?)?.get("name") as String? ?: ""
        val officerEmail = userData["email"] as String? ?: ""
        val officerFirstName = (userData["name"] as Document?)?.get("first") as String? ?: ""
        val officerLastName = (userData["name"] as Document?)?.get("last") as String? ?: ""
        val profilePicId = userData["profilePic"] as String? ?: ""

        currentOfficer =
            OfficerData(officerEmail, officerFirstName, officerLastName, agencyName, profilePicId)

        val configuration = SyncConfiguration
            .Builder(user, agencyName)
            .build()
        realm = Realm.getInstance(configuration)
    }

    fun getAllDeliveryBusiness(): List<Delivery> {
        return realm.where<Delivery>()
            .isNotEmpty(BUSINESS)
            .sort(BUSINESS, Sort.ASCENDING)
            .distinct(BUSINESS, LOCATION)
            .findAll()
            .toList()
    }

    fun savePhoto(photo: Photo) {
        realm.executeTransactionAsync {
            it.copyToRealmOrUpdate(photo)
        }
    }
}


<|start_filename|>app/src/main/java/org/wildaid/ofish/ui/login/LoginFragment.kt<|end_filename|>
package org.wildaid.ofish.ui.login

import android.os.Bundle
import android.text.Editable
import android.text.TextWatcher
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.Observer
import androidx.navigation.NavController
import androidx.navigation.fragment.findNavController
import org.wildaid.ofish.BuildConfig
import org.wildaid.ofish.EventObserver
import org.wildaid.ofish.R
import org.wildaid.ofish.databinding.FragmentLoginBinding
import org.wildaid.ofish.ui.base.ConfirmationDialogFragment
import org.wildaid.ofish.ui.base.DIALOG_CLICK_EVENT
import org.wildaid.ofish.ui.base.DialogButton
import org.wildaid.ofish.ui.base.DialogClickEvent
import org.wildaid.ofish.util.getViewModelFactory

const val LOGIN_ERROR_DIALOG = 401

class LoginFragment : Fragment() {

    private val loginViewModel by viewModels<LoginViewModel> { getViewModelFactory() }
    private val navigation: NavController by lazy { findNavController() }

    private lateinit var binding: FragmentLoginBinding

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
        return FragmentLoginBinding.inflate(inflater, container, false)
                .also { this.binding = it }
                .root
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        loginViewModel.loginLiveData.observe(viewLifecycleOwner, Observer(::handleLoginResult))
        loginViewModel.progressLiveData.observe(viewLifecycleOwner, EventObserver(::handleProgress))

        subscribeToDialogEvents()

        if (BuildConfig.REALM_USER.isBlank() || BuildConfig.REALM_PASSWORD.isBlank()) {
            Log.i(
                    "Login Setup",
                    "You can specify properties realm_user and/or realm_password in realm.properties to pre fill credentials"
            )
        }
        binding.edUser.setText(BuildConfig.REALM_USER)
        binding.edPassword.setText(BuildConfig.REALM_PASSWORD)

        binding.btnLogin.isEnabled = isUserFieldsValid()

        binding.edUser.addTextChangedListener(watcher)
        binding.edPassword.addTextChangedListener(watcher)

        binding.btnLogin.setOnClickListener {
            loginViewModel.login(binding.edUser.text.toString(), binding.edPassword.text.toString())
        }
    }

    private fun isUserFieldsValid() =
            !(binding.edUser.text.isNullOrBlank() || binding.edPassword.text.isNullOrBlank())

    private fun handleLoginResult(loginResult: LoginViewModel.LoginResult) {
        when (loginResult) {
            is LoginViewModel.LoginResult.LoginSuccess -> {
                navigation.navigate(R.id.home_screen)
                requireActivity().finish()
            }
            is LoginViewModel.LoginResult.LoginError -> {
                showLoginErrorDialog()
            }
        }
    }

    private fun subscribeToDialogEvents() {
        val navStack = navigation.currentBackStackEntry!!
        navStack.lifecycle.addObserver(LifecycleEventObserver { _, event ->
            if (event != Lifecycle.Event.ON_RESUME) {
                return@LifecycleEventObserver
            }

            if (navStack.savedStateHandle.contains(DIALOG_CLICK_EVENT)) {
                val click = navStack.savedStateHandle.get<DialogClickEvent>(DIALOG_CLICK_EVENT)!!
                val handled = handleDialogClick(click)
                if (handled) {
                    navStack.savedStateHandle.remove<DialogClickEvent>(DIALOG_CLICK_EVENT)!!
                }
            }
        })
    }

    private fun handleDialogClick(click: DialogClickEvent): Boolean {
        return when (click.dialogId) {
            LOGIN_ERROR_DIALOG -> {
                if (click.dialogBtn == DialogButton.POSITIVE) {
                    navigation.popBackStack()
                }
                true
            }
            else -> false
        }
    }

    private fun showLoginErrorDialog() {
        val dialogBundle = ConfirmationDialogFragment.Bundler(
                LOGIN_ERROR_DIALOG,
                getString(R.string.login_error),
                getString(R.string.invalid_email_or_password),
                getString(android.R.string.ok)
        ).bundle()
        navigation.navigate(R.id.error_login_dialog, dialogBundle)
    }

    private fun handleProgress(showProgress: Boolean) {
        if (showProgress) {
            navigation.navigate(R.id.progress_dialog)
        } else if (navigation.currentDestination?.id == R.id.progress_dialog) {
            navigation.popBackStack()
        }
    }

    private val watcher = object : TextWatcher {
        override fun afterTextChanged(s: Editable?) {
        }

        override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
        }

        override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
            binding.btnLogin.isEnabled = isUserFieldsValid()
        }
    }
}

<|start_filename|>app/src/main/java/org/wildaid/ofish/ui/createreport/CreateReportViewModel.kt<|end_filename|>
package org.wildaid.ofish.ui.createreport

import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import org.bson.types.ObjectId
import org.wildaid.ofish.Event
import org.wildaid.ofish.data.OnSaveListener
import org.wildaid.ofish.data.Repository
import org.wildaid.ofish.data.report.*
import org.wildaid.ofish.ui.base.PhotoItem
import java.util.*

class CreateReportViewModel(val repository: Repository) : ViewModel() {
    private val _createReportUserEvent = MutableLiveData<Event<CreateReportUserEvent>>()
    val createReportUserEvent: LiveData<Event<CreateReportUserEvent>>
        get() = _createReportUserEvent

    var isOnSearch: Boolean = false
    var isAddingCrewMember: Boolean = false

    lateinit var report: Report
    val reportPhotos: MutableList<PhotoItem> = mutableListOf()

    fun initReport(reportDraftId: ObjectId?) {
        report = if (reportDraftId == null) {
            initiateNewReport()
        } else {
            getDraft(reportDraftId)
        }

        _createReportUserEvent.value = Event(CreateReportUserEvent.StartReportCreation)
    }

    fun saveReport(isDraft: Boolean? = null, listener: OnSaveListener) {
        report.draft = isDraft
        val photosToSave = reportPhotos.map { Pair(it.photo, it.localUri) }
        repository.saveReport(report, isDraft, photosToSave, listener)
    }

    fun deleteReport() {
        repository.deleteDraft(report)
        _createReportUserEvent.value = Event(CreateReportUserEvent.NavigateToDraftList)
    }

    fun onBackPressed(): Boolean {
        if (isOnSearch || isAddingCrewMember) {
            return false
        }
        if (report.draft!!) {
            _createReportUserEvent.value = Event(CreateReportUserEvent.AskDeleteDraft)
        } else {
            _createReportUserEvent.value = Event(CreateReportUserEvent.AskDiscardBoarding)
        }
        return true
    }

    private fun initiateNewReport(): Report {
        val officer = repository.getCurrentOfficer()
        return Report().apply {
            reportingOfficer?.apply {
                email = officer.email
                name?.apply {
                    first = officer.firstName
                    last = officer.lastName
                }
            }
            vessel?.lastDelivery?.date = Date(0)

            // Pre-filled empty items for create report flow
            inspection?.summary?.seizures = Seizures()
            inspection?.summary?.violations?.add(Violation())
            notes.add(AnnotatedNote())
        }
    }

    private fun getDraft(draftId: ObjectId): Report {
        val draft = repository.findDraft(draftId) ?: return initiateNewReport()

        if (draft.vessel?.lastDelivery == null) {
            draft.vessel?.lastDelivery = Delivery()
        }
        return draft
    }

    sealed class CreateReportUserEvent {
        object AskDiscardBoarding : CreateReportUserEvent()
        object StartReportCreation : CreateReportUserEvent()
        object AskDeleteDraft : CreateReportUserEvent()
        object NavigateToDraftList: CreateReportUserEvent()
    }
}

<|start_filename|>app/src/main/java/org/wildaid/ofish/util/ViewExt.kt<|end_filename|>
package org.wildaid.ofish.util

import android.content.Context
import android.view.View
import android.view.inputmethod.InputMethodManager

fun View.setVisible(visible: Boolean) {
    if (visible) {
        this.visibility = View.VISIBLE
    } else {
        this.visibility = View.GONE
    }
}

fun View.hideKeyboard() {
    val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
    imm.hideSoftInputFromWindow(windowToken, 0)
}

fun View.showKeyboard() {
    val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
    imm.toggleSoftInput(InputMethodManager.SHOW_FORCED, InputMethodManager.HIDE_IMPLICIT_ONLY)
}

<|start_filename|>app/src/main/java/org/wildaid/ofish/ui/notes/NoteItem.kt<|end_filename|>
package org.wildaid.ofish.ui.notes

import org.wildaid.ofish.data.report.AnnotatedNote
import org.wildaid.ofish.ui.base.PhotoItem

data class NoteItem(
    var note: AnnotatedNote,
    var title: String,
    var inEditMode: Boolean,
    val photos: MutableList<PhotoItem> = mutableListOf()
) {
    fun hasPhotos() = photos.isNotEmpty()

    fun addPhoto(newPhoto: PhotoItem) {
        photos.add(newPhoto)
        note.photoIDs.add(newPhoto.photo._id.toString())
    }

    fun removePhoto(removed: PhotoItem) {
        photos.remove(removed)
        note.photoIDs.remove(removed.photo._id.toString())
    }
}

<|start_filename|>app/src/main/java/org/wildaid/ofish/ui/base/AttachmentItem.kt<|end_filename|>
package org.wildaid.ofish.ui.base

import android.net.Uri
import org.wildaid.ofish.Event
import org.wildaid.ofish.data.report.Attachments
import org.wildaid.ofish.data.report.Photo

data class AttachmentItem(
    var attachment: Attachments,
    val photos: MutableList<PhotoItem> = mutableListOf(),
    var isNoteFocused: Event<Boolean> = Event(false)
) {
    fun hasNotes(): Boolean = attachment.notes.isNotEmpty()
    fun hasPhotos(): Boolean = photos.isNotEmpty()

    fun addPhoto(newPhoto: PhotoItem) {
        photos.add(newPhoto)
        attachment.photoIDs.add(newPhoto.photo._id.toString())
    }

    fun removePhoto(removed: PhotoItem) {
        photos.remove(removed)
        attachment.photoIDs.remove(removed.photo._id.toString())
    }

    fun setNote(note: String?) {
        if (attachment.notes.isEmpty()) {
            attachment.notes.add(0, note)
        } else {
            attachment.notes[0] = note
        }
    }

    fun getNote(): String? {
        return attachment.notes.getOrNull(0)
    }

    fun addNote() {
        if (attachment.notes.isEmpty()) {
            attachment.notes.add(0, "")
        } else {
            attachment.notes[0] = ""
        }
        isNoteFocused = Event(true)
    }

    fun removeNote() {
        attachment.notes.clear()
    }
}

data class PhotoItem(
    val photo: Photo,
    var localUri: Uri? = null
)

<|start_filename|>app/src/main/java/org/wildaid/ofish/ui/search/complex/ComplexSearchViewModel.kt<|end_filename|>
package org.wildaid.ofish.ui.search.complex

import android.app.Application
import kotlinx.coroutines.flow.*
import org.wildaid.ofish.R
import org.wildaid.ofish.data.Repository
import org.wildaid.ofish.data.report.CrewMember
import org.wildaid.ofish.data.report.Report
import org.wildaid.ofish.ui.search.base.BaseSearchType
import org.wildaid.ofish.ui.search.base.BaseSearchViewModel

private const val RECENT_BOARDINGS_COUNT = 5

class ComplexSearchViewModel(repository: Repository, application: Application) :
    BaseSearchViewModel<SearchModel>(application) {
    override fun getDataSource(searchEntity: BaseSearchType, report: Report?): SearchDataSource {
        return when (searchEntity) {
            is ComplexSearchFragment.SearchDrafts -> searchDrafts
            is ComplexSearchFragment.SearchBusiness -> searchBusinessDataSource
            is ComplexSearchFragment.SearchViolation -> searchViolationDataSource
            is ComplexSearchFragment.SearchRecords -> searchRecordsDataSource.apply {
                isAddAvailable = false
            }
            is ComplexSearchFragment.SearchBoardVessels -> searchRecordsDataSource.apply {
                isAddAvailable = true
            }
            is ComplexSearchFragment.SearchCrew -> searchCrewDataSource.apply {
                this.report = report
                    ?: throw IllegalArgumentException("$searchEntity is not supported with null report")
            }
            else -> throw IllegalArgumentException("Unsupported search entity $searchEntity")
        }
    }

    private val searchBusinessDataSource = object : SearchDataSource() {
        private val addSearchModel = AddSearchModel(R.string.new_business)

        override fun initiateData(): Flow<List<SearchModel>> {
            // TODO: make repo operation a flow instead
            return mutableListOf<SearchModel>().apply {
                addAll(repository.getBusinessAndLocation().map { BusinessSearchModel(it) })
                add(addSearchModel)
            }.let { searchModels ->
                flowOf(searchModels)
            }
        }

        override fun applyFilter(filter: String): Flow<List<SearchModel>> {
            // TODO: make repo operation a flow instead
            return mutableListOf<SearchModel>().apply {
                this.addAll(
                    repository.getBusinessAndLocation()
                        .filter {
                            it.first.contains(filter, true) ||
                                    it.second.contains(filter, true)
                        }.map { BusinessSearchModel(it) }
                )
                this.add(addSearchModel)
            }.let { searchModels ->
                flowOf(searchModels)
            }
        }
    }

    private val searchViolationDataSource = object : SearchDataSource() {
        override fun initiateData(): Flow<List<SearchModel>> {
            // TODO: make repo operation a flow instead
            return repository.getOffences()
                .map { offenceData ->
                    ViolationSearchModel(offenceData)
                }.let { violationSearchModels ->
                    flowOf(violationSearchModels)
                }
        }

        override fun applyFilter(filter: String): Flow<List<SearchModel>> {
            // TODO: make repo operation a flow instead
            return repository.getOffences()
                .filter { it.code.contains(filter, true) || it.explanation.contains(filter, true) }
                .map { ViolationSearchModel(it) }
                .let {
                    flowOf(it)
                }
        }
    }

    private val searchRecordsDataSource = object : SearchDataSource() {
        var isAddAvailable: Boolean = false

        private val addSearchModel = AddSearchModel(R.string.add_new_vessel)
        private var cachedAllReports = emptyList<Report>()

        override fun initiateData(): Flow<List<SearchModel>> {
            return repository.findReportsGroupedByVessel()
                .map { reports ->
                    cachedAllReports = reports

                    mutableListOf<SearchModel>().apply {
                        if (isAddAvailable) {
                            this.add(addSearchModel)
                        }
                        if (reports.isNotEmpty()) {
                            this.add(TextViewSearchModel(R.string.recently_boarded))
                        }

                        this.addAll(
                            reports.asSequence()
                                .filterNot { it.vessel?.name.isNullOrBlank() }
                                .groupBy { it.vessel?.permitNumber to it.vessel?.name }
                                .map { pair ->
                                    RecordSearchModel(
                                        pair.value.find { (it.vessel?.permitNumber to it.vessel?.name) == pair.key }?.vessel!!,
                                        pair.value.sortedByDescending { report -> report.date },
                                        repository
                                    )
                                }.take(RECENT_BOARDINGS_COUNT)
                        )
                    }
                }
        }

        override fun applyFilter(filter: String): Flow<List<SearchModel>> {
            return if (filter.isBlank()) {
                initiateData()
            } else {
                repository.findReportsGroupedByVessel()
                    .map { reports ->
                        if (cachedAllReports.isNullOrEmpty()) {
                            cachedAllReports = reports
                        }

                        mutableListOf<SearchModel>().apply {
                            if (isAddAvailable) {
                                this.add(addSearchModel)
                            }

                            this.addAll(
                                cachedAllReports.filterNot { it.vessel?.name.isNullOrBlank() }
                                    .filter { it.vessel?.name.orEmpty().contains(filter, true) }
                                    .groupBy { it.vessel?.permitNumber to it.vessel?.name }
                                    .map { pair ->
                                        RecordSearchModel(
                                            pair.value.find { (it.vessel?.permitNumber to it.vessel?.name) == pair.key }?.vessel!!,
                                            pair.value.sortedByDescending { report -> report.date },
                                            repository
                                        )
                                    }
                            )
                        }
                    }
            }
        }
    }

    private val searchDrafts = object : SearchDataSource() {
        private var cachedDraftBoardings = emptyList<Report>()

        override fun initiateData(): Flow<List<SearchModel>> {
            return repository.findDraftsGroupedByOfficerNameAndEmail()
                .onEach { draftBoardings ->
                    if (cachedDraftBoardings.isNullOrEmpty()) {
                        this.cachedDraftBoardings = draftBoardings
                    }
                }.map {
                    mutableListOf<SearchModel>().apply {
                        this.addAll(
                            cachedDraftBoardings.asSequence()
                                .map {
                                    RecordSearchModel(
                                        it.vessel!!,
                                        listOf(it).sortedByDescending { report -> report.date },
                                        repository
                                    )
                                }
                        )
                    }
                }
        }

        override fun applyFilter(filter: String): Flow<List<SearchModel>> {
            return mutableListOf<SearchModel>().apply {
                if (filter.isBlank()) {
                    return initiateData()
                }

                this.addAll(
                    cachedDraftBoardings.filter {
                        it.vessel?.name.orEmpty().contains(filter, true)
                    }
                        .map {
                            RecordSearchModel(
                                it.vessel!!,
                                listOf(it).sortedByDescending { report -> report.date },
                                repository
                            )
                        }
                )
            }.let { searchModels ->
                flowOf(searchModels)
            }
        }
    }

    private val searchCrewDataSource = object : SearchDataSource() {
        lateinit var report: Report

        private val addSearchModel = AddSearchModel(R.string.add_crew_member)

        override fun initiateData(): Flow<List<SearchModel>> {
            // TODO: make repo operation a flow instead
            return mutableListOf<SearchModel>().apply {
                val captain = report.captain
                if (isCaptainNotNullOrBlank(captain)) {
                    this.add(CrewSearchModel(captain!!, true))
                }

                this.addAll(
                    report.crew
                        .filter { it.name.isNotBlank() || it.license.isNotBlank() }
                        .map { member -> CrewSearchModel(member, false) }
                )
                this.add(addSearchModel)
            }.let { searchModels ->
                flowOf(searchModels)
            }
        }

        override fun applyFilter(filter: String): Flow<List<SearchModel>> {
            // TODO: make repo operation a flow instead
            return mutableListOf<SearchModel>().apply {
                val captain = report.captain
                if (isCaptainNotNullOrBlank(captain) && containFilter(captain!!, filter)) {
                    this.add(CrewSearchModel(captain, true))
                }
                this.addAll(
                    report.crew
                        .filter { it.name.isNotBlank() || it.license.isNotBlank() }
                        .filter { member -> containFilter(member, filter) }
                        .map { member -> CrewSearchModel(member, false) }
                )
                this.add(addSearchModel)
            }.let { searchModels ->
                flowOf(searchModels)
            }
        }

        private fun isCaptainNotNullOrBlank(captain: CrewMember?) =
            !captain?.name.isNullOrBlank() && !captain?.license.isNullOrBlank()

        private fun containFilter(member: CrewMember, filter: String) =
            member.name.contains(filter, true) || member.license.contains(filter, true)
    }
}

<|start_filename|>app/src/main/java/org/wildaid/ofish/ui/home/HomeFragment.kt<|end_filename|>
package org.wildaid.ofish.ui.home

import android.Manifest.permission.ACCESS_COARSE_LOCATION
import android.Manifest.permission.ACCESS_FINE_LOCATION
import android.annotation.SuppressLint
import android.app.Activity
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.view.View
import android.widget.ImageView
import android.widget.RelativeLayout
import androidx.appcompat.app.AppCompatDelegate
import androidx.core.os.bundleOf
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.fragment.app.viewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.Observer
import androidx.navigation.NavController
import androidx.navigation.fragment.findNavController
import com.bumptech.glide.Glide
import com.google.android.gms.location.FusedLocationProviderClient
import com.google.android.gms.location.LocationServices
import com.google.android.gms.maps.CameraUpdateFactory
import com.google.android.gms.maps.GoogleMap
import com.google.android.gms.maps.OnMapReadyCallback
import com.google.android.gms.maps.SupportMapFragment
import com.google.android.gms.maps.model.LatLng
import com.google.android.gms.maps.model.MapStyleOptions
import kotlinx.android.synthetic.main.fragment_home.*
import org.wildaid.ofish.EventObserver
import org.wildaid.ofish.R
import org.wildaid.ofish.data.mpa.addTestMpa
import org.wildaid.ofish.databinding.FragmentHomeBinding
import org.wildaid.ofish.ui.base.*
import org.wildaid.ofish.ui.createreport.SHOULD_NAVIGATE_TO_DRAFT_LIST
import org.wildaid.ofish.ui.search.base.BaseSearchFragment
import org.wildaid.ofish.ui.search.complex.ComplexSearchFragment
import org.wildaid.ofish.util.*

const val KEY_CREATE_REPORT_RESULT = "create_report_message_result"
const val ZOOM_LEVEL = 10f

private const val REQUEST_CODE_PERMISSIONS = 1444
private const val CREATE_REPORT_FINISHED_DIALOG_ID = 100

class HomeFragment : Fragment(R.layout.fragment_home),
    OnMapReadyCallback {

    private lateinit var fusedLocationClient: FusedLocationProviderClient
    private lateinit var androidPermissions: AndroidPermissions
    private lateinit var dataBinding: FragmentHomeBinding
    private lateinit var googleMap: GoogleMap
    private lateinit var pendingImageUri: Uri

    private val navigation: NavController by lazy { findNavController() }
    private val fragmentViewModel: HomeFragmentViewModel by viewModels { getViewModelFactory() }
    private val activityViewModel: HomeActivityViewModel by activityViewModels { getViewModelFactory() }
    private val requiredPermissions = arrayOf(ACCESS_FINE_LOCATION,ACCESS_COARSE_LOCATION)

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        androidPermissions = AndroidPermissions(requireActivity(), this)
        fusedLocationClient = LocationServices.getFusedLocationProviderClient(requireActivity())
        fragmentViewModel.activityViewModel = activityViewModel
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        initUI(view)

        setObservers()
        subscribeToDialogEvents()

        arguments?.let {
            if (it.containsKey(KEY_CREATE_REPORT_RESULT)) {
                val message = it.getString(KEY_CREATE_REPORT_RESULT, null)
                it.remove(KEY_CREATE_REPORT_RESULT)
                showCreateReportDialog(message)
            }
        }
    }

    override fun onResume() {
        super.onResume()
        fragmentViewModel.updateDraftCount()
    }

    private fun setObservers() {
        fragmentViewModel.locationLiveData.observe(viewLifecycleOwner, Observer {
            home_latitude.text = convert(it.first, LATITUDE)
            home_longitude.text = convert(it.second, LONGITUDE)
        })

        fragmentViewModel.userEventLiveData.observe(viewLifecycleOwner, EventObserver {
            when (it) {
                HomeFragmentViewModel.HomeFragmentUserEvent.ShowDrafts -> findDrafts()
                HomeFragmentViewModel.HomeFragmentUserEvent.BoardVessel -> boardVessel()
                HomeFragmentViewModel.HomeFragmentUserEvent.FindRecords -> findRecords()
                HomeFragmentViewModel.HomeFragmentUserEvent.ShowUserStatus -> navigateToProfile()
            }
        })

        fragmentViewModel.amountOfDrafts.observe(viewLifecycleOwner, Observer { amount ->
            if (amount > 0) {
                showAmountLabel()
                text_amount_of_drafts.text = amount.toString()
            } else {
                hideAmountLabel()
            }
        })

        activityViewModel.currentOfficerLiveData.observe(viewLifecycleOwner, Observer {
            showOfficerPhoto(image_user)
        })

        activityViewModel.onDutyStatusLiveData.observe(viewLifecycleOwner, Observer { dutyStatus ->
            image_user_status.isEnabled = dutyStatus
        })
    }

    private fun hideAmountLabel() {
        text_amount_of_drafts.visibility = View.GONE
        image_amount_drafts.visibility = View.GONE
    }

    private fun showAmountLabel() {
        text_amount_of_drafts.visibility = View.VISIBLE
        image_amount_drafts.visibility = View.VISIBLE
    }

    private fun navigateToProfile() {
        navigation.navigate(R.id.action_home_fragment_to_profileFragment)
    }

    private fun showOfficerPhoto(view: ImageView?) {
        view?.let {
            val photo = fragmentViewModel.repository.getCurrentOfficerPhoto()
            Glide.with(this)
                .load(photo?.getResourceForLoading())
                .placeholder(R.drawable.ic_account_circle)
                .into(it)
        }
    }

    private fun showCreateReportDialog(message: String) {
        val dialogBundle = ConfirmationDialogFragment.Bundler(
            CREATE_REPORT_FINISHED_DIALOG_ID,
            null,
            message,
            getString(android.R.string.ok)
        ).bundle()

        navigation.navigate(R.id.ask_change_duty_dialog, dialogBundle)
    }

    private fun updateProfilePhoto() {
        Glide.with(this)
            .clear(image_user)

        Glide.get(requireContext()).clearMemory()
        Thread {
            Glide.get(requireContext()).clearDiskCache()
        }.start()

        showOfficerPhoto(image_user)
    }

    private fun initUI(view: View) {
        dataBinding = FragmentHomeBinding.bind(view).apply {
            this.lifecycleOwner = viewLifecycleOwner
            this.fragmentViewModel = this@HomeFragment.fragmentViewModel
            this.homeActivityViewModel = this@HomeFragment.activityViewModel
        }

        val mapFragment = childFragmentManager.findFragmentById(R.id.home_map) as SupportMapFragment
        mapFragment.getMapAsync(this)
    }

    override fun onMapReady(map: GoogleMap) {
        googleMap = map
        checkPermissions()
        addTestMpa(googleMap, resources)
        if(isDarkModeEnabled())
            map.setMapStyle(MapStyleOptions.loadRawResourceStyle(activity?.applicationContext,R.raw.map_dark_mode))
    }

    @SuppressLint("ResourceType")
    private fun setMyLocationPosition() {
        googleMap.isMyLocationEnabled = true
        val locationButton: View = (home_map.findViewById<View>(1).parent as View).findViewById(2)
        if(isDarkModeEnabled()) {
            (locationButton as ImageView).setImageResource(R.drawable.my_location)
            locationButton.setBackgroundResource(R.drawable.shape_rectangle_my_location)
            locationButton.setPadding(25,25,25,25)
        }
        val rlp: RelativeLayout.LayoutParams =
            locationButton.layoutParams as RelativeLayout.LayoutParams
        rlp.addRule(RelativeLayout.ALIGN_PARENT_TOP, RelativeLayout.TRUE)
        rlp.addRule(RelativeLayout.ALIGN_PARENT_BOTTOM, 0)
        rlp.setMargins(
            0,
            resources.getDimension(R.dimen.my_location_margin_top).toInt(),
            resources.getDimension(R.dimen.my_location_margin_right).toInt(),
            0
        )
    }

    override fun onRequestPermissionsResult(
        requestCode: Int,
        permissions: Array<out String>,
        grantResults: IntArray
    ) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults)
        if (androidPermissions.isPermissionsGranted(*requiredPermissions)) {
            onPermissionsGranted()
        } else {
            onPermissionsDeclined()
        }
    }

    private fun findRecords() {
        val bundle =
            bundleOf(BaseSearchFragment.SEARCH_ENTITY_KEY to ComplexSearchFragment.SearchRecords)
        navigation.navigate(R.id.action_home_fragment_to_complex_search, bundle)
    }

    private fun findDrafts() {
        val bundle =
            bundleOf(BaseSearchFragment.SEARCH_ENTITY_KEY to ComplexSearchFragment.SearchDrafts)
        navigation.navigate(R.id.action_home_fragment_to_complex_search, bundle)
    }

    private fun boardVessel() {
        val bundle =
            bundleOf(BaseSearchFragment.SEARCH_ENTITY_KEY to ComplexSearchFragment.SearchBoardVessels)
        navigation.navigate(R.id.action_home_fragment_to_complex_search, bundle)
    }

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        if (requestCode == REQUEST_PICK_IMAGE && resultCode == Activity.RESULT_OK) {
            val uri = data?.data ?: pendingImageUri
            fragmentViewModel.saveProfileImage(uri)
            updateProfilePhoto()
        }
    }

    private fun checkPermissions() {
        val nonGranted = androidPermissions.checkNonGrantedPermissions(*requiredPermissions)

        if (nonGranted.isNotEmpty()) {
            androidPermissions.requestPermissions(REQUEST_CODE_PERMISSIONS, *nonGranted)
        } else {
            onPermissionsGranted()
        }
    }

    private fun onPermissionsDeclined() {
        requireActivity().finish()
    }

    @SuppressLint("MissingPermission")
    private fun onPermissionsGranted() {
        setMyLocationPosition()
        fusedLocationClient.lastLocation.addOnSuccessListener {
            it?.let {
                val coordinates = LatLng(it.latitude, it.longitude)
                googleMap.moveCamera(CameraUpdateFactory.newLatLngZoom(coordinates, ZOOM_LEVEL))
                googleMap.isMyLocationEnabled = true
                fragmentViewModel.onLocationAvailable(it.latitude, it.longitude)
            }
        }
    }

    private fun subscribeToDialogEvents() {
        val navStack = navigation.currentBackStackEntry!!
        navStack.lifecycle.addObserver(LifecycleEventObserver { _, event ->
            if (event != Lifecycle.Event.ON_RESUME) {
                return@LifecycleEventObserver
            }

            if (navStack.savedStateHandle.contains(DIALOG_CLICK_EVENT)) {
                val click = navStack.savedStateHandle.get<DialogClickEvent>(DIALOG_CLICK_EVENT)!!
                if (handleDialogClick(click)) {
                    navStack.savedStateHandle.remove<DialogClickEvent>(DIALOG_CLICK_EVENT)!!
                }
            }
        })
    }

    private fun handleDialogClick(event: DialogClickEvent): Boolean {
        return when (event.dialogId) {
            ASK_CHANGE_DUTY_DIALOG_ID -> {
                if (event.dialogBtn == DialogButton.POSITIVE) {
                    activityViewModel.onDutyChanged(true)
                    boardVessel()
                }
                if (event.dialogBtn == DialogButton.NEGATIVE) {
                    activityViewModel.onDutyChanged(false)
                }
                true
            }
            CREATE_REPORT_FINISHED_DIALOG_ID -> {

                arguments?.let {
                    if (it.containsKey(SHOULD_NAVIGATE_TO_DRAFT_LIST)) {
                        val shouldOpenDrafts = it.getBoolean(SHOULD_NAVIGATE_TO_DRAFT_LIST)
                        if (shouldOpenDrafts) {
                            findDrafts()
                        }
                    }
                }
                true
            }
            else -> false
        }
    }

    private fun isDarkModeEnabled(): Boolean {
        val defaultNightMode: Int = AppCompatDelegate.getDefaultNightMode()
        return defaultNightMode == AppCompatDelegate.MODE_NIGHT_YES
    }
}


<|start_filename|>app/src/main/java/org/wildaid/ofish/ui/base/BaseReportFragment.kt<|end_filename|>
package org.wildaid.ofish.ui.base

import android.app.Activity.RESULT_OK
import android.content.Intent
import android.content.res.ColorStateList
import android.net.Uri
import android.os.Bundle
import android.view.View
import androidx.annotation.LayoutRes
import androidx.core.content.ContextCompat
import androidx.core.os.bundleOf
import androidx.core.view.ViewCompat
import androidx.fragment.app.Fragment
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.navigation.NavController
import androidx.navigation.fragment.FragmentNavigatorExtras
import androidx.navigation.fragment.findNavController
import com.google.android.material.snackbar.BaseTransientBottomBar
import com.google.android.material.snackbar.Snackbar
import com.google.android.material.textfield.TextInputLayout
import org.wildaid.ofish.R
import org.wildaid.ofish.data.report.Report
import org.wildaid.ofish.util.combineIntents
import org.wildaid.ofish.util.createCameraIntent
import org.wildaid.ofish.util.createGalleryIntent
import org.wildaid.ofish.util.createImageUri

const val CARDS_OFFSET_SIZE = 48
private const val ATTACHMENT_DIALOG_ID = 1231
const val REQUEST_PICK_IMAGE = 10001
const val PHOTO_ID = "photo_id"

abstract class BaseReportFragment(@LayoutRes contentLayoutId: Int) : Fragment(contentLayoutId) {
    lateinit var onNextListener: OnNextClickedListener
    lateinit var currentReport: Report
    lateinit var currentReportPhotos: MutableList<PhotoItem>

    protected val navigation: NavController by lazy { findNavController() }

    private var pendingImageUri: Uri? = null
    private var pendingNoteSelection: (() -> Unit)? = null
    private var pendingPhotoSelection: ((Uri) -> Unit)? = null

    protected var isFieldCheckPassed = false

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        subscribeForAttachmentDialogResult()
    }

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)
        if (requestCode == REQUEST_PICK_IMAGE && resultCode == RESULT_OK) {
            if (pendingPhotoSelection != null && pendingImageUri != null) {
                pendingPhotoSelection?.invoke(data?.data ?: pendingImageUri!!)
                pendingPhotoSelection = null
            }
        }
    }

    abstract fun isAllRequiredFieldsNotEmpty(): Boolean

    abstract fun validateForms(): Boolean

    protected fun validateField(field: TextInputLayout): Boolean {
        var result = true
        val text = field.editText?.text
        if (field.visibility == View.VISIBLE && text.isNullOrBlank()) {
            result = false
            showEmptyFieldWarning(field)
        }
        return result
    }

    protected fun askForAttachmentType(onNoteSelected: () -> Unit, onPhotoSelected: (Uri) -> Unit) {
        pendingNoteSelection = onNoteSelected
        pendingPhotoSelection = onPhotoSelected

        val dialogBundle = ConfirmationDialogFragment.Bundler(
            ATTACHMENT_DIALOG_ID,
            getString(R.string.select_attachment_dialog_title),
            getString(R.string.select_attachment_dialog_message),
            getString(R.string.select_attachment_dialog_note),
            getString(R.string.select_attachment_dialog_photo)
        ).bundle()

        navigation.navigate(R.id.confirmation_dialog, dialogBundle)
    }

    private fun onNoteAttachmentSelected() {
        pendingNoteSelection?.invoke()
        pendingNoteSelection = null
    }

    private fun onPhotoAttachmentSelected() {
        if (pendingPhotoSelection != null) {
            peekImage(pendingPhotoSelection!!)
        }
    }

    fun peekImage(pendingPhotoSelection: (Uri) -> Unit) {
        this.pendingPhotoSelection = pendingPhotoSelection

        val pickImageIntent = createGalleryIntent()
        pendingImageUri = createImageUri()
        val takePhotoIntent = createCameraIntent(pendingImageUri!!)

        val intentList: MutableList<Intent> = mutableListOf()
        combineIntents(intentList, pickImageIntent)
        combineIntents(intentList, takePhotoIntent)

        val chooserIntent: Intent?
        if (intentList.size > 0) {
            chooserIntent = Intent.createChooser(
                intentList.removeAt(intentList.size - 1),
                getString(R.string.chose_image_source)
            )
            chooserIntent.putExtra(
                Intent.EXTRA_INITIAL_INTENTS,
                intentList.toTypedArray()
            )

            startActivityForResult(chooserIntent, REQUEST_PICK_IMAGE)
        }
    }

    private fun subscribeForAttachmentDialogResult() {
        val navStack = navigation.currentBackStackEntry!!
        navStack.lifecycle.addObserver(LifecycleEventObserver { _, event ->
            if (event != Lifecycle.Event.ON_RESUME) {
                return@LifecycleEventObserver
            }

            if (navStack.savedStateHandle.contains(DIALOG_CLICK_EVENT)) {
                val click = navStack.savedStateHandle.get<DialogClickEvent>(DIALOG_CLICK_EVENT)!!
                if (click.dialogId != ATTACHMENT_DIALOG_ID) return@LifecycleEventObserver
                if (pendingPhotoSelection == null || pendingNoteSelection == null) return@LifecycleEventObserver

                navStack.savedStateHandle.remove<DialogClickEvent>(DIALOG_CLICK_EVENT)

                when (click.dialogBtn) {
                    DialogButton.POSITIVE -> onNoteAttachmentSelected()
                    DialogButton.NEGATIVE -> onPhotoAttachmentSelected()
                    else -> {/*  Nothing */
                    }
                }
            }
        })
    }

    protected fun showSnackbarWarning() {
        Snackbar.make(
            requireView().findViewById(R.id.snackbar_container) ?: requireView(),
            R.string.continue_with_empty_fields,
            Snackbar.LENGTH_LONG
        ).apply {
            animationMode = BaseTransientBottomBar.ANIMATION_MODE_FADE
            setAction(R.string.continue_action) { onNextListener.onNextClicked() }
            setActionTextColor(resources.getColor(R.color.orange, null))
        }.show()
    }

    protected fun showFullImage(view: View, photoItem: PhotoItem) {
        val bundle = bundleOf(PHOTO_ID to photoItem.photo._id.toHexString())
        val extra = FragmentNavigatorExtras(view to view.transitionName)
        navigation.navigate(R.id.action_tabsFragment_to_fullImageFragment, bundle, null, extra)
    }

    protected fun showEmptyFieldWarning(inputLayout: TextInputLayout) {
        inputLayout.errorIconDrawable = resources.getDrawable(R.drawable.ic_error_filled, null)

        inputLayout.defaultHintTextColor =  ContextCompat.getColorStateList(requireContext(), R.color.orange)
        inputLayout.hintTextColor =  ContextCompat.getColorStateList(requireContext(), R.color.text_input_hint)

        if (inputLayout.editText != null) {
            val colorStateList = ColorStateList.valueOf(ContextCompat.getColor(requireContext(), R.color.orange))
            ViewCompat.setBackgroundTintList(inputLayout.editText!!, colorStateList)
        }
    }

}

interface OnNextClickedListener {
    fun onNextClicked(isSubmitted: Boolean = false)
}

<|start_filename|>app/src/main/java/org/wildaid/ofish/ui/login/LoginViewModel.kt<|end_filename|>
package org.wildaid.ofish.ui.login

import android.app.Application
import android.util.Log
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import org.wildaid.ofish.Event
import org.wildaid.ofish.R
import org.wildaid.ofish.data.Repository
import org.wildaid.ofish.util.getString

class LoginViewModel(
        val repository: Repository,
        application: Application
) : AndroidViewModel(application) {

    private val _loginLiveData = MutableLiveData<LoginResult>()
    val loginLiveData: LiveData<LoginResult>
        get() = _loginLiveData

    private val _progressLiveData = MutableLiveData<Event<Boolean>>()
    val progressLiveData: LiveData<Event<Boolean>>
        get() = _progressLiveData

    fun login(userName: String, password: String) {
        repository.login(
                userName = userName,
                password = password,
                loginSuccess = {
                    Log.d("Login", "Logged in as $it")
                    _progressLiveData.value = Event(false)
                    _loginLiveData.value = LoginResult.LoginSuccess
                },
                loginError = {
                    Log.d("Login", "Cannot log in. Error ${it?.errorMessage}")
                    _progressLiveData.value = Event(false)
                    _loginLiveData.value = LoginResult.LoginError(getString(R.string.login_error))
                }
        )

        _progressLiveData.value = Event(true)
    }

    sealed class LoginResult {
        object LoginSuccess : LoginResult()
        class LoginError(val errorMsg: String?) : LoginResult()
    }
}

<|start_filename|>app/src/test/java/org/wildaid/ofish/ui/search/simple/SimpleSearchViewModelTest.kt<|end_filename|>
package org.wildaid.ofish.ui.search.simple

import android.os.Build
import androidx.test.core.app.ApplicationProvider
import io.mockk.MockKAnnotations
import io.mockk.every
import io.mockk.impl.annotations.MockK
import io.mockk.verify
import io.realm.RealmList
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.test.runBlockingTest
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config
import org.wildaid.ofish.data.Repository
import org.wildaid.ofish.data.report.MenuData
import org.wildaid.ofish.ui.search.base.BaseSearchType

@RunWith(RobolectricTestRunner::class)
@Config(sdk = [Build.VERSION_CODES.P])
class SimpleSearchViewModelTest {

    private lateinit var searchViewModel: SimpleSearchViewModel
    private val wholeList = listOf("CA", "UA", "UK", "MX", "US", "DE")
    private val filteredList = listOf("UA", "UK", "US","Other")

    @MockK
    private lateinit var mockedRepository: Repository

    @MockK
    private lateinit var searchData: SimpleSearchViewModel.SimpleSearchDataSource

    @Before
    fun setUp() {
        MockKAnnotations.init(this)

        searchViewModel =
            SimpleSearchViewModel(mockedRepository, ApplicationProvider.getApplicationContext())

        searchData = SimpleSearchViewModel(
            mockedRepository,
            ApplicationProvider.getApplicationContext()
        ).SimpleSearchDataSource(wholeList)

    }

    @Test
    fun getSourceFlagState() {
        every { mockedRepository.getFlagStates() } returns emptyList()

        val searchDataSource =
            searchViewModel.getDataSource(SimpleSearchFragment.SearchFlagState, null)

        assert(searchDataSource is SimpleSearchViewModel.SimpleSearchDataSource)

        verify { mockedRepository.getFlagStates() }
    }

    @Test
    fun getSourceEms() {
        every { mockedRepository.getMenuData() } returns MenuData().apply {
            emsTypes = RealmList()
        }

        val searchDataSource =
            searchViewModel.getDataSource(SimpleSearchFragment.SearchEms, null)

        assert(searchDataSource is SimpleSearchViewModel.SimpleSearchDataSource)

        verify { mockedRepository.getMenuData() }
    }

    @Test
    fun getSourceFishery() {
        every { mockedRepository.getMenuData() } returns MenuData().apply {
            fisheries = RealmList()
        }

        val searchDataSource =
            searchViewModel.getDataSource(SimpleSearchFragment.SearchFishery, null)

        assert(searchDataSource is SimpleSearchViewModel.SimpleSearchDataSource)

        verify { mockedRepository.getMenuData() }
    }

    @Test
    fun getSourceGear() {
        every { mockedRepository.getMenuData() } returns MenuData().apply {
            gear = RealmList()
        }

        val searchDataSource =
            searchViewModel.getDataSource(SimpleSearchFragment.SearchGear, null)

        assert(searchDataSource is SimpleSearchViewModel.SimpleSearchDataSource)

        verify { mockedRepository.getMenuData() }
    }

    @Test
    fun getSourceActivity() {
        every { mockedRepository.getMenuData() } returns MenuData().apply {
            activities = RealmList()
        }

        val searchDataSource =
            searchViewModel.getDataSource(SimpleSearchFragment.SearchActivity, null)

        assert(searchDataSource is SimpleSearchViewModel.SimpleSearchDataSource)

        verify { mockedRepository.getMenuData() }
    }

    @Test
    fun getSourceSpecies() {
        every { mockedRepository.getMenuData() } returns MenuData().apply {
            species = RealmList()
        }

        val searchDataSource =
            searchViewModel.getDataSource(SimpleSearchFragment.SearchSpecies, null)

        assert(searchDataSource is SimpleSearchViewModel.SimpleSearchDataSource)

        verify { mockedRepository.getMenuData() }
    }

    @Test(expected = IllegalArgumentException::class)
    fun getSourceIllegalArgument() {
        val invalidSearchEntity = BaseSearchType()
        every {
            searchViewModel.getDataSource(
                invalidSearchEntity,
                null
            )
        } throws IllegalArgumentException("Unsupported search type $invalidSearchEntity")

        val searchDataSource =
            searchViewModel.getDataSource(invalidSearchEntity, null)

        assert(searchDataSource is SimpleSearchViewModel.SimpleSearchDataSource)

        verify { mockedRepository.getMenuData() }
    }

    @Test
    fun initiateSimpleSearchDataSource() {
        runBlockingTest {
            val result = searchData.initiateData()
                .first()
            assert(result == wholeList)
        }
    }

    @Test
    fun filterSimpleSearchDataSource() {
        runBlockingTest {
            val result = searchData.applyFilter("U")
                .first()
            assert(result == filteredList)
        }
    }
}