DebugMenuFragment.kt 12.1 KB
Newer Older
1
package org.dpppt.android.app.debug.debugmenu
2
3
4
5
6
7
8

import android.annotation.SuppressLint
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.RadioGroup
9
import android.widget.Toast
10
11
import androidx.fragment.app.viewModels
import androidx.lifecycle.Observer
12
import androidx.navigation.fragment.findNavController
13
14
15
16
17
18
19
20
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.observers.DisposableSingleObserver
import io.reactivex.rxjava3.schedulers.Schedulers
import mobi.lab.mvvm.MvvmFragment
import org.dpppt.android.app.R
import org.dpppt.android.app.common.dialog.DialogUtil.show
import org.dpppt.android.app.common.dialog.ErrorGenericDialogFragment.Companion.newInstance
import org.dpppt.android.app.common.dialog.ErrorShareDeniedDialogFragment
21
22
import org.dpppt.android.app.common.util.FragmentBindingHolder
import org.dpppt.android.app.common.util.ViewBindingHolder
23
import org.dpppt.android.app.common.util.ViewModelFactory
24
import org.dpppt.android.app.common.util.exhaustive
25
import org.dpppt.android.app.databinding.FragmentDebugBinding
26
import org.dpppt.android.app.debug.TracingStatusWrapper
27
28
29
import org.dpppt.android.app.di.Injector
import org.dpppt.android.app.domain.entity.ConfirmInfectionRequest
import org.dpppt.android.app.domain.entity.enums.PatientPortalResult
30
import org.dpppt.android.app.domain.entity.enums.SettingsScrollDestination
31
import org.dpppt.android.app.domain.usecase.infection.request.InitConfirmInfectionRequestUseCase
32
import org.dpppt.android.app.logposting.LogPoster
33
34
import org.dpppt.android.app.notifications.Notifier
import org.dpppt.android.app.storage.SecureStorage
35
import org.dpppt.android.app.util.createErrorMessageFromThrowable
36
37
38
39
40
41
42
import org.dpppt.android.app.viewmodel.TracingViewModel
import org.dpppt.android.sdk.InfectionStatus
import org.dpppt.android.sdk.TracingStatus
import timber.log.Timber
import java.text.DateFormat
import java.util.Date
import javax.inject.Inject
43
44
import org.dpppt.android.app.BuildConfig as AppBuildConfig
import org.dpppt.android.sdk.BuildConfig as SdkBuildConfig
45

46
class DebugMenuFragment : MvvmFragment(), ViewBindingHolder<FragmentDebugBinding> by FragmentBindingHolder() {
47
48
49
50
51
52
53
54
55
56
57
58
59

    @Inject
    lateinit var notifier: Notifier

    @Inject
    lateinit var initConfirmInfectionRequestUseCase: InitConfirmInfectionRequestUseCase

    @Inject
    lateinit var secureStorage: SecureStorage

    @Inject
    lateinit var factory: ViewModelFactory

60
61
62
    @Inject
    lateinit var logPoster: LogPoster

63
    override val viewModel: DebugMenuViewModel by viewModels { factory }
64
65
66
67
68
69
    private val tracingViewModel: TracingViewModel by viewModels { factory }

    init {
        Injector.inject(this)
    }

70
71
    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
        return createBinding(FragmentDebugBinding.inflate(inflater), this)
72
73
74
75
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
76
        requireBinding().buttonBack.setOnClickListener { findNavController().popBackStack() }
77
78
79
        setupSdkViews()
        setupStateOptions()
        setupAppScreens()
80
        setupLogActions()
81
82
83
84
85
86
87
        initViewModel()
    }

    private fun initViewModel() {
        viewModel.action.onEachEvent(::handleAction)
    }

