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