Skip to content

Commit

Permalink
[MBL-18548][All] Images embedded in pages fail to load after 24h from…
Browse files Browse the repository at this point in the history
… first logging into the app (#2799)

refs: MBL-18548
affects: Student, Parent, Teacher
release note: none

* Added webview authentication in activities onResume.

* Fixed Parent unit tests.

* Removed try-catch to see where FlutterAppMigration tests are failing.

* Add log and test with different timezone

* More logs

* More logs

* Revert test changes.
  • Loading branch information
tamaskozmer authored Feb 14, 2025
1 parent 1e3aede commit fe04336
Show file tree
Hide file tree
Showing 10 changed files with 128 additions and 61 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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

Expand All @@ -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() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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]
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -329,6 +329,7 @@ class NavigationActivity : BaseRouterActivity(), Navigation, MasqueradingDialog.
override fun onResume() {
super.onResume()
applyCurrentFragmentTheme()
webViewAuthenticator.authenticateWebViews(lifecycleScope, this)
}

private fun checkAppUpdates() {
Expand Down Expand Up @@ -406,11 +407,6 @@ class NavigationActivity : BaseRouterActivity(), Navigation, MasqueradingDialog.
}

scheduleAlarms()

if (ApiPrefs.isFirstMasqueradingStart) {
loadAuthenticatedSession()
ApiPrefs.isFirstMasqueradingStart = false
}
}

private fun logOfflineEvents(isOnline: Boolean) {
Expand All @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -150,10 +148,10 @@ class InitActivity : BasePresenterActivity<InitActivityPresenter, InitActivityVi
lateinit var featureFlagProvider: FeatureFlagProvider

@Inject
lateinit var oAuthApi: OAuthAPI.OAuthInterface
lateinit var alarmScheduler: AlarmScheduler

@Inject
lateinit var alarmScheduler: AlarmScheduler
lateinit var webViewAuthenticator: WebViewAuthenticator

private var selectedTab = 0
private var drawerItemSelectedJob: Job? = null
Expand Down Expand Up @@ -267,22 +265,11 @@ class InitActivity : BasePresenterActivity<InitActivityPresenter, InitActivityVi
fetchFeatureFlags()

requestNotificationsPermission()

if (ApiPrefs.isFirstMasqueradingStart) {
loadAuthenticatedSession()
ApiPrefs.isFirstMasqueradingStart = false
}
}

private fun loadAuthenticatedSession() {
lifecycleScope.launch {
oAuthApi.getAuthenticatedSession(
ApiPrefs.fullDomain,
RestParams(isForceReadFromNetwork = true)
).dataOrNull?.sessionUrl?.let {
loadUrlIntoHeadlessWebView(this@InitActivity, it)
}
}
override fun onResume() {
super.onResume()
webViewAuthenticator.authenticateWebViews(lifecycleScope, this)
}

private fun requestNotificationsPermission() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,6 @@ object ApiPrefs : PrefManager(PREFERENCE_FILE_NAME) {
var isStudentView by BooleanPref()
var isMasqueradingFromQRCode by BooleanPref()
var masqueradeId by LongPref(-1L)
var isFirstMasqueradingStart by BooleanPref()
internal var masqueradeDomain by StringPref()
internal var masqueradeUser: User? by GsonPref(User::class.java, null, "masq-user", false)

Expand Down Expand Up @@ -147,6 +146,8 @@ object ApiPrefs : PrefManager(PREFERENCE_FILE_NAME) {
val showElementaryView
get() = canvasForElementary && elementaryDashboardEnabledOverride

var webViewAuthenticationTimestamp by LongPref(0)

/**
* clearAllData is required for logout.
* Clears all data including credentials and cache.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ object MasqueradeHelper {
cleanupMasquerading(ContextKeeper.appContext)
// isMasquerading is already set so this will set the masqueradeUser
ApiPrefs.user = response.body()
ApiPrefs.isFirstMasqueradingStart = true
ApiPrefs.webViewAuthenticationTimestamp = 0 // Reset the timestamp so the WebViews will authenticate for the new user
restartApplication(startingClass)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import androidx.work.WorkManager
import com.google.firebase.FirebaseApp
import com.google.firebase.crashlytics.FirebaseCrashlytics
import com.instructure.canvasapi2.apis.FileFolderAPI
import com.instructure.canvasapi2.apis.OAuthAPI
import com.instructure.canvasapi2.managers.OAuthManager
import com.instructure.canvasapi2.utils.Analytics
import com.instructure.canvasapi2.utils.ApiPrefs
Expand All @@ -41,6 +42,7 @@ import com.instructure.pandautils.utils.FeatureFlagProvider
import com.instructure.pandautils.utils.HtmlContentFormatter
import com.instructure.pandautils.utils.StorageUtils
import com.instructure.pandautils.utils.ThemePrefs
import com.instructure.pandautils.utils.WebViewAuthenticator
import com.instructure.pandautils.utils.date.DateTimeProvider
import dagger.Module
import dagger.Provides
Expand Down Expand Up @@ -185,4 +187,12 @@ class ApplicationModule {
fun provideRatingDialogPrefs(): RatingDialog.Prefs {
return RatingDialog.Prefs
}

@Provides
fun provideWebViewAuthenticator(
oAuthApi: OAuthAPI.OAuthInterface,
apiPrefs: ApiPrefs
): WebViewAuthenticator {
return WebViewAuthenticator(oAuthApi, apiPrefs)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/*
* 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 android.content.Context
import com.instructure.canvasapi2.apis.OAuthAPI
import com.instructure.canvasapi2.builders.RestParams
import com.instructure.canvasapi2.utils.ApiPrefs
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch

private const val HOUR_IN_MILLIS = 1000 * 60 * 60

class WebViewAuthenticator(private val oAuthApi: OAuthAPI.OAuthInterface, private val apiPrefs: ApiPrefs) {

fun authenticateWebViews(coroutineScope: CoroutineScope, context: Context) {
val currentTime = System.currentTimeMillis()
val lastAuthenticated = apiPrefs.webViewAuthenticationTimestamp
if (currentTime - lastAuthenticated > HOUR_IN_MILLIS) {
coroutineScope.launch {
oAuthApi.getAuthenticatedSession(
apiPrefs.fullDomain,
RestParams(isForceReadFromNetwork = true)
).dataOrNull?.sessionUrl?.let {
loadUrlIntoHeadlessWebView(context, it)
apiPrefs.webViewAuthenticationTimestamp = currentTime
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -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()) }
}
}

0 comments on commit fe04336

Please sign in to comment.