88
    private fun handleAction(action: DebugMenuViewModel.Action) {
89
        when (action) {
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
            is DebugMenuViewModel.Action.ShowLogAccessError -> showLogAccessError(action.error)
            is DebugMenuViewModel.Action.PostCurrentDevelopmentLog -> postCurrentDevelopmentLog()
            is DebugMenuViewModel.Action.ViewAllDevelopmentLogs -> viewAllDevelopmentLogs()
        }.exhaustive
    }

    private fun postCurrentDevelopmentLog() {
        try {
            logPoster.postCurrentDevelopmentLog(requireContext())
        } catch (error: Throwable) {
            Timber.e(error, "postCurrentDevelopmentLog")
            viewModel.handleLogAccessError(error)
        }
    }

    private fun viewAllDevelopmentLogs() {
        try {
            logPoster.viewAllDevelopmentLogs(requireContext())
        } catch (error: Throwable) {
            Timber.e(error, "viewAllDevelopmentLogs")
            viewModel.handleLogAccessError(error)
111
112
113
114
115
116
117
118
119
        }
    }

    private fun showLogAccessError(error: Throwable) {
        Toast.makeText(
            requireContext(),
            createErrorMessageFromThrowable(requireContext(), R.string.debug_error_accessing_logs, error),
            Toast.LENGTH_SHORT
        ).show()
120
121
122
123
124
125
126
127
    }

    private fun setupSdkViews() {
        tracingViewModel.tracingStatusLiveData.observe(viewLifecycleOwner, Observer(::setUpStatusText))
        tracingViewModel.tracingStatusLiveData.observe(viewLifecycleOwner, Observer(::setUpSdkStateText))
    }

    private fun setUpSdkStateText(status: TracingStatus) {
128
129
130
131
132
133
        showLastSynchronisation(status)
        showSdkStateSelfExposed(status)
        showSdkStateContactExposed(status)
        showDebugAppVersion()
        showDebugSdkVersion()
        showErrors(status)
134
135
136
    }

    private fun setUpStatusText(status: TracingStatus) {
137
        val statusText = requireBinding().textStatusTracing
138
        val isTracing = status.isTracingEnabled && status.errors.isEmpty()
139
140
        statusText.text = formatStatusString(isTracing)
        statusText.setBackgroundColor(
141
142
143
144
            resources.getColor(
                if (isTracing) R.color.dark_green else R.color.pink,
                null
            )
145
146
147
148
149
        )
    }

    @SuppressLint("NonConstantResourceId")
    private fun setupStateOptions() {
150
151
        val optionsGroup = requireBinding().debugStateOptionsGroup
        optionsGroup.setOnCheckedChangeListener { _: RadioGroup, checkedId: Int ->
152
153
154
155
156
157
158
            when (checkedId) {
                R.id.debug_state_option_none -> setDebugAppState(DebugAppState.NONE)
                R.id.debug_state_option_healthy -> setDebugAppState(DebugAppState.HEALTHY)
                R.id.debug_state_option_exposed -> setDebugAppState(DebugAppState.CONTACT_EXPOSED)
                R.id.debug_state_option_infected -> setDebugAppState(DebugAppState.REPORTED_EXPOSED)
            }
        }
159
        updateRadioGroup(optionsGroup)
160
161
162
163
164
165
166
167
168
169
170
171
172
    }

    private fun updateRadioGroup(optionsGroup: RadioGroup) {
        val preSetId: Int = when (getDebugAppState()) {
            DebugAppState.NONE -> R.id.debug_state_option_none
            DebugAppState.HEALTHY -> R.id.debug_state_option_healthy
            DebugAppState.CONTACT_EXPOSED -> R.id.debug_state_option_exposed
            DebugAppState.REPORTED_EXPOSED -> R.id.debug_state_option_infected
        }.exhaustive
        optionsGroup.check(preSetId)
    }

    private fun setupAppScreens() {
173
174
175
176
177
        requireBinding {
            buttonDebugStateErrorDialog.setOnClickListener { showDebugStateErrorDialog() }
            buttonDebugStateContactNotification.setOnClickListener { notifier.createNewContactNotification(requireActivity(), -1) }
            buttonDebugStatePreShareScreen.setOnClickListener { setupAndNavigateToPreShareScreen() }
            buttonDebugStateShareScreenError.setOnClickListener { showShareDeniedErrorDialog() }
178
            buttonDebugStateShareScreenSuccess.setOnClickListener { showShareSuccessScreen() }
179
            buttonDebugStateOpenSettingsAndScrollToCrossCountry.setOnClickListener { openSettingsAndScrollToCrossCountry() }
180
            buttonDebugStateCrossCountryPopUp.setOnClickListener { openCrossCountryPopUp() }
181
        }
182
183
    }

184
    private fun setupLogActions() {
185
186
187
188
        requireBinding {
            buttonDebugPostLogs.setOnClickListener { viewModel.postCurrentDevelopmentLogClicked() }
            buttonDebugViewLogs.setOnClickListener { viewModel.viewDevelopmentLogsClicked() }
        }
189
190
    }

