diff --git a/apps/parent/src/main/java/com/instructure/parentapp/features/main/MainActivity.kt b/apps/parent/src/main/java/com/instructure/parentapp/features/main/MainActivity.kt index 0423f3228e..950bf931c4 100644 --- a/apps/parent/src/main/java/com/instructure/parentapp/features/main/MainActivity.kt +++ b/apps/parent/src/main/java/com/instructure/parentapp/features/main/MainActivity.kt @@ -27,8 +27,6 @@ import androidx.lifecycle.lifecycleScope import androidx.navigation.NavController import androidx.navigation.fragment.NavHostFragment import com.google.android.material.snackbar.Snackbar -import com.instructure.canvasapi2.apis.OAuthAPI -import com.instructure.canvasapi2.builders.RestParams import com.instructure.canvasapi2.utils.ApiPrefs import com.instructure.canvasapi2.utils.MasqueradeHelper import com.instructure.loginapi.login.dialog.MasqueradingDialog @@ -42,7 +40,7 @@ import com.instructure.pandautils.utils.AppType import com.instructure.pandautils.utils.ColorKeeper import com.instructure.pandautils.utils.Const import com.instructure.pandautils.utils.ThemePrefs -import com.instructure.pandautils.utils.loadUrlIntoHeadlessWebView +import com.instructure.pandautils.utils.WebViewAuthenticator import com.instructure.parentapp.R import com.instructure.parentapp.databinding.ActivityMainBinding import com.instructure.parentapp.features.dashboard.InboxCountUpdater @@ -68,7 +66,7 @@ class MainActivity : BaseCanvasActivity(), OnUnreadCountInvalidated, Masqueradin lateinit var alarmScheduler: AlarmScheduler @Inject - lateinit var oAuthApi: OAuthAPI.OAuthInterface + lateinit var webViewAuthenticator: WebViewAuthenticator private lateinit var navController: NavController @@ -80,23 +78,12 @@ class MainActivity : BaseCanvasActivity(), OnUnreadCountInvalidated, Masqueradin handleQrMasquerading() scheduleAlarms() - if (ApiPrefs.isFirstMasqueradingStart) { - loadAuthenticatedSession() - ApiPrefs.isFirstMasqueradingStart = false - } - RatingDialog.showRatingDialog(this, AppType.PARENT) } - private fun loadAuthenticatedSession() { - lifecycleScope.launch { - oAuthApi.getAuthenticatedSession( - ApiPrefs.fullDomain, - RestParams(isForceReadFromNetwork = true) - ).dataOrNull?.sessionUrl?.let { - loadUrlIntoHeadlessWebView(this@MainActivity, it) - } - } + override fun onResume() { + super.onResume() + webViewAuthenticator.authenticateWebViews(lifecycleScope, this) } private fun handleQrMasquerading() { diff --git a/apps/parent/src/main/java/com/instructure/parentapp/util/FlutterAppMigration.kt b/apps/parent/src/main/java/com/instructure/parentapp/util/FlutterAppMigration.kt index 2c86ad1cb6..adee092bea 100644 --- a/apps/parent/src/main/java/com/instructure/parentapp/util/FlutterAppMigration.kt +++ b/apps/parent/src/main/java/com/instructure/parentapp/util/FlutterAppMigration.kt @@ -205,7 +205,6 @@ class FlutterAppMigration( isMasquerading = flutterSignedInUser.masqueradeUser != null isMasqueradingFromQRCode = flutterSignedInUser.isMasqueradingFromQRCode.orDefault() masqueradeId = flutterSignedInUser.masqueradeUser?.id ?: -1 - isFirstMasqueradingStart = true domain = if (isMasquerading) Uri.parse(flutterSignedInUser.masqueradeDomain).host.orEmpty() else signedInUser.domain user = if (isMasquerading) flutterSignedInUser.masqueradeUser else signedInUser.user } diff --git a/apps/parent/src/test/java/com/instructure/parentapp/util/FlutterAppMigrationTest.kt b/apps/parent/src/test/java/com/instructure/parentapp/util/FlutterAppMigrationTest.kt index 00514fe491..525d499f88 100644 --- a/apps/parent/src/test/java/com/instructure/parentapp/util/FlutterAppMigrationTest.kt +++ b/apps/parent/src/test/java/com/instructure/parentapp/util/FlutterAppMigrationTest.kt @@ -246,7 +246,6 @@ class FlutterAppMigrationTest { apiPrefs.isMasquerading = false apiPrefs.isMasqueradingFromQRCode = false apiPrefs.masqueradeId = -1 - apiPrefs.isFirstMasqueradingStart = true apiPrefs.domain = "domain2.com" apiPrefs.user = expectedUsers[1] } diff --git a/apps/student/src/main/java/com/instructure/student/activity/NavigationActivity.kt b/apps/student/src/main/java/com/instructure/student/activity/NavigationActivity.kt index 32f10de8dd..621c063ce9 100644 --- a/apps/student/src/main/java/com/instructure/student/activity/NavigationActivity.kt +++ b/apps/student/src/main/java/com/instructure/student/activity/NavigationActivity.kt @@ -112,11 +112,11 @@ import com.instructure.pandautils.utils.RequestCodes.PICK_FILE_FROM_DEVICE import com.instructure.pandautils.utils.RequestCodes.PICK_IMAGE_GALLERY import com.instructure.pandautils.utils.ThemePrefs import com.instructure.pandautils.utils.ViewStyler +import com.instructure.pandautils.utils.WebViewAuthenticator import com.instructure.pandautils.utils.applyTheme import com.instructure.pandautils.utils.hideKeyboard import com.instructure.pandautils.utils.isAccessibilityEnabled import com.instructure.pandautils.utils.items -import com.instructure.pandautils.utils.loadUrlIntoHeadlessWebView import com.instructure.pandautils.utils.onClickWithRequireNetwork import com.instructure.pandautils.utils.post import com.instructure.pandautils.utils.postSticky @@ -212,15 +212,15 @@ class NavigationActivity : BaseRouterActivity(), Navigation, MasqueradingDialog. @Inject lateinit var alarmScheduler: AlarmScheduler - @Inject - lateinit var oAuthApi: OAuthAPI.OAuthInterface - @Inject lateinit var offlineAnalyticsManager: OfflineAnalyticsManager @Inject lateinit var enabledCourseTabs: EnabledTabs + @Inject + lateinit var webViewAuthenticator: WebViewAuthenticator + private var routeJob: WeaveJob? = null private var debounceJob: Job? = null private var drawerItemSelectedJob: Job? = null @@ -329,6 +329,7 @@ class NavigationActivity : BaseRouterActivity(), Navigation, MasqueradingDialog. override fun onResume() { super.onResume() applyCurrentFragmentTheme() + webViewAuthenticator.authenticateWebViews(lifecycleScope, this) } private fun checkAppUpdates() { @@ -406,11 +407,6 @@ class NavigationActivity : BaseRouterActivity(), Navigation, MasqueradingDialog. } scheduleAlarms() - - if (ApiPrefs.isFirstMasqueradingStart) { - loadAuthenticatedSession() - ApiPrefs.isFirstMasqueradingStart = false - } } private fun logOfflineEvents(isOnline: Boolean) { @@ -423,17 +419,6 @@ class NavigationActivity : BaseRouterActivity(), Navigation, MasqueradingDialog. } } - private fun loadAuthenticatedSession() { - lifecycleScope.launch { - oAuthApi.getAuthenticatedSession( - ApiPrefs.fullDomain, - RestParams(isForceReadFromNetwork = true) - ).dataOrNull?.sessionUrl?.let { - loadUrlIntoHeadlessWebView(this@NavigationActivity, it) - } - } - } - private fun handleTokenCheck(online: Boolean?) { val checkToken = ApiPrefs.checkTokenAfterOfflineLogin if (checkToken && online == true) { diff --git a/apps/teacher/src/main/java/com/instructure/teacher/activities/InitActivity.kt b/apps/teacher/src/main/java/com/instructure/teacher/activities/InitActivity.kt index b4d48263d2..1ad27ff4d8 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/activities/InitActivity.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/activities/InitActivity.kt @@ -40,8 +40,6 @@ import androidx.fragment.app.FragmentManager import androidx.lifecycle.lifecycleScope import androidx.localbroadcastmanager.content.LocalBroadcastManager import com.google.android.material.bottomnavigation.BottomNavigationView -import com.instructure.canvasapi2.apis.OAuthAPI -import com.instructure.canvasapi2.builders.RestParams import com.instructure.canvasapi2.managers.CourseNicknameManager import com.instructure.canvasapi2.managers.ThemeManager import com.instructure.canvasapi2.managers.UserManager @@ -94,10 +92,10 @@ import com.instructure.pandautils.utils.LocaleUtils import com.instructure.pandautils.utils.ProfileUtils import com.instructure.pandautils.utils.ThemePrefs import com.instructure.pandautils.utils.ViewStyler +import com.instructure.pandautils.utils.WebViewAuthenticator import com.instructure.pandautils.utils.applyTheme import com.instructure.pandautils.utils.isAccessibilityEnabled import com.instructure.pandautils.utils.items -import com.instructure.pandautils.utils.loadUrlIntoHeadlessWebView import com.instructure.pandautils.utils.setGone import com.instructure.pandautils.utils.setVisible import com.instructure.pandautils.utils.toast @@ -150,10 +148,10 @@ class InitActivity : BasePresenterActivity HOUR_IN_MILLIS) { + coroutineScope.launch { + oAuthApi.getAuthenticatedSession( + apiPrefs.fullDomain, + RestParams(isForceReadFromNetwork = true) + ).dataOrNull?.sessionUrl?.let { + loadUrlIntoHeadlessWebView(context, it) + apiPrefs.webViewAuthenticationTimestamp = currentTime + } + } + } + } +} \ No newline at end of file diff --git a/libs/pandautils/src/test/java/com/instructure/pandautils/utils/WebViewAuthenticatorTest.kt b/libs/pandautils/src/test/java/com/instructure/pandautils/utils/WebViewAuthenticatorTest.kt new file mode 100644 index 0000000000..3c2ee7aa9e --- /dev/null +++ b/libs/pandautils/src/test/java/com/instructure/pandautils/utils/WebViewAuthenticatorTest.kt @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2025 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.instructure.pandautils.utils + +import com.instructure.canvasapi2.apis.OAuthAPI +import com.instructure.canvasapi2.utils.ApiPrefs +import com.instructure.canvasapi2.utils.DataResult +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.every +import io.mockk.mockk +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class WebViewAuthenticatorTest { + + private val oAuthApi: OAuthAPI.OAuthInterface = mockk(relaxed = true) + private val apiPrefs: ApiPrefs = mockk(relaxed = true) + + private val webViewAuthenticator = WebViewAuthenticator(oAuthApi, apiPrefs) + + @Test + fun `Authenticate webviews when timestamp is older than an hour`() = runTest { + every { apiPrefs.webViewAuthenticationTimestamp } returns System.currentTimeMillis() - 1000 * 60 * 61 + coEvery { oAuthApi.getAuthenticatedSession(any(), any()) } returns DataResult.Fail() + + webViewAuthenticator.authenticateWebViews(this, mockk()) + this.testScheduler.advanceUntilIdle() + + coVerify { oAuthApi.getAuthenticatedSession(any(), any()) } + } + + @Test + fun `Do not authenticate webviews when timestamp is not older than an hour`() = runTest { + every { apiPrefs.webViewAuthenticationTimestamp } returns System.currentTimeMillis() + coEvery { oAuthApi.getAuthenticatedSession(any(), any()) } returns DataResult.Fail() + + webViewAuthenticator.authenticateWebViews(this, mockk()) + + coVerify(exactly = 0) { oAuthApi.getAuthenticatedSession(any(), any()) } + } +} \ No newline at end of file