1 /*
<lambda>null2  * Copyright (C) 2024 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.healthconnect.controller.tests.backuprestore
18 
19 import android.Manifest
20 import android.app.Activity
21 import android.app.Instrumentation.ActivityResult
22 import android.content.Context
23 import android.content.Intent
24 import android.health.connect.exportimport.ImportStatus.*
25 import android.net.Uri
26 import android.os.Bundle
27 import android.platform.test.annotations.DisableFlags
28 import android.platform.test.annotations.EnableFlags
29 import android.platform.test.flag.junit.SetFlagsRule
30 import androidx.lifecycle.MutableLiveData
31 import androidx.navigation.Navigation
32 import androidx.navigation.testing.TestNavHostController
33 import androidx.test.core.app.ActivityScenario
34 import androidx.test.espresso.Espresso.onView
35 import androidx.test.espresso.action.ViewActions.click
36 import androidx.test.espresso.action.ViewActions.scrollTo
37 import androidx.test.espresso.assertion.ViewAssertions.doesNotExist
38 import androidx.test.espresso.assertion.ViewAssertions.matches
39 import androidx.test.espresso.intent.Intents
40 import androidx.test.espresso.intent.Intents.intended
41 import androidx.test.espresso.intent.Intents.intending
42 import androidx.test.espresso.intent.matcher.IntentMatchers.hasComponent
43 import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
44 import androidx.test.espresso.matcher.ViewMatchers.isEnabled
45 import androidx.test.espresso.matcher.ViewMatchers.withText
46 import androidx.test.platform.app.InstrumentationRegistry
47 import com.android.healthconnect.controller.R
48 import com.android.healthconnect.controller.backuprestore.BackupAndRestoreSettingsFragment
49 import com.android.healthconnect.controller.exportimport.ExportSetupActivity
50 import com.android.healthconnect.controller.exportimport.ImportFlowActivity
51 import com.android.healthconnect.controller.exportimport.api.DocumentProviders
52 import com.android.healthconnect.controller.exportimport.api.ExportFrequency
53 import com.android.healthconnect.controller.exportimport.api.ExportSettings
54 import com.android.healthconnect.controller.exportimport.api.ExportSettingsViewModel
55 import com.android.healthconnect.controller.exportimport.api.ExportStatusViewModel
56 import com.android.healthconnect.controller.exportimport.api.ImportFlowViewModel
57 import com.android.healthconnect.controller.exportimport.api.ImportStatusViewModel
58 import com.android.healthconnect.controller.exportimport.api.ImportUiState
59 import com.android.healthconnect.controller.exportimport.api.ImportUiStatus
60 import com.android.healthconnect.controller.exportimport.api.ScheduledExportUiState
61 import com.android.healthconnect.controller.exportimport.api.ScheduledExportUiStatus
62 import com.android.healthconnect.controller.tests.TestActivity
63 import com.android.healthconnect.controller.tests.utils.ClearTimeFormatRule
64 import com.android.healthconnect.controller.tests.utils.InstantTaskExecutorRule
65 import com.android.healthconnect.controller.tests.utils.NOW
66 import com.android.healthconnect.controller.tests.utils.TestTimeSource
67 import com.android.healthconnect.controller.tests.utils.di.FakeDeviceInfoUtils
68 import com.android.healthconnect.controller.tests.utils.launchFragment
69 import com.android.healthconnect.controller.utils.DeviceInfoUtils
70 import com.android.healthconnect.controller.utils.DeviceInfoUtilsModule
71 import com.android.healthconnect.controller.utils.ToastManager
72 import com.android.healthconnect.controller.utils.ToastManagerModule
73 import com.android.healthconnect.controller.utils.logging.BackupAndRestoreElement
74 import com.android.healthconnect.controller.utils.logging.HealthConnectLogger
75 import com.android.healthfitness.flags.Flags
76 import com.google.common.truth.Truth.assertThat
77 import dagger.hilt.android.testing.BindValue
78 import dagger.hilt.android.testing.HiltAndroidRule
79 import dagger.hilt.android.testing.HiltAndroidTest
80 import dagger.hilt.android.testing.UninstallModules
81 import java.time.Instant
82 import java.time.ZoneId
83 import java.util.Locale
84 import java.util.TimeZone
85 import kotlinx.coroutines.Dispatchers
86 import kotlinx.coroutines.ExperimentalCoroutinesApi
87 import kotlinx.coroutines.test.StandardTestDispatcher
88 import kotlinx.coroutines.test.resetMain
89 import kotlinx.coroutines.test.runTest
90 import kotlinx.coroutines.test.setMain
91 import org.hamcrest.Matchers.not
92 import org.junit.After
93 import org.junit.Before
94 import org.junit.Rule
95 import org.junit.Test
96 import org.mockito.ArgumentMatchers
97 import org.mockito.Mockito
98 import org.mockito.MockitoAnnotations
99 import org.mockito.kotlin.eq
100 import org.mockito.kotlin.mock
101 import org.mockito.kotlin.reset
102 import org.mockito.kotlin.verify
103 import org.mockito.kotlin.whenever
104 
105 @OptIn(ExperimentalCoroutinesApi::class)
106 @HiltAndroidTest
107 @UninstallModules(DeviceInfoUtilsModule::class, ToastManagerModule::class)
108 class BackupAndRestoreSettingsFragmentTest {
109 
110     companion object {
111         private const val TEST_EXPORT_PERIOD_IN_DAYS = 1
112         private const val TEST_LAST_EXPORT_APP_NAME = "Drive"
113         private const val TEST_LAST_EXPORT_FILE_NAME = "healthconnect.zip"
114         private const val TEST_LAST_IMPORT_URI = "content://com.android.documents.testFile"
115         private const val IMPORT_FILE_URI_KEY = "selectedUri"
116         private val TEST_LAST_IMPORT_COMPLETION_TIME = Instant.parse("2022-09-20T07:06:05.432Z")
117     }
118 
119     private val testDispatcher = StandardTestDispatcher()
120 
121     @get:Rule val hiltRule = HiltAndroidRule(this)
122     @get:Rule val clearTimeFormatRule = ClearTimeFormatRule()
123     @get:Rule val instantTaskExecutorRule = InstantTaskExecutorRule()
124     @get:Rule val setFlagsRule = SetFlagsRule()
125 
126     // TODO: b/348591669 - Replace the mock with a fake and investigate the UI tests.
127     @BindValue
128     val exportSettingsViewModel: ExportSettingsViewModel =
129         Mockito.mock(ExportSettingsViewModel::class.java)
130 
131     @BindValue
132     val exportStatusViewModel: ExportStatusViewModel =
133         Mockito.mock(ExportStatusViewModel::class.java)
134 
135     @BindValue
136     val importStatusViewModel: ImportStatusViewModel =
137         Mockito.mock(ImportStatusViewModel::class.java)
138 
139     @BindValue
140     val importFlowViewModel: ImportFlowViewModel = Mockito.mock(ImportFlowViewModel::class.java)
141 
142     @BindValue var toastManager: ToastManager = mock()
143     @BindValue val timeSource = TestTimeSource
144     @BindValue val healthConnectLogger: HealthConnectLogger = mock()
145     @BindValue val deviceInfoUtils: DeviceInfoUtils = FakeDeviceInfoUtils()
146     private val fakeDeviceInfoUtils = deviceInfoUtils as FakeDeviceInfoUtils
147 
148     private var previousDefaultTimeZone: TimeZone? = null
149     private var previousLocale: Locale? = null
150 
151     private lateinit var navHostController: TestNavHostController
152     private lateinit var context: Context
153 
154     @Before
155     fun setup() {
156         MockitoAnnotations.initMocks(this)
157         Dispatchers.setMain(testDispatcher)
158 
159         previousDefaultTimeZone = TimeZone.getDefault()
160         previousLocale = Locale.getDefault()
161 
162         Locale.setDefault(Locale.US)
163         TimeZone.setDefault(TimeZone.getTimeZone(ZoneId.of("UTC")))
164 
165         hiltRule.inject()
166         // Required for aconfig flag reading for tests run on pre V devices
167         InstrumentationRegistry.getInstrumentation()
168             .getUiAutomation()
169             .adoptShellPermissionIdentity(Manifest.permission.READ_DEVICE_CONFIG)
170         context = InstrumentationRegistry.getInstrumentation().context
171         navHostController = TestNavHostController(context)
172 
173         Intents.init()
174 
175         whenever(importStatusViewModel.storedImportStatus).then {
176             MutableLiveData(
177                 ImportUiStatus.WithData(
178                     ImportUiState(
179                         dataImportError = ImportUiState.DataImportError.DATA_IMPORT_ERROR_NONE,
180                         isImportOngoing = false,
181                     )
182                 )
183             )
184         }
185         whenever(exportStatusViewModel.storedScheduledExportStatus).then {
186             MutableLiveData(
187                 ScheduledExportUiStatus.WithData(
188                     ScheduledExportUiState(
189                         dataExportError =
190                             ScheduledExportUiState.DataExportError.DATA_EXPORT_ERROR_NONE,
191                         periodInDays = 0,
192                         lastExportFileName = TEST_LAST_EXPORT_FILE_NAME,
193                         lastExportAppName = TEST_LAST_EXPORT_APP_NAME,
194                     )
195                 )
196             )
197         }
198         whenever(importFlowViewModel.lastImportCompletionInstant).then {
199             MutableLiveData(TEST_LAST_IMPORT_COMPLETION_TIME)
200         }
201     }
202 
203     @After
204     fun tearDown() {
205         Dispatchers.resetMain()
206         reset(healthConnectLogger)
207         fakeDeviceInfoUtils.reset()
208         Intents.release()
209 
210         TimeZone.setDefault(previousDefaultTimeZone)
211         previousLocale?.let { locale -> Locale.setDefault(locale) }
212     }
213 
214     @Test
215     @DisableFlags(Flags.FLAG_EXPORT_IMPORT_FAST_FOLLOW)
216     fun backupAndRestoreSettingsFragmentInit_showsFragmentCorrectly() {
217         whenever(exportStatusViewModel.storedScheduledExportStatus).then {
218             MutableLiveData(
219                 ScheduledExportUiStatus.WithData(
220                     ScheduledExportUiState(
221                         NOW,
222                         ScheduledExportUiState.DataExportError.DATA_EXPORT_ERROR_NONE,
223                         TEST_EXPORT_PERIOD_IN_DAYS,
224                         TEST_LAST_EXPORT_FILE_NAME,
225                         TEST_LAST_EXPORT_APP_NAME,
226                     )
227                 )
228             )
229         }
230         whenever(exportSettingsViewModel.storedExportSettings).then {
231             MutableLiveData(ExportSettings.WithData(ExportFrequency.EXPORT_FREQUENCY_WEEKLY))
232         }
233 
234         launchFragment<BackupAndRestoreSettingsFragment>(Bundle())
235 
236         onView(withText("Scheduled export")).check(matches(isDisplayed()))
237         onView(withText("Import data")).check(matches(isDisplayed()))
238         onView(withText("Restore data from a previously exported file"))
239             .check(matches(isDisplayed()))
240         onView(withText("Last export: Oct 20, 7:06 AM")).check(matches(isDisplayed()))
241         onView(withText("Export lets you save your data so you can transfer it to a new phone"))
242             .check(matches(isDisplayed()))
243         onView(withText("About backup and restore")).check(matches(isDisplayed()))
244         onView(withText("Drive • healthconnect.zip")).check(matches(isDisplayed()))
245     }
246 
247     @Test
248     fun backupAndRestoreSettingsFragment_impressionsLogged() {
249         whenever(exportSettingsViewModel.storedExportSettings).then {
250             MutableLiveData(ExportSettings.WithData(ExportFrequency.EXPORT_FREQUENCY_DAILY))
251         }
252 
253         launchFragment<BackupAndRestoreSettingsFragment>(Bundle())
254 
255         verify(healthConnectLogger).logPageImpression()
256         verify(healthConnectLogger).logImpression(BackupAndRestoreElement.SCHEDULED_EXPORT_BUTTON)
257         verify(healthConnectLogger).logImpression(BackupAndRestoreElement.RESTORE_DATA_BUTTON)
258     }
259 
260     @Test
261     fun backupAndRestoreSettingsFragment_withNoLastSuccessfulDate_doesNotShowLastExportTime() {
262         whenever(exportSettingsViewModel.storedExportSettings).then {
263             MutableLiveData(ExportSettings.WithData(ExportFrequency.EXPORT_FREQUENCY_WEEKLY))
264         }
265         whenever(exportStatusViewModel.storedScheduledExportStatus).then {
266             MutableLiveData(
267                 ScheduledExportUiStatus.WithData(
268                     ScheduledExportUiState(
269                         lastSuccessfulExportTime = null,
270                         dataExportError =
271                             ScheduledExportUiState.DataExportError.DATA_EXPORT_ERROR_NONE,
272                         periodInDays = TEST_EXPORT_PERIOD_IN_DAYS,
273                     )
274                 )
275             )
276         }
277         launchFragment<BackupAndRestoreSettingsFragment>(Bundle())
278 
279         onView(withText("Last export: Oct 20, 7:06 AM")).check(doesNotExist())
280     }
281 
282     @Test
283     @EnableFlags(Flags.FLAG_EXPORT_IMPORT_FAST_FOLLOW)
284     fun backupAndRestoreSettingsFragment_withNoLastExport_showsCorrectMessage() {
285         whenever(exportSettingsViewModel.storedExportSettings).then {
286             MutableLiveData(ExportSettings.WithData(ExportFrequency.EXPORT_FREQUENCY_WEEKLY))
287         }
288         whenever(exportStatusViewModel.storedScheduledExportStatus).then {
289             MutableLiveData(
290                 ScheduledExportUiStatus.WithData(
291                     ScheduledExportUiState(
292                         lastSuccessfulExportTime = null,
293                         lastFailedExportTime = null,
294                         dataExportError =
295                             ScheduledExportUiState.DataExportError.DATA_EXPORT_ERROR_NONE,
296                         periodInDays = TEST_EXPORT_PERIOD_IN_DAYS,
297                     )
298                 )
299             )
300         }
301         launchFragment<BackupAndRestoreSettingsFragment>(Bundle())
302 
303         onView(withText("Last export: none")).check(matches(isDisplayed()))
304     }
305 
306     @Test
307     @EnableFlags(Flags.FLAG_EXPORT_IMPORT_FAST_FOLLOW)
308     fun backupAndRestoreSettingsFragment_lastExportWithin1Minute_showsLastExportAsNow() {
309         whenever(exportSettingsViewModel.storedExportSettings).then {
310             MutableLiveData(ExportSettings.WithData(ExportFrequency.EXPORT_FREQUENCY_WEEKLY))
311         }
312         whenever(exportStatusViewModel.storedScheduledExportStatus).then {
313             MutableLiveData(
314                 ScheduledExportUiStatus.WithData(
315                     ScheduledExportUiState(
316                         // The fake 'now' is 2022-10-20T07:06:05.432Z.
317                         lastSuccessfulExportTime = Instant.parse("2022-10-20T07:05:35.432Z"),
318                         dataExportError =
319                             ScheduledExportUiState.DataExportError.DATA_EXPORT_ERROR_NONE,
320                         periodInDays = TEST_EXPORT_PERIOD_IN_DAYS,
321                     )
322                 )
323             )
324         }
325         launchFragment<BackupAndRestoreSettingsFragment>(Bundle())
326 
327         onView(withText("Last export: now")).check(matches(isDisplayed()))
328     }
329 
330     @Test
331     @EnableFlags(Flags.FLAG_EXPORT_IMPORT_FAST_FOLLOW)
332     fun backupAndRestoreSettingsFragment_lastExportBetween1MinAnd1Hour_showsLastExportAsXMinutesAgo() {
333         whenever(exportSettingsViewModel.storedExportSettings).then {
334             MutableLiveData(ExportSettings.WithData(ExportFrequency.EXPORT_FREQUENCY_WEEKLY))
335         }
336         whenever(exportStatusViewModel.storedScheduledExportStatus).then {
337             MutableLiveData(
338                 ScheduledExportUiStatus.WithData(
339                     ScheduledExportUiState(
340                         // The fake 'now' is 2022-10-20T07:06:05.432Z.
341                         lastSuccessfulExportTime = Instant.parse("2022-10-20T06:36:05.432Z"),
342                         dataExportError =
343                             ScheduledExportUiState.DataExportError.DATA_EXPORT_ERROR_NONE,
344                         periodInDays = TEST_EXPORT_PERIOD_IN_DAYS,
345                     )
346                 )
347             )
348         }
349         launchFragment<BackupAndRestoreSettingsFragment>(Bundle())
350 
351         onView(withText("Last export: 30 minutes ago")).check(matches(isDisplayed()))
352     }
353 
354     @Test
355     @EnableFlags(Flags.FLAG_EXPORT_IMPORT_FAST_FOLLOW)
356     fun backupAndRestoreSettingsFragment_lastExportAt1MinAgo_showsLastExportAs1MinuteAgo() {
357         whenever(exportSettingsViewModel.storedExportSettings).then {
358             MutableLiveData(ExportSettings.WithData(ExportFrequency.EXPORT_FREQUENCY_WEEKLY))
359         }
360         whenever(exportStatusViewModel.storedScheduledExportStatus).then {
361             MutableLiveData(
362                 ScheduledExportUiStatus.WithData(
363                     ScheduledExportUiState(
364                         // The fake 'now' is 2022-10-20T07:06:05.432Z.
365                         lastSuccessfulExportTime = Instant.parse("2022-10-20T07:05:05.432Z"),
366                         dataExportError =
367                             ScheduledExportUiState.DataExportError.DATA_EXPORT_ERROR_NONE,
368                         periodInDays = TEST_EXPORT_PERIOD_IN_DAYS,
369                     )
370                 )
371             )
372         }
373         launchFragment<BackupAndRestoreSettingsFragment>(Bundle())
374 
375         onView(withText("Last export: 1 minute ago")).check(matches(isDisplayed()))
376     }
377 
378     @Test
379     @EnableFlags(Flags.FLAG_EXPORT_IMPORT_FAST_FOLLOW)
380     fun backupAndRestoreSettingsFragment_lastExportBetween1HourAnd1Day_showsLastExportAsXHoursAgo() {
381         whenever(exportSettingsViewModel.storedExportSettings).then {
382             MutableLiveData(ExportSettings.WithData(ExportFrequency.EXPORT_FREQUENCY_WEEKLY))
383         }
384         whenever(exportStatusViewModel.storedScheduledExportStatus).then {
385             MutableLiveData(
386                 ScheduledExportUiStatus.WithData(
387                     ScheduledExportUiState(
388                         // The fake 'now' is 2022-10-20T07:06:05.432Z.
389                         lastSuccessfulExportTime = Instant.parse("2022-10-19T08:06:05.432Z"),
390                         dataExportError =
391                             ScheduledExportUiState.DataExportError.DATA_EXPORT_ERROR_NONE,
392                         periodInDays = TEST_EXPORT_PERIOD_IN_DAYS,
393                     )
394                 )
395             )
396         }
397         launchFragment<BackupAndRestoreSettingsFragment>(Bundle())
398 
399         onView(withText("Last export: 23 hours ago")).check(matches(isDisplayed()))
400     }
401 
402     @Test
403     @EnableFlags(Flags.FLAG_EXPORT_IMPORT_FAST_FOLLOW)
404     fun backupAndRestoreSettingsFragment_lastExportAt1HourAgo_showsLastExportAs1HourAgo() {
405         whenever(exportSettingsViewModel.storedExportSettings).then {
406             MutableLiveData(ExportSettings.WithData(ExportFrequency.EXPORT_FREQUENCY_WEEKLY))
407         }
408         whenever(exportStatusViewModel.storedScheduledExportStatus).then {
409             MutableLiveData(
410                 ScheduledExportUiStatus.WithData(
411                     ScheduledExportUiState(
412                         // The fake 'now' is 2022-10-20T07:06:05.432Z.
413                         lastSuccessfulExportTime = Instant.parse("2022-10-20T06:06:05.432Z"),
414                         dataExportError =
415                             ScheduledExportUiState.DataExportError.DATA_EXPORT_ERROR_NONE,
416                         periodInDays = TEST_EXPORT_PERIOD_IN_DAYS,
417                     )
418                 )
419             )
420         }
421         launchFragment<BackupAndRestoreSettingsFragment>(Bundle())
422 
423         onView(withText("Last export: 1 hour ago")).check(matches(isDisplayed()))
424     }
425 
426     @Test
427     @EnableFlags(Flags.FLAG_EXPORT_IMPORT_FAST_FOLLOW)
428     fun backupAndRestoreSettingsFragment_lastExportBetween1DayAnd1Year_showsCorrectDate() {
429         whenever(exportSettingsViewModel.storedExportSettings).then {
430             MutableLiveData(ExportSettings.WithData(ExportFrequency.EXPORT_FREQUENCY_WEEKLY))
431         }
432         whenever(exportStatusViewModel.storedScheduledExportStatus).then {
433             MutableLiveData(
434                 ScheduledExportUiStatus.WithData(
435                     ScheduledExportUiState(
436                         // The fake 'now' is 2022-10-20T07:06:05.432Z.
437                         lastSuccessfulExportTime = Instant.parse("2021-12-20T01:06:05.432Z"),
438                         dataExportError =
439                             ScheduledExportUiState.DataExportError.DATA_EXPORT_ERROR_NONE,
440                         periodInDays = TEST_EXPORT_PERIOD_IN_DAYS,
441                     )
442                 )
443             )
444         }
445         launchFragment<BackupAndRestoreSettingsFragment>(Bundle())
446 
447         onView(withText("Last export: Dec 20, 1:06 AM")).check(matches(isDisplayed()))
448     }
449 
450     @Test
451     @EnableFlags(Flags.FLAG_EXPORT_IMPORT_FAST_FOLLOW)
452     fun backupAndRestoreSettingsFragment_lastExportAfter1Year_showsCorrectDate() {
453         whenever(exportSettingsViewModel.storedExportSettings).then {
454             MutableLiveData(ExportSettings.WithData(ExportFrequency.EXPORT_FREQUENCY_WEEKLY))
455         }
456         whenever(exportStatusViewModel.storedScheduledExportStatus).then {
457             MutableLiveData(
458                 ScheduledExportUiStatus.WithData(
459                     ScheduledExportUiState(
460                         // The fake 'now' is 2022-10-20T07:06:05.432Z.
461                         lastSuccessfulExportTime = Instant.parse("2021-10-20T07:06:05.432Z"),
462                         dataExportError =
463                             ScheduledExportUiState.DataExportError.DATA_EXPORT_ERROR_NONE,
464                         periodInDays = TEST_EXPORT_PERIOD_IN_DAYS,
465                     )
466                 )
467             )
468         }
469         launchFragment<BackupAndRestoreSettingsFragment>(Bundle())
470 
471         onView(withText("Last export: October 20, 2021")).check(matches(isDisplayed()))
472     }
473 
474     @Test
475     fun backupAndRestoreSettingsFragment_whenOnlyAppNameIsAvailable_showsAppName() {
476         whenever(exportSettingsViewModel.storedExportSettings).then {
477             MutableLiveData(ExportSettings.WithData(ExportFrequency.EXPORT_FREQUENCY_WEEKLY))
478         }
479         whenever(exportStatusViewModel.storedScheduledExportStatus).then {
480             MutableLiveData(
481                 ScheduledExportUiStatus.WithData(
482                     ScheduledExportUiState(
483                         lastSuccessfulExportTime = NOW,
484                         dataExportError =
485                             ScheduledExportUiState.DataExportError.DATA_EXPORT_ERROR_NONE,
486                         periodInDays = TEST_EXPORT_PERIOD_IN_DAYS,
487                         lastExportAppName = TEST_LAST_EXPORT_APP_NAME,
488                     )
489                 )
490             )
491         }
492         launchFragment<BackupAndRestoreSettingsFragment>(Bundle())
493 
494         onView(withText("Drive")).check(matches(isDisplayed()))
495     }
496 
497     @Test
498     fun backupAndRestoreSettingsFragment_whenOnlyFileNameIsAvailable_showsFileName() {
499         whenever(exportSettingsViewModel.storedExportSettings).then {
500             MutableLiveData(ExportSettings.WithData(ExportFrequency.EXPORT_FREQUENCY_WEEKLY))
501         }
502         whenever(exportStatusViewModel.storedScheduledExportStatus).then {
503             MutableLiveData(
504                 ScheduledExportUiStatus.WithData(
505                     ScheduledExportUiState(
506                         lastSuccessfulExportTime = NOW,
507                         dataExportError =
508                             ScheduledExportUiState.DataExportError.DATA_EXPORT_ERROR_NONE,
509                         periodInDays = TEST_EXPORT_PERIOD_IN_DAYS,
510                         lastExportFileName = TEST_LAST_EXPORT_FILE_NAME,
511                     )
512                 )
513             )
514         }
515         launchFragment<BackupAndRestoreSettingsFragment>(Bundle())
516 
517         onView(withText("healthconnect.zip")).check(matches(isDisplayed()))
518     }
519 
520     @Test
521     fun backupAndRestoreSettingsFragment_whenImportStarted_importPreferenceDisabled() = runTest {
522         whenever(exportSettingsViewModel.storedExportSettings).then {
523             MutableLiveData(ExportSettings.WithData(ExportFrequency.EXPORT_FREQUENCY_NEVER))
524         }
525         whenever(exportSettingsViewModel.documentProviders).then {
526             MutableLiveData(DocumentProviders.WithData(listOf()))
527         }
528 
529         val expectedResult =
530             ActivityResult(
531                 Activity.RESULT_OK,
532                 Intent().putExtra(IMPORT_FILE_URI_KEY, TEST_LAST_IMPORT_URI),
533             )
534         intending(hasComponent(ImportFlowActivity::class.java.name)).respondWith(expectedResult)
535 
536         launchFragment<BackupAndRestoreSettingsFragment>(Bundle())
537 
538         onView(withText("Import data")).check(matches(isEnabled()))
539         onView(withText("Import data")).perform(click())
540         onView(withText("Import data")).check(matches(not(isEnabled())))
541 
542         intended(hasComponent(ImportFlowActivity::class.java.name))
543         verify(importFlowViewModel).triggerImportOfSelectedFile(Uri.parse(TEST_LAST_IMPORT_URI))
544     }
545 
546     @Test
547     fun backupAndRestoreSettingsFragment_whenImportTriggered_importStatusToastsShown() {
548         whenever(exportSettingsViewModel.storedExportSettings).then {
549             MutableLiveData(ExportSettings.WithData(ExportFrequency.EXPORT_FREQUENCY_NEVER))
550         }
551         whenever(exportSettingsViewModel.documentProviders).then {
552             MutableLiveData(DocumentProviders.WithData(listOf()))
553         }
554         whenever(importFlowViewModel.setLastCompletionInstant(Instant.now())).then {
555             MutableLiveData(Instant.now())
556         }
557 
558         val expectedInProgressMessage: Int = R.string.import_in_progress_toast_text
559         val expectedCompleteMessage: Int = R.string.import_complete_toast_text
560 
561         val expectedResult =
562             ActivityResult(
563                 Activity.RESULT_OK,
564                 Intent().putExtra(IMPORT_FILE_URI_KEY, TEST_LAST_IMPORT_URI),
565             )
566         intending(hasComponent(ImportFlowActivity::class.java.name)).respondWith(expectedResult)
567 
568         val scenario: ActivityScenario<TestActivity> =
569             launchFragment<BackupAndRestoreSettingsFragment>(Bundle())
570 
571         onView(withText("Import data")).perform(click())
572 
573         intended(hasComponent(ImportFlowActivity::class.java.name))
574         verify(importFlowViewModel).triggerImportOfSelectedFile(Uri.parse(TEST_LAST_IMPORT_URI))
575 
576         scenario.onActivity { activity: TestActivity ->
577             verify(toastManager)
578                 .showToast(eq(activity), eq(expectedInProgressMessage), ArgumentMatchers.anyInt())
579             verify(toastManager)
580                 .showToast(eq(activity), eq(expectedCompleteMessage), ArgumentMatchers.anyInt())
581         }
582     }
583 
584     @Test
585     fun backupAndRestoreSettingsFragment_clicksImportData_navigatesToImportFlowActivity() {
586         whenever(exportSettingsViewModel.storedExportSettings).then {
587             MutableLiveData(ExportSettings.WithData(ExportFrequency.EXPORT_FREQUENCY_NEVER))
588         }
589         whenever(exportSettingsViewModel.documentProviders).then {
590             MutableLiveData(DocumentProviders.WithData(listOf()))
591         }
592 
593         val expectedResult =
594             ActivityResult(
595                 Activity.RESULT_OK,
596                 Intent().putExtra(IMPORT_FILE_URI_KEY, TEST_LAST_IMPORT_URI),
597             )
598         intending(hasComponent(ImportFlowActivity::class.java.name)).respondWith(expectedResult)
599 
600         launchFragment<BackupAndRestoreSettingsFragment>(Bundle())
601 
602         onView(withText("Import data")).check(matches(isDisplayed()))
603         onView(withText("Import data")).check(matches(isEnabled()))
604         onView(withText("Import data")).perform(click())
605 
606         intended(hasComponent(ImportFlowActivity::class.java.name))
607 
608         verify(healthConnectLogger).logInteraction(BackupAndRestoreElement.RESTORE_DATA_BUTTON)
609     }
610 
611     @Test
612     fun backupAndRestoreSettingsFragment_clicksScheduledExportWhenItIsOff_navigatesToExportSetupActivity() {
613         whenever(exportSettingsViewModel.storedExportSettings).then {
614             MutableLiveData(ExportSettings.WithData(ExportFrequency.EXPORT_FREQUENCY_NEVER))
615         }
616         launchFragment<BackupAndRestoreSettingsFragment>(Bundle())
617         val expectedResult = ActivityResult(Activity.RESULT_OK, Intent())
618         intending(hasComponent(ExportSetupActivity::class.java.name)).respondWith(expectedResult)
619 
620         onView(withText("Scheduled export")).perform(click())
621 
622         intended(hasComponent(ExportSetupActivity::class.java.name))
623         verify(healthConnectLogger).logInteraction(BackupAndRestoreElement.SCHEDULED_EXPORT_BUTTON)
624     }
625 
626     @Test
627     fun backupAndRestoreSettingsFragment_clicksScheduledExportWhenItIsOn_navigatesToScheduledExportFragment() {
628         whenever(exportSettingsViewModel.storedExportSettings).then {
629             MutableLiveData(ExportSettings.WithData(ExportFrequency.EXPORT_FREQUENCY_DAILY))
630         }
631         launchFragment<BackupAndRestoreSettingsFragment>(Bundle()) {
632             navHostController.setGraph(R.navigation.nav_graph)
633             navHostController.setCurrentDestination(R.id.backupAndRestoreSettingsFragment)
634             Navigation.setViewNavController(this.requireView(), navHostController)
635         }
636 
637         onView(withText("Scheduled export")).perform(click())
638 
639         assertThat(navHostController.currentDestination?.id).isEqualTo(R.id.scheduledExportFragment)
640     }
641 
642     @Test
643     @EnableFlags(Flags.FLAG_EXPORT_IMPORT_FAST_FOLLOW)
644     fun backupAndRestoreSettingsFragment_whenExportSetupCompletes_toastShown() {
645         whenever(exportSettingsViewModel.storedExportSettings).then {
646             MutableLiveData(ExportSettings.WithData(ExportFrequency.EXPORT_FREQUENCY_NEVER))
647         }
648         whenever(exportSettingsViewModel.documentProviders).then {
649             MutableLiveData(DocumentProviders.WithData(listOf()))
650         }
651         whenever(importFlowViewModel.setLastCompletionInstant(Instant.now())).then {
652             MutableLiveData(Instant.now())
653         }
654 
655         val expectedMessage: Int = R.string.scheduled_export_on_toast_text
656 
657         val expectedResult = ActivityResult(Activity.RESULT_OK, Intent())
658         intending(hasComponent(ExportSetupActivity::class.java.name)).respondWith(expectedResult)
659 
660         val scenario: ActivityScenario<TestActivity> =
661             launchFragment<BackupAndRestoreSettingsFragment>(Bundle())
662 
663         onView(withText("Scheduled export")).perform(click())
664         intended(hasComponent(ExportSetupActivity::class.java.name))
665 
666         scenario.onActivity { activity: TestActivity ->
667             verify(toastManager)
668                 .showToast(eq(activity), eq(expectedMessage), ArgumentMatchers.anyInt())
669         }
670     }
671 
672     @Test
673     fun backupAndRestoreSettingsFragment_whenExportFrequencyIsNever_showsCorrectSummary() {
674         whenever(exportSettingsViewModel.storedExportSettings).then {
675             MutableLiveData(ExportSettings.WithData(ExportFrequency.EXPORT_FREQUENCY_NEVER))
676         }
677         launchFragment<BackupAndRestoreSettingsFragment>(Bundle())
678 
679         onView(withText("Off")).check(matches(isDisplayed()))
680     }
681 
682     @Test
683     fun backupAndRestoreSettingsFragment_whenExportFrequencyIsDaily_showsCorrectSummary() {
684         whenever(exportSettingsViewModel.storedExportSettings).then {
685             MutableLiveData(ExportSettings.WithData(ExportFrequency.EXPORT_FREQUENCY_DAILY))
686         }
687         launchFragment<BackupAndRestoreSettingsFragment>(Bundle())
688 
689         onView(withText("On • Daily")).check(matches(isDisplayed()))
690     }
691 
692     @Test
693     fun backupAndRestoreSettingsFragment_whenExportFrequencyIsWeekly_showsCorrectSummary() {
694         whenever(exportSettingsViewModel.storedExportSettings).then {
695             MutableLiveData(ExportSettings.WithData(ExportFrequency.EXPORT_FREQUENCY_WEEKLY))
696         }
697         launchFragment<BackupAndRestoreSettingsFragment>(Bundle())
698 
699         onView(withText("On • Weekly")).check(matches(isDisplayed()))
700     }
701 
702     @Test
703     fun backupAndRestoreSettingsFragment_whenExportFrequencyIsMonthly_showsCorrectSummary() {
704         whenever(exportSettingsViewModel.storedExportSettings).then {
705             MutableLiveData(ExportSettings.WithData(ExportFrequency.EXPORT_FREQUENCY_MONTHLY))
706         }
707         launchFragment<BackupAndRestoreSettingsFragment>(Bundle())
708 
709         onView(withText("On • Monthly")).check(matches(isDisplayed()))
710     }
711 
712     @Test
713     fun backupAndRestoreSettingsFragment_whenImportErrorIsWrongFile_showsImportErrorBanner() {
714         whenever(exportSettingsViewModel.storedExportSettings).then {
715             MutableLiveData(ExportSettings.WithData(ExportFrequency.EXPORT_FREQUENCY_NEVER))
716         }
717         whenever(importStatusViewModel.storedImportStatus).then {
718             MutableLiveData(
719                 ImportUiStatus.WithData(
720                     ImportUiState(
721                         dataImportError =
722                             ImportUiState.DataImportError.DATA_IMPORT_ERROR_WRONG_FILE,
723                         isImportOngoing = false,
724                     )
725                 )
726             )
727         }
728         launchFragment<BackupAndRestoreSettingsFragment>(Bundle())
729 
730         onView(withText("Choose file")).check(matches(isDisplayed()))
731         onView(withText("Couldn't restore data")).check(matches(isDisplayed()))
732         onView(
733                 withText(
734                     "The file you selected isn't compatible for restore. Make sure to select the correct exported file."
735                 )
736             )
737             .check(matches(isDisplayed()))
738         verify(healthConnectLogger)
739             .logImpression(BackupAndRestoreElement.IMPORT_WRONG_FILE_ERROR_BANNER)
740         verify(healthConnectLogger)
741             .logImpression(BackupAndRestoreElement.IMPORT_WRONG_FILE_ERROR_BANNER_BUTTON)
742     }
743 
744     @Test
745     fun backupAndRestoreSettingsFragment_whenImportErrorIsVersionMismatch_showsImportErrorBanner() {
746         whenever(exportSettingsViewModel.storedExportSettings).then {
747             MutableLiveData(ExportSettings.WithData(ExportFrequency.EXPORT_FREQUENCY_NEVER))
748         }
749         whenever(importStatusViewModel.storedImportStatus).then {
750             MutableLiveData(
751                 ImportUiStatus.WithData(
752                     ImportUiState(
753                         ImportUiState.DataImportError.DATA_IMPORT_ERROR_VERSION_MISMATCH,
754                         /** isImportOngoing= */
755                         false,
756                     )
757                 )
758             )
759         }
760         launchFragment<BackupAndRestoreSettingsFragment>(Bundle())
761 
762         onView(withText("Update now")).check(matches(isDisplayed()))
763         onView(withText("Couldn't restore data")).check(matches(isDisplayed()))
764         onView(
765                 withText(
766                     "Update your system so that Health\u00A0Connect can restore your data, then try again."
767                 )
768             )
769             .check(matches(isDisplayed()))
770         verify(healthConnectLogger)
771             .logImpression(BackupAndRestoreElement.IMPORT_VERSION_MISMATCH_ERROR_BANNER)
772         verify(healthConnectLogger)
773             .logImpression(BackupAndRestoreElement.IMPORT_VERSION_MISMATCH_ERROR_BANNER_BUTTON)
774     }
775 
776     @Test
777     fun backupAndRestoreSettingsFragment_whenImportErrorIsUnknown_showsImportErrorBanner() {
778         whenever(exportSettingsViewModel.storedExportSettings).then {
779             MutableLiveData(ExportSettings.WithData(ExportFrequency.EXPORT_FREQUENCY_NEVER))
780         }
781         whenever(importStatusViewModel.storedImportStatus).then {
782             MutableLiveData(
783                 ImportUiStatus.WithData(
784                     ImportUiState(
785                         ImportUiState.DataImportError.DATA_IMPORT_ERROR_UNKNOWN,
786                         /** isImportOngoing= */
787                         false,
788                     )
789                 )
790             )
791         }
792         launchFragment<BackupAndRestoreSettingsFragment>(Bundle())
793 
794         onView(withText("Try again")).check(matches(isDisplayed()))
795         onView(withText("Couldn't restore data")).check(matches(isDisplayed()))
796         onView(withText("There was a problem with restoring data from your export."))
797             .check(matches(isDisplayed()))
798         verify(healthConnectLogger)
799             .logImpression(BackupAndRestoreElement.IMPORT_GENERAL_ERROR_BANNER)
800         verify(healthConnectLogger)
801             .logImpression(BackupAndRestoreElement.IMPORT_GENERAL_ERROR_BANNER_BUTTON)
802     }
803 
804     @Test
805     fun backupAndRestoreSettingsFragment_whenImportErrorIsNone_doesNotShowImportErrorBanner() {
806         whenever(exportSettingsViewModel.storedExportSettings).then {
807             MutableLiveData(ExportSettings.WithData(ExportFrequency.EXPORT_FREQUENCY_NEVER))
808         }
809         whenever(importStatusViewModel.storedImportStatus).then {
810             MutableLiveData(
811                 ImportUiStatus.WithData(
812                     ImportUiState(
813                         dataImportError = ImportUiState.DataImportError.DATA_IMPORT_ERROR_NONE,
814                         isImportOngoing = false,
815                     )
816                 )
817             )
818         }
819         launchFragment<BackupAndRestoreSettingsFragment>(Bundle())
820 
821         onView(withText("Couldn't restore data")).check(doesNotExist())
822     }
823 
824     @Test
825     fun backupAndRestoreSettingsFragment_clicksAboutBackupAndRestore_showsHelpCenterLink() {
826         whenever(exportSettingsViewModel.storedExportSettings).then {
827             MutableLiveData(ExportSettings.WithData(ExportFrequency.EXPORT_FREQUENCY_WEEKLY))
828         }
829 
830         launchFragment<BackupAndRestoreSettingsFragment>(Bundle())
831 
832         onView(withText("About backup and restore")).check(matches(isDisplayed()))
833 
834         onView(withText("About backup and restore")).perform(scrollTo(), click())
835         assertThat(fakeDeviceInfoUtils.backupAndRestoreHelpCenterInvoked).isTrue()
836     }
837 }
838