191
    private fun showDebugStateErrorDialog() {
192
        show(
193
194
            this,
            newInstance(
195
196
                getString(R.string.error_domain_confirm_infection_request_not_found),
                getString(R.string.nav_back_home), null
197
198
            ),
            TAG_DIALOG_ERROR
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
        )
    }

    private fun showShareDeniedErrorDialog() {
        show(this, ErrorShareDeniedDialogFragment.newInstance(), TAG_DIALOG_SHARE_ERROR)
    }

    private fun setupAndNavigateToPreShareScreen() {
        // We need to make a fake infection confirmation state
        initConfirmInfectionRequestUseCase.execute(System.currentTimeMillis())
            .subscribeOn(Schedulers.io())
            .observeOn(AndroidSchedulers.mainThread())
            .subscribe(object : DisposableSingleObserver<ConfirmInfectionRequest>() {
                override fun onSuccess(confirmInfectionRequest: ConfirmInfectionRequest) {
                    // Navigate
214
                    findNavController().navigate(DebugMenuFragmentDirections.confirmInfectionResult(PatientPortalResult.Ok))
215
216
217
218
219
220
221
222
223
224
225
226
227
                }

                override fun onError(e: Throwable) {
                    // Things failed
                    Timber.wtf(e, "setupAndNavigateToPreShareScreen")
                }
            })
    }

    private fun formatStatusString(isTracing: Boolean): String {
        return getString(if (isTracing) R.string.debug_tracing_active_title else R.string.debug_android_tracing_error_title)
    }

228
    private fun showLastSynchronisation(status: TracingStatus) {
229
        val lastSyncDateUTC = status.lastSyncDate
230
231
        val lastSyncDateString =
            if (lastSyncDateUTC > 0) DATE_FORMAT_SYNC.format(Date(lastSyncDateUTC)) else getString(R.string.debug_sdk_state_synced_not_applicable)
232
        requireBinding().lastSynchronisationText.text = getString(R.string.debug_sdk_state_last_synced, lastSyncDateString)
233
234
235
    }

    private fun showSdkStateSelfExposed(status: TracingStatus) {
236
        requireBinding().reportedYourselfText.text = getString(
237
238
239
            R.string.debug_sdk_state_self_exposed,
            getBooleanDebugString(status.infectionStatus == InfectionStatus.INFECTED)
        )
240
241
242
    }

    private fun showSdkStateContactExposed(status: TracingStatus) {
243
        requireBinding().contactReportedText.text = getString(
244
245
246
            R.string.debug_sdk_state_contact_exposed,
            getBooleanDebugString(status.infectionStatus == InfectionStatus.EXPOSED)
        )
247
248
249
250
251
252
    }

    private fun getBooleanDebugString(value: Boolean): String {
        return getString(if (value) R.string.debug_sdk_state_boolean_true else R.string.debug_sdk_state_boolean_false)
    }

253
    private fun showDebugAppVersion() {
254
        requireBinding().appVersionText.text = getString(R.string.debug_app_version, AppBuildConfig.VERSION_NAME)
255
256
257
    }

    private fun showDebugSdkVersion() {
258
        requireBinding().sdkVersionText.text = getString(R.string.debug_sdk_version, SdkBuildConfig.VERSION_NAME)
259
260
261
    }

    private fun showErrors(status: TracingStatus) {
262
        val errors = status.errors ?: emptyList()
263
264
265
266
        requireBinding {
            sdkErrors.text = errors.joinToString(separator = "\n") { error -> error.toString() }
            sdkErrors.visibility = if (errors.isEmpty()) View.GONE else View.VISIBLE
        }
267
268
    }

269
270
271
272
273
274
275
276
    private fun getDebugAppState(): DebugAppState {
        return (tracingViewModel.tracingStatusInterface as TracingStatusWrapper).debugAppState
    }

    private fun setDebugAppState(debugAppState: DebugAppState) {
        (tracingViewModel.tracingStatusInterface as TracingStatusWrapper).setDebugAppState(context, debugAppState)
    }

277
278
279
280
281
    private fun openSettingsAndScrollToCrossCountry() {
        // Navigate
        findNavController().navigate(DebugMenuFragmentDirections.settingsCrossCountry(SettingsScrollDestination.CrossCountryItem))
    }

282
283
284
285
    private fun openCrossCountryPopUp() {
        findNavController().navigate(DebugMenuFragmentDirections.crossCountryPopUp())
    }

286
287
288
289
290
    private fun showShareSuccessScreen() {
        // Navigate
        findNavController().navigate(DebugMenuFragmentDirections.confirmInfectionResult(PatientPortalResult.DebugJustDone))
    }

291
292
293
294
295
296
    companion object {
        private const val TAG_DIALOG_ERROR = "DebugFragment.TAG_DIALOG_ERROR"
        private const val TAG_DIALOG_SHARE_ERROR = "DebugFragment.TAG_DIALOG_SHARE_ERROR"
        private val DATE_FORMAT_SYNC = DateFormat.getDateTimeInstance()
    }
}