DebugMenuFragment.kt 12.8 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
        setupSdkViews()
        setupStateOptions()
79
        setupDebugButtons()
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
        updateButtonStatesBasedOnTheDebugState()
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)
    }

173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
    private fun updateButtonStatesBasedOnTheDebugState() {
        when (getDebugAppState()) {
            DebugAppState.NONE -> enableButtonDebugDp3tSync(true)
            DebugAppState.HEALTHY -> enableButtonDebugDp3tSync(false)
            DebugAppState.CONTACT_EXPOSED -> enableButtonDebugDp3tSync(false)
            DebugAppState.REPORTED_EXPOSED -> enableButtonDebugDp3tSync(false)
        }.exhaustive
    }

    private fun enableButtonDebugDp3tSync(enabled: Boolean) {
        requireBinding {
            buttonDebugDp3tSync.isEnabled = enabled
        }
    }

    private fun setupDebugButtons() {
189
190
191
192
193
        requireBinding {
            buttonDebugStateErrorDialog.setOnClickListener { showDebugStateErrorDialog() }
            buttonDebugStateContactNotification.setOnClickListener { notifier.createNewContactNotification(requireActivity(), -1) }
            buttonDebugStatePreShareScreen.setOnClickListener { setupAndNavigateToPreShareScreen() }
            buttonDebugStateShareScreenError.setOnClickListener { showShareDeniedErrorDialog() }
194
            buttonDebugStateShareScreenSuccess.setOnClickListener { showShareSuccessScreen() }
195
            buttonDebugStateOpenSettingsAndScrollToCrossCountry.setOnClickListener { openSettingsAndScrollToCrossCountry() }
196
            buttonDebugStateCrossCountryPopUp.setOnClickListener { openCrossCountryPopUp() }
197
            buttonDebugDp3tSync.setOnClickListener { viewModel.startDP3TSyncClicked() }
198
        }
199
200
    }

201
    private fun setupLogActions() {
202
203
204
205
        requireBinding {
            buttonDebugPostLogs.setOnClickListener { viewModel.postCurrentDevelopmentLogClicked() }
            buttonDebugViewLogs.setOnClickListener { viewModel.viewDevelopmentLogsClicked() }
        }
206
207
    }

208
    private fun showDebugStateErrorDialog() {
209
        show(
210
211
            this,
            newInstance(
212
213
                getString(R.string.error_domain_confirm_infection_request_not_found),
                getString(R.string.nav_back_home), null
214
215
            ),
            TAG_DIALOG_ERROR
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
        )
    }

    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
231
                    findNavController().navigate(DebugMenuFragmentDirections.confirmInfectionResult(PatientPortalResult.Ok))
232
233
234
235
236
237
238
239
240
241
242
243
244
                }

                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)
    }

245
    private fun showLastSynchronisation(status: TracingStatus) {
246
        val lastSyncDateUTC = status.lastSyncDate
247
248
        val lastSyncDateString =
            if (lastSyncDateUTC > 0) DATE_FORMAT_SYNC.format(Date(lastSyncDateUTC)) else getString(R.string.debug_sdk_state_synced_not_applicable)
249
        requireBinding().lastSynchronisationText.text = getString(R.string.debug_sdk_state_last_synced, lastSyncDateString)
250
251
252
    }

    private fun showSdkStateSelfExposed(status: TracingStatus) {
253
        requireBinding().reportedYourselfText.text = getString(
254
255
256
            R.string.debug_sdk_state_self_exposed,
            getBooleanDebugString(status.infectionStatus == InfectionStatus.INFECTED)
        )
257
258
259
    }

    private fun showSdkStateContactExposed(status: TracingStatus) {
260
        requireBinding().contactReportedText.text = getString(
261
262
263
            R.string.debug_sdk_state_contact_exposed,
            getBooleanDebugString(status.infectionStatus == InfectionStatus.EXPOSED)
        )
264
265
266
267
268
269
    }

    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)
    }

270
    private fun showDebugAppVersion() {
271
        requireBinding().appVersionText.text = getString(R.string.debug_app_version, AppBuildConfig.VERSION_NAME)
272
273
274
    }

    private fun showDebugSdkVersion() {
275
        requireBinding().sdkVersionText.text = getString(R.string.debug_sdk_version, SdkBuildConfig.VERSION_NAME)
276
277
278
    }

    private fun showErrors(status: TracingStatus) {
279
        val errors = status.errors ?: emptyList()
280
281
282
283
        requireBinding {
            sdkErrors.text = errors.joinToString(separator = "\n") { error -> error.toString() }
            sdkErrors.visibility = if (errors.isEmpty()) View.GONE else View.VISIBLE
        }
284
285
    }

286
287
288
289
290
291
    private fun getDebugAppState(): DebugAppState {
        return (tracingViewModel.tracingStatusInterface as TracingStatusWrapper).debugAppState
    }

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

295
296
297
298
299
    private fun openSettingsAndScrollToCrossCountry() {
        // Navigate
        findNavController().navigate(DebugMenuFragmentDirections.settingsCrossCountry(SettingsScrollDestination.CrossCountryItem))
    }

300
301
302
303
    private fun openCrossCountryPopUp() {
        findNavController().navigate(DebugMenuFragmentDirections.crossCountryPopUp())
    }

304
305
306
307
308
    private fun showShareSuccessScreen() {
        // Navigate
        findNavController().navigate(DebugMenuFragmentDirections.confirmInfectionResult(PatientPortalResult.DebugJustDone))
    }

309
310
311
312
313
314
    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()
    }
}