From 12d5e2807665770ab936aac004aeee439957fcfe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81kos=20Hermann?= Date: Wed, 8 Nov 2023 11:10:54 +0100 Subject: [PATCH 001/132] Release Student 7.0.0 (256) --- apps/student/build.gradle | 4 ++-- .../offline/offlinecontent/OfflineContentViewModel.kt | 3 --- .../offlinecontent/itemviewmodels/CourseItemViewModel.kt | 1 - 3 files changed, 2 insertions(+), 6 deletions(-) diff --git a/apps/student/build.gradle b/apps/student/build.gradle index eb671160b8..9aa4e3023d 100644 --- a/apps/student/build.gradle +++ b/apps/student/build.gradle @@ -50,8 +50,8 @@ android { applicationId "com.instructure.candroid" minSdkVersion Versions.MIN_SDK targetSdkVersion Versions.TARGET_SDK - versionCode = 255 - versionName = '6.26.1' + versionCode = 256 + versionName = '7.0.0' vectorDrawables.useSupportLibrary = true multiDexEnabled = true diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/offlinecontent/OfflineContentViewModel.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/offlinecontent/OfflineContentViewModel.kt index 18323b2c61..9a4fcbcd92 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/offlinecontent/OfflineContentViewModel.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/offlinecontent/OfflineContentViewModel.kt @@ -177,8 +177,6 @@ class OfflineContentViewModel @Inject constructor( val tabs = course.tabs?.filter { it.tabId in ALLOWED_TAB_IDS }.orEmpty() val size = "~${Formatter.formatShortFileSize(context, files.sumOf { it.size } + tabs.filter { it.tabId != Tab.FILES_ID }.size * TAB_SIZE)}" - val collapsed = _data.value?.courseItems?.find { it.courseId == courseId }?.collapsed ?: (this.course == null) - return CourseItemViewModel( data = CourseItemViewData( fullContentSync = courseSyncSettingsWithFiles.courseSyncSettings.fullContentSync, @@ -192,7 +190,6 @@ class OfflineContentViewModel @Inject constructor( } ), courseId = course.id, - collapsed = collapsed, onCheckedChanged = this::onCourseCheckedChanged ) } diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/offlinecontent/itemviewmodels/CourseItemViewModel.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/offlinecontent/itemviewmodels/CourseItemViewModel.kt index 7fa6a016d6..f57ffa3a00 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/offlinecontent/itemviewmodels/CourseItemViewModel.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/offlinecontent/itemviewmodels/CourseItemViewModel.kt @@ -28,7 +28,6 @@ import com.instructure.pandautils.mvvm.ItemViewModel data class CourseItemViewModel( val data: CourseItemViewData, val courseId: Long, - @get:Bindable override var collapsed: Boolean, val onCheckedChanged: (Boolean, CourseItemViewModel) -> Unit ) : GroupItemViewModel(collapsable = true, items = data.tabs.ifEmpty { listOf(EmptyCourseContentViewModel()) }) { override val layoutId = R.layout.item_offline_course From 15c24c6aa34f63eb1e0c5de8e573afddc4b1f32b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81kos=20Hermann?= Date: Wed, 8 Nov 2023 12:06:01 +0100 Subject: [PATCH 002/132] fix tests --- .../student/ui/interaction/OfflineContentInteractionTest.kt | 4 ++-- .../offline/offlinecontent/OfflineContentViewModel.kt | 3 +++ .../offlinecontent/itemviewmodels/CourseItemViewModel.kt | 1 + 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/OfflineContentInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/OfflineContentInteractionTest.kt index 2be3ec69ac..eaf9d6cd9a 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/OfflineContentInteractionTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/OfflineContentInteractionTest.kt @@ -73,10 +73,10 @@ class OfflineContentInteractionTest : StudentTest() { @Test @TestMetaData(Priority.IMPORTANT, FeatureCategory.OFFLINE_CONTENT, TestCategory.INTERACTION, false) - fun displaysCourseExpandedIfCourseOfflineContent() { + fun displaysCourseCollapsedIfCourseOfflineContent() { val data = createMockCanvas() goToOfflineContentByCourse(data) - manageOfflineContentPage.assertDisplaysItemWithExpandedState(data.courses.values.first().name, true) + manageOfflineContentPage.assertDisplaysItemWithExpandedState(data.courses.values.first().name, false) } @Test diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/offlinecontent/OfflineContentViewModel.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/offlinecontent/OfflineContentViewModel.kt index 9a4fcbcd92..f91571e6b1 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/offlinecontent/OfflineContentViewModel.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/offlinecontent/OfflineContentViewModel.kt @@ -177,6 +177,8 @@ class OfflineContentViewModel @Inject constructor( val tabs = course.tabs?.filter { it.tabId in ALLOWED_TAB_IDS }.orEmpty() val size = "~${Formatter.formatShortFileSize(context, files.sumOf { it.size } + tabs.filter { it.tabId != Tab.FILES_ID }.size * TAB_SIZE)}" + val collapsed = _data.value?.courseItems?.find { it.courseId == courseId }?.collapsed ?: true + return CourseItemViewModel( data = CourseItemViewData( fullContentSync = courseSyncSettingsWithFiles.courseSyncSettings.fullContentSync, @@ -190,6 +192,7 @@ class OfflineContentViewModel @Inject constructor( } ), courseId = course.id, + collapsed = collapsed, onCheckedChanged = this::onCourseCheckedChanged ) } diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/offlinecontent/itemviewmodels/CourseItemViewModel.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/offlinecontent/itemviewmodels/CourseItemViewModel.kt index f57ffa3a00..7fa6a016d6 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/offlinecontent/itemviewmodels/CourseItemViewModel.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/offlinecontent/itemviewmodels/CourseItemViewModel.kt @@ -28,6 +28,7 @@ import com.instructure.pandautils.mvvm.ItemViewModel data class CourseItemViewModel( val data: CourseItemViewData, val courseId: Long, + @get:Bindable override var collapsed: Boolean, val onCheckedChanged: (Boolean, CourseItemViewModel) -> Unit ) : GroupItemViewModel(collapsable = true, items = data.tabs.ifEmpty { listOf(EmptyCourseContentViewModel()) }) { override val layoutId = R.layout.item_offline_course From deac2c9b9b74e1ab475028eed4475b6890d37b2a Mon Sep 17 00:00:00 2001 From: "kristof.nemere" Date: Thu, 9 Nov 2023 14:19:26 +0100 Subject: [PATCH 003/132] fixed landscape tests --- .../OfflineContentInteractionTest.kt | 3 +- .../pages/offline/ManageOfflineContentPage.kt | 30 ++++++++++++------- .../res/layout/fragment_offline_content.xml | 1 + 3 files changed, 21 insertions(+), 13 deletions(-) diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/OfflineContentInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/OfflineContentInteractionTest.kt index eaf9d6cd9a..57590587ec 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/OfflineContentInteractionTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/OfflineContentInteractionTest.kt @@ -98,7 +98,6 @@ class OfflineContentInteractionTest : StudentTest() { manageOfflineContentPage.assertDisplaysItemWithExpandedState(course.name, false) manageOfflineContentPage.expandCollapseItem(course.name) manageOfflineContentPage.assertDisplaysItemWithExpandedState(course.name, true) - manageOfflineContentPage.assertItemDisplayed(data.courseTabs[course.id]!!.first().label!!) getCourseItemNames(data, course).forEach { manageOfflineContentPage.assertItemDisplayed(it) } } @@ -353,7 +352,7 @@ class OfflineContentInteractionTest : StudentTest() { } private fun getCourseItemNames(data: MockCanvas, course: Course): List { - return data.courseTabs[course.id]!!.map { it.label!! } + course.name + + return listOf(course.name) + data.courseTabs[course.id]!!.map { it.label!! } + data.folderFiles[data.courseRootFolders[course.id]!!.id]!!.map { it.displayName!! } } diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/offline/ManageOfflineContentPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/offline/ManageOfflineContentPage.kt index 87d65a84ae..80887424eb 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/offline/ManageOfflineContentPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/offline/ManageOfflineContentPage.kt @@ -17,9 +17,8 @@ package com.instructure.student.ui.pages.offline -import androidx.test.espresso.matcher.ViewMatchers -import androidx.test.espresso.matcher.ViewMatchers.hasSibling -import androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility +import androidx.test.espresso.contrib.RecyclerViewActions +import androidx.test.espresso.matcher.ViewMatchers.* import com.instructure.canvas.espresso.containsTextCaseInsensitive import com.instructure.canvas.espresso.hasCheckedState import com.instructure.canvas.espresso.withRotation @@ -27,6 +26,7 @@ import com.instructure.espresso.* import com.instructure.espresso.actions.ForceClick import com.instructure.espresso.page.* import com.instructure.pandautils.R +import com.instructure.pandautils.binding.BindableViewHolder import org.hamcrest.CoreMatchers.allOf class ManageOfflineContentPage : BasePage(R.id.manageOfflineContentPage) { @@ -36,12 +36,14 @@ class ManageOfflineContentPage : BasePage(R.id.manageOfflineContentPage) { //OfflineMethod fun changeItemSelectionState(itemName: String) { + onView(withId(R.id.offlineContentRecyclerView)) + .perform(RecyclerViewActions.scrollTo(hasDescendant(withText(itemName)))) onView(withId(R.id.checkbox) + hasSibling(withId(R.id.title) + withText(itemName))).scrollTo().click() } //OfflineMethod fun expandCollapseItem(itemName: String) { - onView(withId(R.id.arrow) + withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE) + hasSibling(withId(R.id.title) + withText(itemName))).scrollTo().perform(ForceClick()) + onView(withId(R.id.arrow) + withEffectiveVisibility(Visibility.VISIBLE) + hasSibling(withId(R.id.title) + withText(itemName))).scrollTo().perform(ForceClick()) } //OfflineMethod @@ -82,7 +84,7 @@ class ManageOfflineContentPage : BasePage(R.id.manageOfflineContentPage) { //OfflineMethod fun assertSelectButtonText(selectAll: Boolean) { - if(selectAll) waitForView(withId(R.id.menu_select_all) + withText(R.string.offline_content_select_all)).assertDisplayed() + if (selectAll) waitForView(withId(R.id.menu_select_all) + withText(R.string.offline_content_select_all)).assertDisplayed() else waitForView(withId(R.id.menu_select_all) + withText(R.string.offline_content_deselect_all)).assertDisplayed() } @@ -98,12 +100,12 @@ class ManageOfflineContentPage : BasePage(R.id.manageOfflineContentPage) { //OfflineMethod fun assertCourseCountWithMatcher(expectedCount: Int) { - ConstraintLayoutItemCountAssertionWithMatcher((allOf(withId(R.id.arrow), withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE))), expectedCount) + ConstraintLayoutItemCountAssertionWithMatcher((allOf(withId(R.id.arrow), withEffectiveVisibility(Visibility.VISIBLE))), expectedCount) } //OfflineMethod fun assertCourseCount(expectedCount: Int) { - onView((allOf(withId(R.id.arrow), withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))).check(ConstraintLayoutItemCountAssertion(expectedCount)) + onView((allOf(withId(R.id.arrow), withEffectiveVisibility(Visibility.VISIBLE)))).check(ConstraintLayoutItemCountAssertion(expectedCount)) } //OfflineMethod @@ -114,7 +116,10 @@ class ManageOfflineContentPage : BasePage(R.id.manageOfflineContentPage) { //OfflineMethod fun assertCheckedStateOfItem(itemName: String, state: Int) { - onView(withId(R.id.checkbox) + hasSibling(withId(R.id.title) + withText(itemName)) + hasCheckedState(state)).scrollTo().assertDisplayed() + val matcher = withId(R.id.checkbox) + hasSibling(withId(R.id.title) + withText(itemName)) + hasCheckedState(state) + onView(withId(R.id.offlineContentRecyclerView)) + .perform(RecyclerViewActions.scrollTo(hasDescendant(withText(itemName)))) + onView(matcher).scrollTo().assertDisplayed() } //OfflineMethod @@ -129,21 +134,24 @@ class ManageOfflineContentPage : BasePage(R.id.manageOfflineContentPage) { //OfflineMethod fun assertDisplaysEmptyCourse() { - onView(withText(R.string.offline_content_empty_course_message)).assertDisplayed() + onView(withText(R.string.offline_content_empty_course_message)).scrollTo().assertDisplayed() } //OfflineMethod fun assertDisplaysItemWithExpandedState(title: String, expanded: Boolean) { onView(withId(R.id.arrow) + withRotation(if (expanded) 180f else 0f) - + withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE) + + withEffectiveVisibility(Visibility.VISIBLE) + hasSibling(withId(R.id.title) + withText(title)) ).scrollTo().assertDisplayed() } //OfflineMethod fun assertItemDisplayed(title: String) { - onView(withId(R.id.title) + withText(title)).scrollTo().assertDisplayed() + val matcher = withId(R.id.title) + withText(title) + onView(withId(R.id.offlineContentRecyclerView)) + .perform(RecyclerViewActions.scrollTo(hasDescendant(matcher))) + onView(matcher).scrollTo().assertDisplayed() } //OfflineMethod diff --git a/libs/pandautils/src/main/res/layout/fragment_offline_content.xml b/libs/pandautils/src/main/res/layout/fragment_offline_content.xml index 903a4ee3e3..5948dac458 100644 --- a/libs/pandautils/src/main/res/layout/fragment_offline_content.xml +++ b/libs/pandautils/src/main/res/layout/fragment_offline_content.xml @@ -195,6 +195,7 @@ app:refreshState="@{viewModel.state}"> Date: Thu, 9 Nov 2023 16:13:49 +0100 Subject: [PATCH 004/132] move fetching feature flags --- .../instructure/student/activity/CallbackActivity.kt | 10 ++++++++++ .../student/activity/NavigationActivity.kt | 11 ----------- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/apps/student/src/main/java/com/instructure/student/activity/CallbackActivity.kt b/apps/student/src/main/java/com/instructure/student/activity/CallbackActivity.kt index 988a81b30b..4eb72c7801 100644 --- a/apps/student/src/main/java/com/instructure/student/activity/CallbackActivity.kt +++ b/apps/student/src/main/java/com/instructure/student/activity/CallbackActivity.kt @@ -18,6 +18,7 @@ package com.instructure.student.activity import android.os.Bundle +import androidx.lifecycle.lifecycleScope import com.google.firebase.crashlytics.FirebaseCrashlytics import com.heapanalytics.android.Heap import com.instructure.canvasapi2.StatusCallback @@ -39,12 +40,19 @@ import com.instructure.student.flutterChannels.FlutterComm import com.instructure.student.fragment.NotificationListFragment import com.instructure.student.service.StudentPageViewService import com.instructure.student.util.StudentPrefs +import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.Job +import kotlinx.coroutines.launch import retrofit2.Call import retrofit2.Response +import javax.inject.Inject +@AndroidEntryPoint abstract class CallbackActivity : ParentActivity(), OnUnreadCountInvalidated, NotificationListFragment.OnNotificationCountInvalidated { + @Inject + lateinit var featureFlagProvider: FeatureFlagProvider + private var loadInitialDataJob: Job? = null abstract fun gotLaunchDefinitions(launchDefinitions: List?) @@ -130,6 +138,8 @@ abstract class CallbackActivity : ParentActivity(), OnUnreadCountInvalidated, No getUnreadNotificationCount() + featureFlagProvider.fetchEnvironmentFeatureFlags() + initialCoreDataLoadingComplete() } catch { initialCoreDataLoadingComplete() 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 674981de32..9187905881 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 @@ -143,9 +143,6 @@ class NavigationActivity : BaseRouterActivity(), Navigation, MasqueradingDialog. @Inject lateinit var databaseProvider: DatabaseProvider - @Inject - lateinit var featureFlagProvider: FeatureFlagProvider - @Inject lateinit var repository: NavigationRepository @@ -305,8 +302,6 @@ class NavigationActivity : BaseRouterActivity(), Navigation, MasqueradingDialog. setupNavDrawerItems() - loadFeatureFlags() - checkAppUpdates() val savedBottomScreens = savedInstanceState?.getStringArrayList(BOTTOM_SCREENS_BUNDLE_KEY) @@ -345,12 +340,6 @@ class NavigationActivity : BaseRouterActivity(), Navigation, MasqueradingDialog. } } - private fun loadFeatureFlags() { - lifecycleScope.launch { - featureFlagProvider.fetchEnvironmentFeatureFlags() - } - } - private fun requestNotificationsPermission() { if (checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) { notificationsPermissionContract.launch(Manifest.permission.POST_NOTIFICATIONS) From 85185e8bf16d3aeccd940ef92be6c2b4e87b62fe Mon Sep 17 00:00:00 2001 From: Kristof Deak <92309696+kdeakinstructure@users.noreply.github.com> Date: Fri, 10 Nov 2023 11:55:17 +0100 Subject: [PATCH 005/132] [MBL-17118][Student] - Create Sync Settings E2E test (#2232) --- .../offline/ManageOfflineContentE2ETest.kt | 4 +- ...lineTest.kt => OfflineDashboardE2ETest.kt} | 4 +- .../e2e/offline/OfflineSyncProgressE2ETest.kt | 2 + .../e2e/offline/OfflineSyncSettingsE2ETest.kt | 145 ++++++++++++++++++ .../OfflineContentInteractionTest.kt | 6 +- .../SyncSettingsInteractionTest.kt | 38 ++--- .../student/ui/pages/SettingsPage.kt | 33 +++- .../student/ui/pages/SyncSettingsPage.kt | 79 ---------- .../pages/offline/ManageOfflineContentPage.kt | 70 +++++---- .../pages/offline/OfflineSyncSettingsPage.kt | 120 +++++++++++++++ .../student/ui/utils/StudentTest.kt | 4 +- 11 files changed, 361 insertions(+), 144 deletions(-) rename apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/{DashboardE2EOfflineTest.kt => OfflineDashboardE2ETest.kt} (96%) create mode 100644 apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineSyncSettingsE2ETest.kt delete mode 100644 apps/student/src/androidTest/java/com/instructure/student/ui/pages/SyncSettingsPage.kt create mode 100644 apps/student/src/androidTest/java/com/instructure/student/ui/pages/offline/OfflineSyncSettingsPage.kt diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/ManageOfflineContentE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/ManageOfflineContentE2ETest.kt index 5ca6d91392..2d0af62c9e 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/ManageOfflineContentE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/ManageOfflineContentE2ETest.kt @@ -131,7 +131,7 @@ class ManageOfflineContentE2ETest : StudentTest() { manageOfflineContentPage.assertCheckedStateOfItem("Announcements", MaterialCheckBox.STATE_UNCHECKED) manageOfflineContentPage.assertCheckedStateOfItem("Discussions", MaterialCheckBox.STATE_UNCHECKED) - Log.d(STEP_TAG, "Assert that the 'SELECT ALL' will be displayed after clicking the 'SELECT ALL' button.") + Log.d(STEP_TAG, "Assert that the 'SELECT ALL' will be displayed after clicking the 'DESELECT ALL' button.") manageOfflineContentPage.assertSelectButtonText(selectAll = true) Log.d(STEP_TAG, "Navigate back to Dashboard Page. Open 'Global' Manage Offline Content page.") @@ -186,7 +186,7 @@ class ManageOfflineContentE2ETest : StudentTest() { manageOfflineContentPage.assertCourseCountWithMatcher(2) Log.d(STEP_TAG, "Click on the 'Sync' button.") - manageOfflineContentPage.clickOnSyncButtonAndConfirm() + manageOfflineContentPage.clickOnSyncButton() Log.d(STEP_TAG, "Wait for the 'Download Started' dashboard notification to be displayed, and the to disappear.") dashboardPage.waitForRender() diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/DashboardE2EOfflineTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineDashboardE2ETest.kt similarity index 96% rename from apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/DashboardE2EOfflineTest.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineDashboardE2ETest.kt index 599a31aa0a..b874bb2430 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/DashboardE2EOfflineTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineDashboardE2ETest.kt @@ -31,7 +31,7 @@ import org.junit.After import org.junit.Test @HiltAndroidTest -class DashboardE2EOfflineTest : StudentTest() { +class OfflineDashboardE2ETest : StudentTest() { override fun displaysPageObjects() = Unit override fun enableAndConfigureAccessibilityChecks() = Unit @@ -57,6 +57,8 @@ class DashboardE2EOfflineTest : StudentTest() { Log.d(STEP_TAG, "Select the entire '${course1.name}' course for sync. Click on the 'Sync' button.") manageOfflineContentPage.changeItemSelectionState(course1.name) manageOfflineContentPage.clickOnSyncButtonAndConfirm() + manageOfflineContentPage.changeItemSelectionState(course1.name) + manageOfflineContentPage.clickOnSyncButton() Log.d(STEP_TAG, "Wait for the 'Download Started' dashboard notification to be displayed, and the to disappear.") dashboardPage.waitForRender() diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineSyncProgressE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineSyncProgressE2ETest.kt index 77cea19bc2..dc0b91da01 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineSyncProgressE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineSyncProgressE2ETest.kt @@ -59,6 +59,8 @@ class OfflineSyncProgressE2ETest : StudentTest() { Log.d(STEP_TAG, "Select the entire '${course1.name}' course for sync. Click on the 'Sync' button.") manageOfflineContentPage.changeItemSelectionState(course1.name) manageOfflineContentPage.clickOnSyncButtonAndConfirm() + manageOfflineContentPage.changeItemSelectionState(course1.name) + manageOfflineContentPage.clickOnSyncButton() Log.d(STEP_TAG, "Wait for the 'Download Started' dashboard notification to be displayed, and the to disappear.") dashboardPage.waitForRender() diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineSyncSettingsE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineSyncSettingsE2ETest.kt new file mode 100644 index 0000000000..fac7879901 --- /dev/null +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineSyncSettingsE2ETest.kt @@ -0,0 +1,145 @@ +/* + * Copyright (C) 2023 - 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.student.ui.e2e.offline + +import android.util.Log +import com.instructure.canvas.espresso.OfflineE2E +import com.instructure.panda_annotations.FeatureCategory +import com.instructure.panda_annotations.Priority +import com.instructure.panda_annotations.TestCategory +import com.instructure.panda_annotations.TestMetaData +import com.instructure.pandautils.R +import com.instructure.student.ui.e2e.offline.utils.OfflineTestUtils +import com.instructure.student.ui.utils.StudentTest +import com.instructure.student.ui.utils.ViewUtils +import com.instructure.student.ui.utils.seedData +import com.instructure.student.ui.utils.tokenLogin +import dagger.hilt.android.testing.HiltAndroidTest +import org.junit.After +import org.junit.Test + +@HiltAndroidTest +class OfflineSyncSettingsE2ETest : StudentTest() { + override fun displaysPageObjects() = Unit + + override fun enableAndConfigureAccessibilityChecks() = Unit + + @OfflineE2E + @Test + @TestMetaData(Priority.MANDATORY, FeatureCategory.OFFLINE_CONTENT, TestCategory.E2E) + fun offlineSyncSettingsE2ETest() { + + Log.d(PREPARATION_TAG,"Seeding data.") + val data = seedData(students = 1, teachers = 1, courses = 2, announcements = 1) + val student = data.studentsList[0] + + Log.d(STEP_TAG,"Login with user: ${student.name}, login id: ${student.loginId}.") + tokenLogin(student) + dashboardPage.waitForRender() + + Log.d(STEP_TAG, "Open Settings page from the Left Side menu.") + leftSideNavigationDrawerPage.clickSettingsMenu() + + Log.d(STEP_TAG, "Assert that the Offline Sync Settings related information is displayed properly on the Settings Page ('Daily' is the default status).") + settingsPage.assertOfflineContentDisplayed() + settingsPage.assertOfflineContentTitle() + settingsPage.assertOfflineSyncSettingsStatus(R.string.daily) + + Log.d(STEP_TAG, "Open Offline Sync Settings page and wait for it to be loaded.") + settingsPage.openOfflineSyncSettingsPage() + offlineSyncSettingsPage.assertPageObjects() + + Log.d(STEP_TAG, "Assert that further settings, such as the toolbar title is displayed and correct, and both the Auto Content Sync and Wi-Fi Only Sync toggles are displayed and checked by default.") + offlineSyncSettingsPage.assertFurtherSettingsIsDisplayed() + offlineSyncSettingsPage.assertSyncSettingsToolbarTitle() + offlineSyncSettingsPage.assertAutoSyncSwitchIsChecked() + offlineSyncSettingsPage.assertWifiOnlySwitchIsChecked() + + Log.d(STEP_TAG, "Assert that all the descriptions of how these settings are working are displayed.") + offlineSyncSettingsPage.assertSyncSettingsPageDescriptions() + + Log.d(STEP_TAG, "Assert that the sync frequency label is 'Daily', because that is the default setting.") + offlineSyncSettingsPage.assertSyncFrequencyLabelText(R.string.daily) + + Log.d(STEP_TAG, "Switch off the 'Auto Content Sync' toggle, and assert if that the further settings below will disappear.") + offlineSyncSettingsPage.clickAutoSyncSwitch() + + Log.d(STEP_TAG, "Assert that further settings, such as the toolbar title, Auto Content Sync and Wi-Fi Only Sync toggles are NOT displayed.") + offlineSyncSettingsPage.assertFurtherSettingsNotDisplayed() + + Log.d(STEP_TAG, "Switch back the 'Auto Content Sync' toggle, and assert if that the further settings below will be displayed again.") + offlineSyncSettingsPage.clickAutoSyncSwitch() + + Log.d(STEP_TAG, "Assert that further settings, such as the toolbar title is displayed and correct, and both the Auto Content Sync and Wi-Fi Only Sync toggles are displayed again.") + offlineSyncSettingsPage.assertFurtherSettingsIsDisplayed() + + Log.d(STEP_TAG, "Switch off the 'Sync Content Wi-Fi Only' toggle and assert that the confirmation dialog (with the proper texts) is displayed.") + offlineSyncSettingsPage.clickWifiOnlySwitch() + offlineSyncSettingsPage.assertTurnOffWifiOnlyDialogTexts() + + Log.d(STEP_TAG, "Click on the 'TURN OFF' button on the dialog to really turn off the 'Sync Content Wi-Fi Only' switch.") + offlineSyncSettingsPage.clickTurnOff() + + Log.d(STEP_TAG, "Assert that the 'Sync Content Wi-Fi Only' switch is not checked any more.") + offlineSyncSettingsPage.assertWifiOnlySwitchIsNotChecked() + + Log.d(STEP_TAG, "Open the Sync Frequency Settings dialog and select 'Weekly' option.") + offlineSyncSettingsPage.openSyncFrequencySettingsDialog() + offlineSyncSettingsPage.clickSyncFrequencyDialogOption(R.string.weekly) + + Log.d(STEP_TAG, "Assert that the sync frequency label became 'Weekly' (without any manual refresh).") + offlineSyncSettingsPage.assertSyncFrequencyLabelText(R.string.weekly) + + Log.d(STEP_TAG, "Navigate back to Dashboard Page and logout.") + ViewUtils.pressBackButton(2) + leftSideNavigationDrawerPage.logout() + + Log.d(STEP_TAG, "Click 'Find My School' button.") + loginLandingPage.clickFindMySchoolButton() + + Log.d(STEP_TAG, "Enter domain: ${student.domain}.") + loginFindSchoolPage.enterDomain(student.domain) + + Log.d(STEP_TAG, "Click on 'Next' button on the Toolbar.") + loginFindSchoolPage.clickToolbarNextMenuItem() + + Log.d(STEP_TAG, "Login with user: ${student.name}, login id: ${student.loginId}.") + loginSignInPage.loginAs(student) + dashboardPage.waitForRender() + + Log.d(STEP_TAG, "Open Settings page from the Left Side menu.") + leftSideNavigationDrawerPage.clickSettingsMenu() + + Log.d(STEP_TAG, "Assert that the Offline Sync Settings frequency text is 'Weekly' (because we set it previously).") + settingsPage.assertOfflineSyncSettingsStatus(R.string.weekly) + + Log.d(STEP_TAG, "Open Offline Sync Settings page and wait for it to be loaded.") + settingsPage.openOfflineSyncSettingsPage() + offlineSyncSettingsPage.assertPageObjects() + + Log.d(STEP_TAG, "Assert that the Offline Sync Settings frequency text is 'Weekly' (because we set it previously) and the 'Sync Content Wi-Fi Only' switch is switched off.") + offlineSyncSettingsPage.assertSyncFrequencyLabelText(R.string.weekly) + offlineSyncSettingsPage.assertWifiOnlySwitchIsNotChecked() + } + + @After + fun tearDown() { + Log.d(PREPARATION_TAG, "Turn back on the Wi-Fi and Mobile Data on the device via ADB, so it will come back online.") + OfflineTestUtils.turnOnConnectionViaADB() + } + +} \ No newline at end of file diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/OfflineContentInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/OfflineContentInteractionTest.kt index 2be3ec69ac..55cdbbd4e4 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/OfflineContentInteractionTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/OfflineContentInteractionTest.kt @@ -368,9 +368,9 @@ class OfflineContentInteractionTest : StudentTest() { tokenLogin(data.domain, token, student) dashboardPage.waitForRender() leftSideNavigationDrawerPage.clickSettingsMenu() - settingsPage.openOfflineContentPage() - syncSettingsPage.clickWifiOnlySwitch() - syncSettingsPage.clickTurnOff() + settingsPage.openOfflineSyncSettingsPage() + offlineSyncSettingsPage.clickWifiOnlySwitch() + offlineSyncSettingsPage.clickTurnOff() Espresso.pressBack() Espresso.pressBack() dashboardPage.openGlobalManageOfflineContentPage() diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/SyncSettingsInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/SyncSettingsInteractionTest.kt index e81a192070..319772e5f3 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/SyncSettingsInteractionTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/SyncSettingsInteractionTest.kt @@ -38,36 +38,36 @@ class SyncSettingsInteractionTest : StudentTest() { @TestMetaData(Priority.IMPORTANT, FeatureCategory.SYNC_SETTINGS, TestCategory.INTERACTION, false) fun testFurtherSettingsDisplayedByDefault() { goToSyncSettings() - syncSettingsPage.assertFurtherSettingsIsDisplayed() + offlineSyncSettingsPage.assertFurtherSettingsIsDisplayed() } @Test @TestMetaData(Priority.IMPORTANT, FeatureCategory.SYNC_SETTINGS, TestCategory.INTERACTION, false) fun testClickAutoSyncHidesFurtherSettings() { goToSyncSettings() - syncSettingsPage.clickAutoSyncSwitch() - syncSettingsPage.assertFurtherSettingsNotDisplayed() + offlineSyncSettingsPage.clickAutoSyncSwitch() + offlineSyncSettingsPage.assertFurtherSettingsNotDisplayed() } @Test @TestMetaData(Priority.IMPORTANT, FeatureCategory.SYNC_SETTINGS, TestCategory.INTERACTION, false) fun testChangeFrequency() { goToSyncSettings() - syncSettingsPage.assertFrequencyLabelText(R.string.daily) - syncSettingsPage.clickFrequency() - syncSettingsPage.clickDialogOption(R.string.weekly) - syncSettingsPage.assertFrequencyLabelText(R.string.weekly) + offlineSyncSettingsPage.assertSyncFrequencyLabelText(R.string.daily) + offlineSyncSettingsPage.openSyncFrequencySettingsDialog() + offlineSyncSettingsPage.clickSyncFrequencyDialogOption(R.string.weekly) + offlineSyncSettingsPage.assertSyncFrequencyLabelText(R.string.weekly) } @Test @TestMetaData(Priority.IMPORTANT, FeatureCategory.SYNC_SETTINGS, TestCategory.INTERACTION, false) fun testChangeContentOverWifiOnly() { goToSyncSettings() - syncSettingsPage.assertWifiOnlySwitchIsChecked() - syncSettingsPage.clickWifiOnlySwitch() - syncSettingsPage.assertDialogDisplayedWithTitle(R.string.syncSettings_wifiConfirmationTitle) - syncSettingsPage.clickTurnOff() - syncSettingsPage.assertWifiOnlySwitchIsNotChecked() + offlineSyncSettingsPage.assertWifiOnlySwitchIsChecked() + offlineSyncSettingsPage.clickWifiOnlySwitch() + offlineSyncSettingsPage.assertTurnOffWifiOnlyDialogTexts() + offlineSyncSettingsPage.clickTurnOff() + offlineSyncSettingsPage.assertWifiOnlySwitchIsNotChecked() } @Test @@ -76,10 +76,10 @@ class SyncSettingsInteractionTest : StudentTest() { val data = createMockCanvas() goToSyncSettings(data) - syncSettingsPage.clickFrequency() - syncSettingsPage.clickDialogOption(R.string.weekly) - syncSettingsPage.clickWifiOnlySwitch() - syncSettingsPage.clickTurnOff() + offlineSyncSettingsPage.openSyncFrequencySettingsDialog() + offlineSyncSettingsPage.clickSyncFrequencyDialogOption(R.string.weekly) + offlineSyncSettingsPage.clickWifiOnlySwitch() + offlineSyncSettingsPage.clickTurnOff() with(activityRule) { finishActivity() @@ -88,8 +88,8 @@ class SyncSettingsInteractionTest : StudentTest() { goToSyncSettings(data) - syncSettingsPage.assertFrequencyLabelText(R.string.weekly) - syncSettingsPage.assertWifiOnlySwitchIsNotChecked() + offlineSyncSettingsPage.assertSyncFrequencyLabelText(R.string.weekly) + offlineSyncSettingsPage.assertWifiOnlySwitchIsNotChecked() } private fun createMockCanvas(): MockCanvas { @@ -104,6 +104,6 @@ class SyncSettingsInteractionTest : StudentTest() { tokenLogin(data.domain, token, student) dashboardPage.waitForRender() leftSideNavigationDrawerPage.clickSettingsMenu() - settingsPage.openOfflineContentPage() + settingsPage.openOfflineSyncSettingsPage() } } diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/SettingsPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/SettingsPage.kt index 8f40d0d703..0792369df0 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/SettingsPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/SettingsPage.kt @@ -16,12 +16,22 @@ */ package com.instructure.student.ui.pages -import com.instructure.espresso.* -import com.instructure.espresso.page.* +import androidx.test.espresso.matcher.ViewMatchers.hasSibling +import com.instructure.espresso.OnViewWithId +import com.instructure.espresso.TextViewColorAssertion +import com.instructure.espresso.assertDisplayed +import com.instructure.espresso.assertNotDisplayed +import com.instructure.espresso.click +import com.instructure.espresso.page.BasePage +import com.instructure.espresso.page.onView +import com.instructure.espresso.page.plus +import com.instructure.espresso.page.withId +import com.instructure.espresso.page.withParent +import com.instructure.espresso.page.withText +import com.instructure.espresso.scrollTo import com.instructure.student.R class SettingsPage : BasePage(R.id.settingsFragment) { - private val toolbar by OnViewWithId(R.id.toolbar) private val profileSettingLabel by OnViewWithId(R.id.profileSettings) private val accountPreferencesLabel by OnViewWithId(R.id.accountPreferences) private val pushNotificationsLabel by OnViewWithId(R.id.pushNotifications) @@ -81,15 +91,30 @@ class SettingsPage : BasePage(R.id.settingsFragment) { appThemeStatus.check(TextViewColorAssertion(expectedTextColor)) } - fun openOfflineContentPage() { + //OfflineMethod + fun openOfflineSyncSettingsPage() { offlineContent.scrollTo().click() } + //OfflineMethod fun assertOfflineContentDisplayed() { offlineContent.scrollTo().assertDisplayed() } + //OfflineMethod fun assertOfflineContentNotDisplayed() { offlineContent.assertNotDisplayed() } + + //OfflineMethod + fun assertOfflineContentTitle() { + onView(withId(R.id.offlineContentTitle) + withText(R.string.offlineContent)).assertDisplayed() + } + + //OfflineMethod + fun assertOfflineSyncSettingsStatus(expectedStatus: Int) { + onView(withId(R.id.offlineSyncSettingsStatus) + withText(expectedStatus) + withParent(R.id.offlineSyncSettingsContainer) + + hasSibling(withId(R.id.offlineSyncSettingsTitle) + withText(R.string.offlineSyncSettingsTitle))).assertDisplayed() + } + } diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/SyncSettingsPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/SyncSettingsPage.kt deleted file mode 100644 index a7f2db9f3b..0000000000 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/SyncSettingsPage.kt +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Copyright (C) 2023 - present Instructure, Inc. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, version 3 of the License. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - * - */ - -package com.instructure.student.ui.pages - -import androidx.test.espresso.assertion.ViewAssertions.matches -import androidx.test.espresso.matcher.ViewMatchers.isChecked -import androidx.test.espresso.matcher.ViewMatchers.isNotChecked -import com.instructure.espresso.* -import com.instructure.espresso.page.BasePage -import com.instructure.espresso.page.onViewWithText -import com.instructure.pandautils.R - -class SyncSettingsPage : BasePage(R.id.syncSettingsPage) { - - private val toolbar by OnViewWithId(R.id.toolbar) - private val autoSyncSwitch by OnViewWithId(R.id.autoSyncSwitch) - private val furtherSettings by OnViewWithId(R.id.furtherSettings) - private val syncFrequencyLabel by OnViewWithId(R.id.syncFrequencyLabel) - private val wifiOnlySwitch by OnViewWithId(R.id.wifiOnlySwitch) - - fun clickAutoSyncSwitch() { - autoSyncSwitch.click() - } - - fun clickFrequency() { - syncFrequencyLabel.click() - } - - fun clickDialogOption(stringResId: Int) { - onViewWithText(stringResId).click() - } - - fun clickWifiOnlySwitch() { - wifiOnlySwitch.click() - } - - fun clickTurnOff() { - onViewWithText(R.string.syncSettings_wifiConfirmationPositiveButton).click() - } - - fun assertFurtherSettingsIsDisplayed() { - furtherSettings.assertDisplayed() - } - - fun assertFurtherSettingsNotDisplayed() { - furtherSettings.assertNotDisplayed() - } - - fun assertFrequencyLabelText(expected: Int) { - syncFrequencyLabel.assertHasText(expected) - } - - fun assertWifiOnlySwitchIsChecked() { - wifiOnlySwitch.check(matches(isChecked())) - } - - fun assertWifiOnlySwitchIsNotChecked() { - wifiOnlySwitch.check(matches(isNotChecked())) - } - - fun assertDialogDisplayedWithTitle(title: Int) { - onViewWithText(title).assertDisplayed() - } -} diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/offline/ManageOfflineContentPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/offline/ManageOfflineContentPage.kt index 87d65a84ae..383f9fe2df 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/offline/ManageOfflineContentPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/offline/ManageOfflineContentPage.kt @@ -17,15 +17,32 @@ package com.instructure.student.ui.pages.offline +import androidx.test.espresso.Espresso import androidx.test.espresso.matcher.ViewMatchers +import androidx.test.espresso.matcher.ViewMatchers.Visibility import androidx.test.espresso.matcher.ViewMatchers.hasSibling import androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility import com.instructure.canvas.espresso.containsTextCaseInsensitive import com.instructure.canvas.espresso.hasCheckedState import com.instructure.canvas.espresso.withRotation -import com.instructure.espresso.* +import com.instructure.espresso.ConstraintLayoutItemCountAssertion +import com.instructure.espresso.ConstraintLayoutItemCountAssertionWithMatcher +import com.instructure.espresso.DoesNotExistAssertion +import com.instructure.espresso.OnViewWithId +import com.instructure.espresso.WaitForViewWithId import com.instructure.espresso.actions.ForceClick -import com.instructure.espresso.page.* +import com.instructure.espresso.assertDisplayed +import com.instructure.espresso.click +import com.instructure.espresso.matchers.WaitForViewMatcher +import com.instructure.espresso.page.BasePage +import com.instructure.espresso.page.onView +import com.instructure.espresso.page.plus +import com.instructure.espresso.page.waitForView +import com.instructure.espresso.page.withAncestor +import com.instructure.espresso.page.withId +import com.instructure.espresso.page.withParent +import com.instructure.espresso.page.withText +import com.instructure.espresso.scrollTo import com.instructure.pandautils.R import org.hamcrest.CoreMatchers.allOf @@ -34,43 +51,35 @@ class ManageOfflineContentPage : BasePage(R.id.manageOfflineContentPage) { private val syncButton by OnViewWithId(R.id.syncButton) private val storageInfoContainer by WaitForViewWithId(R.id.storageInfoContainer) - //OfflineMethod fun changeItemSelectionState(itemName: String) { onView(withId(R.id.checkbox) + hasSibling(withId(R.id.title) + withText(itemName))).scrollTo().click() } - //OfflineMethod fun expandCollapseItem(itemName: String) { onView(withId(R.id.arrow) + withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE) + hasSibling(withId(R.id.title) + withText(itemName))).scrollTo().perform(ForceClick()) } - //OfflineMethod fun expandCollapseFiles() { expandCollapseItem("Files") } - //OfflineMethod fun clickOnSyncButton() { syncButton.click() } - //OfflineMethod fun clickOnSyncButtonAndConfirm() { clickOnSyncButton() confirmSync() } - //OfflineMethod private fun confirmSync() { waitForView(withText("Sync") + withAncestor(R.id.buttonPanel)).click() } - //OfflineMethod fun confirmDiscardChanges() { waitForView(withText("Discard") + withAncestor(R.id.buttonPanel)).click() } - //OfflineMethod fun assertStorageInfoDetails() { onView(withId(R.id.storageLabel) + withText(R.string.offline_content_storage)).assertDisplayed() onView(withId(R.id.storageInfo) + containsTextCaseInsensitive("Used")).assertDisplayed() @@ -80,85 +89,78 @@ class ManageOfflineContentPage : BasePage(R.id.manageOfflineContentPage) { onView(withId(R.id.remainingLabel) + withText(R.string.offline_content_remaining)).assertDisplayed() } - //OfflineMethod fun assertSelectButtonText(selectAll: Boolean) { if(selectAll) waitForView(withId(R.id.menu_select_all) + withText(R.string.offline_content_select_all)).assertDisplayed() else waitForView(withId(R.id.menu_select_all) + withText(R.string.offline_content_deselect_all)).assertDisplayed() } - //OfflineMethod fun clickOnSelectAllButton() { waitForView(withId(R.id.menu_select_all) + withText(R.string.offline_content_select_all)).click() } - //OfflineMethod fun clickOnDeselectAllButton() { waitForView(withId(R.id.menu_select_all) + withText(R.string.offline_content_deselect_all)).click() } - //OfflineMethod fun assertCourseCountWithMatcher(expectedCount: Int) { ConstraintLayoutItemCountAssertionWithMatcher((allOf(withId(R.id.arrow), withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE))), expectedCount) } - //OfflineMethod fun assertCourseCount(expectedCount: Int) { onView((allOf(withId(R.id.arrow), withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))).check(ConstraintLayoutItemCountAssertion(expectedCount)) } - //OfflineMethod fun assertToolbarTexts(courseName: String) { onView(withText(courseName) + withParent(R.id.toolbar) + withAncestor(R.id.manageOfflineContentPage)).assertDisplayed() onView(withText(R.string.offline_content_toolbar_title) + withParent(R.id.toolbar) + withAncestor(R.id.manageOfflineContentPage)).assertDisplayed() } - //OfflineMethod fun assertCheckedStateOfItem(itemName: String, state: Int) { onView(withId(R.id.checkbox) + hasSibling(withId(R.id.title) + withText(itemName)) + hasCheckedState(state)).scrollTo().assertDisplayed() } - //OfflineMethod fun waitForItemDisappear(itemName: String) { onView(withId(R.id.checkbox) + hasSibling(withId(R.id.title) + withText(itemName))).check(DoesNotExistAssertion(5)) } - //OfflineMethod fun assertDisplaysNoCourses() { - onView(withText(R.string.offline_content_empty_message)).assertDisplayed() + Espresso.onView(ViewMatchers.withText(R.string.offline_content_empty_message)).assertDisplayed() } - //OfflineMethod fun assertDisplaysEmptyCourse() { - onView(withText(R.string.offline_content_empty_course_message)).assertDisplayed() + Espresso.onView(ViewMatchers.withText(R.string.offline_content_empty_course_message)).assertDisplayed() } - //OfflineMethod fun assertDisplaysItemWithExpandedState(title: String, expanded: Boolean) { - onView(withId(R.id.arrow) + Espresso.onView( + ViewMatchers.withId(R.id.arrow) + withRotation(if (expanded) 180f else 0f) - + withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE) - + hasSibling(withId(R.id.title) + withText(title)) + + withEffectiveVisibility(Visibility.VISIBLE) + + hasSibling(ViewMatchers.withId(R.id.title) + ViewMatchers.withText(title)) ).scrollTo().assertDisplayed() } - //OfflineMethod fun assertItemDisplayed(title: String) { - onView(withId(R.id.title) + withText(title)).scrollTo().assertDisplayed() + Espresso.onView(ViewMatchers.withId(R.id.title) + ViewMatchers.withText(title)).scrollTo().assertDisplayed() } - //OfflineMethod fun assertDiscardDialogDisplayed() { - waitForView(withText(R.string.offline_content_discard_dialog_title)).assertDisplayed() + WaitForViewMatcher.waitForView(ViewMatchers.withText(R.string.offline_content_discard_dialog_title)) + .assertDisplayed() } - //OfflineMethod fun assertSyncDialogDisplayed(text: String) { - waitForView(withText(text)).assertDisplayed() + WaitForViewMatcher.waitForView(ViewMatchers.withText(text)).assertDisplayed() } - //OfflineMethod fun assertStorageInfoText(storageInfoText: String) { - onView(withId(R.id.storageInfo) + withText(storageInfoText)).assertDisplayed() + Espresso.onView( + ViewMatchers.withId(R.id.storageInfo) + ViewMatchers.withText( + storageInfoText + ) + ).assertDisplayed() } } + + diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/offline/OfflineSyncSettingsPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/offline/OfflineSyncSettingsPage.kt new file mode 100644 index 0000000000..d27445850e --- /dev/null +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/offline/OfflineSyncSettingsPage.kt @@ -0,0 +1,120 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.instructure.student.ui.pages.offline + +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.matcher.ViewMatchers.isChecked +import androidx.test.espresso.matcher.ViewMatchers.isNotChecked +import com.instructure.espresso.OnViewWithId +import com.instructure.espresso.assertDisplayed +import com.instructure.espresso.assertHasText +import com.instructure.espresso.assertNotDisplayed +import com.instructure.espresso.click +import com.instructure.espresso.page.BasePage +import com.instructure.espresso.page.onView +import com.instructure.espresso.page.onViewWithText +import com.instructure.espresso.page.plus +import com.instructure.espresso.page.waitForView +import com.instructure.espresso.page.withAncestor +import com.instructure.espresso.page.withId +import com.instructure.espresso.page.withParent +import com.instructure.espresso.page.withText +import com.instructure.pandautils.R + +class OfflineSyncSettingsPage : BasePage(R.id.syncSettingsPage) { + + private val toolbar by OnViewWithId(R.id.toolbar) + private val autoSyncSwitch by OnViewWithId(R.id.autoSyncSwitch) + private val furtherSettings by OnViewWithId(R.id.furtherSettings) + private val syncFrequencyLabel by OnViewWithId(R.id.syncFrequencyLabel) + private val wifiOnlySwitch by OnViewWithId(R.id.wifiOnlySwitch) + + fun clickAutoSyncSwitch() { + autoSyncSwitch.click() + } + + fun openSyncFrequencySettingsDialog() { + syncFrequencyLabel.click() + } + + fun clickSyncFrequencyDialogOption(stringResId: Int) { + onView(withText(stringResId) + withParent(R.id.select_dialog_listview)).click() + } + + fun clickWifiOnlySwitch() { + wifiOnlySwitch.click() + } + + fun clickTurnOff() { + onViewWithText(R.string.syncSettings_wifiConfirmationPositiveButton).click() + } + + fun assertTurnOffWifiOnlyDialogTexts() { + waitForView(withId(R.id.alertTitle) + withText(R.string.syncSettings_wifiConfirmationTitle)).assertDisplayed() + waitForView(withText(R.string.syncSettings_wifiConfirmationPositiveButton) + withAncestor(R.id.buttonPanel)).assertDisplayed() + onView(withText(R.string.synySettings_wifiConfirmationMessage)).assertDisplayed() + } + + fun assertFurtherSettingsIsDisplayed() { + furtherSettings.assertDisplayed() + } + + fun assertFurtherSettingsNotDisplayed() { + furtherSettings.assertNotDisplayed() + } + + fun assertSyncFrequencyLabelText(expected: Int) { + syncFrequencyLabel.assertHasText(expected) + } + + fun assertSyncFrequencyTitleText() { + onView(withText(R.string.syncSettings_syncFrequencyTitle) + withParent(R.id.syncFrequencyContainer)).assertDisplayed() + } + + fun assertWifiOnlySwitchIsChecked() { + wifiOnlySwitch.check(matches(isChecked())) + } + + fun assertWifiOnlySwitchIsNotChecked() { + wifiOnlySwitch.check(matches(isNotChecked())) + } + + fun assertAutoSyncSwitchIsChecked() { + autoSyncSwitch.check(matches(isChecked())) + } + + fun assertAutoSyncSwitchIsNotChecked() { + autoSyncSwitch.check(matches(isNotChecked())) + } + + fun assertDialogDisplayedWithTitle(title: String) { + onViewWithText(title).assertDisplayed() + } + + fun assertSyncSettingsToolbarTitle() { + onView(withText(com.instructure.student.R.string.syncSettings_toolbarTitle) + withParent(withId( + com.instructure.student.R.id.toolbar) + withAncestor(com.instructure.student.R.id.syncSettingsPage))).assertDisplayed() + } + + fun assertSyncSettingsPageDescriptions() { + onView(withText(R.string.syncSettings_autoContentSyncDescription)).assertDisplayed() + onView(withText(R.string.syncSettings_syncFrequencyDescription)).assertDisplayed() + onView(withText(R.string.syncSettings_wifiOnlyDescription)).assertDisplayed() + } + +} diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/utils/StudentTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/utils/StudentTest.kt index 331d138f10..88c4ab96d8 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/utils/StudentTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/utils/StudentTest.kt @@ -95,11 +95,11 @@ import com.instructure.student.ui.pages.ShareExtensionStatusPage import com.instructure.student.ui.pages.ShareExtensionTargetPage import com.instructure.student.ui.pages.SubmissionDetailsPage import com.instructure.student.ui.pages.SyllabusPage -import com.instructure.student.ui.pages.SyncSettingsPage import com.instructure.student.ui.pages.TextSubmissionUploadPage import com.instructure.student.ui.pages.TodoPage import com.instructure.student.ui.pages.UrlSubmissionUploadPage import com.instructure.student.ui.pages.offline.ManageOfflineContentPage +import com.instructure.student.ui.pages.offline.OfflineSyncSettingsPage import com.instructure.student.ui.pages.offline.SyncProgressPage import dagger.hilt.android.testing.HiltAndroidRule import instructure.rceditor.RCETextEditor @@ -202,7 +202,7 @@ abstract class StudentTest : CanvasTest() { val importantDatesPage = ImportantDatesPage() val shareExtensionTargetPage = ShareExtensionTargetPage() val shareExtensionStatusPage = ShareExtensionStatusPage() - val syncSettingsPage = SyncSettingsPage() + val offlineSyncSettingsPage = OfflineSyncSettingsPage() val manageOfflineContentPage = ManageOfflineContentPage() val syncProgressPage = SyncProgressPage() From 7bd6726b67c991b1eb2190c680cca68ee4ecf5a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81kos=20Hermann?= Date: Fri, 10 Nov 2023 12:08:38 +0100 Subject: [PATCH 006/132] remove lateinit recycler adapters --- .../list/AssignmentListFragment.kt | 41 ++++----- .../features/grades/GradesListFragment.kt | 84 +++++++++++-------- .../modules/list/ModuleListFragment.kt | 28 ++++--- .../features/pages/list/PageListFragment.kt | 16 ++-- .../fragment/NotificationListFragment.kt | 52 ++++++------ .../student/fragment/ToDoListFragment.kt | 62 +++++++------- 6 files changed, 156 insertions(+), 127 deletions(-) diff --git a/apps/student/src/main/java/com/instructure/student/features/assignments/list/AssignmentListFragment.kt b/apps/student/src/main/java/com/instructure/student/features/assignments/list/AssignmentListFragment.kt index e198678fb9..97c13658cc 100644 --- a/apps/student/src/main/java/com/instructure/student/features/assignments/list/AssignmentListFragment.kt +++ b/apps/student/src/main/java/com/instructure/student/features/assignments/list/AssignmentListFragment.kt @@ -75,7 +75,7 @@ class AssignmentListFragment : ParentFragment(), Bookmarkable { private var canvasContext by ParcelableArg(key = Const.CANVAS_CONTEXT) - private lateinit var recyclerAdapter: AssignmentListRecyclerAdapter + private var recyclerAdapter: AssignmentListRecyclerAdapter? = null private var termAdapter: TermSpinnerAdapter? = null private var filterPosition = 0 @@ -118,7 +118,7 @@ class AssignmentListFragment : ParentFragment(), Bookmarkable { override fun onRefreshFinished() { if (!isAdded) return // Refresh can finish after user has left screen, causing emptyView to be null setRefreshing(false) - if (recyclerAdapter.size() == 0) { + if (recyclerAdapter?.size() == 0) { setEmptyView(binding.emptyView, R.drawable.ic_panda_space, R.string.noAssignments, R.string.noAssignmentsSubtext) } } @@ -140,14 +140,16 @@ class AssignmentListFragment : ParentFragment(), Bookmarkable { binding.sortByTextView.setText(sortOrder.buttonTextRes) binding.sortByButton.contentDescription = getString(sortOrder.contentDescriptionRes) - configureRecyclerView( + recyclerAdapter?.let { + configureRecyclerView( view, requireContext(), - recyclerAdapter, + it, R.id.swipeRefreshLayout, R.id.emptyView, R.id.listView - ) + ) + } binding.appbar.addOnOffsetChangedListener(AppBarLayout.OnOffsetChangedListener { _, i -> // Workaround for Toolbar not showing with swipe to refresh @@ -230,7 +232,7 @@ class AssignmentListFragment : ParentFragment(), Bookmarkable { dialog.dismiss() filterPosition = index filter = AssignmentListFilter.values()[index] - recyclerAdapter.filter = filter + recyclerAdapter?.filter = filter updateBadge() } @@ -262,7 +264,7 @@ class AssignmentListFragment : ParentFragment(), Bookmarkable { } else { emptyView.emptyViewText(getString(R.string.noItemsMatchingQuery, query)) } - recyclerAdapter.searchQuery = query + recyclerAdapter?.searchQuery = query } ViewStyler.themeToolbarColored(requireActivity(), toolbar, canvasContext) } @@ -290,22 +292,22 @@ class AssignmentListFragment : ParentFragment(), Bookmarkable { termSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { override fun onItemSelected(adapterView: AdapterView<*>, view: View?, i: Int, l: Long) { if (adapter.getItem(i)!!.title == getString(R.string.assignmentsListAllGradingPeriods)) { - recyclerAdapter.loadAssignment() + recyclerAdapter?.loadAssignment() } else { - recyclerAdapter.loadAssignmentsForGradingPeriod(adapter.getItem(i)!!.id, true) + recyclerAdapter?.loadAssignmentsForGradingPeriod(adapter.getItem(i)!!.id, true) termSpinner.isEnabled = false adapter.isLoading = true adapter.notifyDataSetChanged() } - recyclerAdapter.currentGradingPeriod = adapter.getItem(i) + recyclerAdapter?.currentGradingPeriod = adapter.getItem(i) } override fun onNothingSelected(adapterView: AdapterView<*>) {} } // If we have a "current" grading period select it - if (hasGradingPeriods && recyclerAdapter.currentGradingPeriod != null) { - val position = adapter.getPositionForId(recyclerAdapter.currentGradingPeriod?.id ?: 0) + if (hasGradingPeriods && recyclerAdapter?.currentGradingPeriod != null) { + val position = adapter.getPositionForId(recyclerAdapter?.currentGradingPeriod?.id ?: 0) if (position != -1) { termSpinner.setSelection(position) } else { @@ -318,16 +320,17 @@ class AssignmentListFragment : ParentFragment(), Bookmarkable { override fun onConfigurationChanged(newConfig: Configuration) = with(binding) { super.onConfigurationChanged(newConfig) - configureRecyclerView( + recyclerAdapter?.let { + configureRecyclerView( requireView(), requireContext(), - recyclerAdapter, + it, R.id.swipeRefreshLayout, R.id.emptyView, - R.id.listView, - R.string.noAssignments - ) - if (recyclerAdapter.size() == 0) { + R.id.listView + ) + } + if (recyclerAdapter?.size() == 0) { emptyView.changeTextSize() if (resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT) { if (isTablet) { @@ -348,7 +351,7 @@ class AssignmentListFragment : ParentFragment(), Bookmarkable { override fun onDestroy() { super.onDestroy() - recyclerAdapter.cancel() + recyclerAdapter?.cancel() } companion object { diff --git a/apps/student/src/main/java/com/instructure/student/features/grades/GradesListFragment.kt b/apps/student/src/main/java/com/instructure/student/features/grades/GradesListFragment.kt index d5b10ebda6..eb8dfb874d 100644 --- a/apps/student/src/main/java/com/instructure/student/features/grades/GradesListFragment.kt +++ b/apps/student/src/main/java/com/instructure/student/features/grades/GradesListFragment.kt @@ -74,7 +74,7 @@ class GradesListFragment : ParentFragment(), Bookmarkable { private var gradingScheme = emptyList() private lateinit var allTermsGradingPeriod: GradingPeriod - private lateinit var recyclerAdapter: GradesListRecyclerAdapter + private var recyclerAdapter: GradesListRecyclerAdapter? = null private val course: Course get() = canvasContext as Course @@ -106,9 +106,9 @@ class GradesListFragment : ParentFragment(), Bookmarkable { //check to see if grade is empty for reset if (whatIf == null) { assignment.submission = null - recyclerAdapter.assignmentsHash[assignment.id]?.submission = null + recyclerAdapter?.assignmentsHash?.get(assignment.id)?.submission = null } else { - recyclerAdapter.assignmentsHash[assignment.id]?.submission = Submission( + recyclerAdapter?.assignmentsHash?.get(assignment.id)?.submission = Submission( score = whatIf, grade = whatIf.toString() ) @@ -122,14 +122,17 @@ class GradesListFragment : ParentFragment(), Bookmarkable { ) view.let { configureViews(it) - configureRecyclerView(it, requireContext(), recyclerAdapter, R.id.swipeRefreshLayout, R.id.gradesEmptyView, R.id.listView) + recyclerAdapter?.let {recyclerAdapter -> + configureRecyclerView(it, requireContext(), recyclerAdapter, R.id.swipeRefreshLayout, R.id.gradesEmptyView, R.id.listView) + } + } } override fun onDestroyView() { super.onDestroyView() computeGradesJob?.cancel() - recyclerAdapter.cancel() + recyclerAdapter?.cancel() } override fun applyTheme() { @@ -143,7 +146,10 @@ class GradesListFragment : ParentFragment(), Bookmarkable { override fun onConfigurationChanged(newConfig: Configuration) { super.onConfigurationChanged(newConfig) - view?.let { configureRecyclerView(it, requireContext(), recyclerAdapter, R.id.swipeRefreshLayout, R.id.gradesEmptyView, R.id.listView) } + view?.let { + recyclerAdapter?.let { recyclerAdapter -> + configureRecyclerView(it, requireContext(), recyclerAdapter, R.id.swipeRefreshLayout, R.id.gradesEmptyView, R.id.listView) } + } } private fun configureViews(rootView: View) { @@ -169,7 +175,7 @@ class GradesListFragment : ParentFragment(), Bookmarkable { computeGrades(showTotalCheckBox.isChecked, -1) } else { val gradeString = getGradeString( - recyclerAdapter.courseGrade, + recyclerAdapter?.courseGrade, !isChecked ) txtOverallGrade.text = gradeString @@ -182,24 +188,26 @@ class GradesListFragment : ParentFragment(), Bookmarkable { whatIfView.setOnClickListener { showWhatIfCheckBox.toggle() } showWhatIfCheckBox.setOnCheckedChangeListener { _, _ -> - val currentScoreVal = recyclerAdapter.courseGrade?.currentScore ?: 0.0 + val currentScoreVal = recyclerAdapter?.courseGrade?.currentScore ?: 0.0 val currentScore = NumberHelper.doubleToPercentage(currentScoreVal) if (!showWhatIfCheckBox.isChecked) { txtOverallGrade.text = currentScore - } else if (recyclerAdapter.whatIfGrade != null) { - txtOverallGrade.text = NumberHelper.doubleToPercentage(recyclerAdapter.whatIfGrade) + } else if (recyclerAdapter?.whatIfGrade != null) { + recyclerAdapter?.let { + txtOverallGrade.text = NumberHelper.doubleToPercentage(it.whatIfGrade) + } } // If the user is turning off what if grades we need to do a full refresh, should be // cached data, so fast. if (!showWhatIfCheckBox.isChecked) { - recyclerAdapter.whatIfGrade = null - recyclerAdapter.loadCachedData() + recyclerAdapter?.whatIfGrade = null + recyclerAdapter?.loadCachedData() } else { // Only log when what if grades is checked on Analytics.logEvent(AnalyticsEventConstants.WHAT_IF_GRADES) - recyclerAdapter.notifyDataSetChanged() + recyclerAdapter?.notifyDataSetChanged() } } } @@ -270,12 +278,12 @@ class GradesListFragment : ParentFragment(), Bookmarkable { override fun onNothingSelected(parent: AdapterView<*>?) {} override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) { // The current item must always be set first - recyclerAdapter.currentGradingPeriod = termAdapter?.getItem(position) + recyclerAdapter?.currentGradingPeriod = termAdapter?.getItem(position) if (termAdapter?.getItem(position)?.title == getString(R.string.allGradingPeriods)) { - recyclerAdapter.loadData() + recyclerAdapter?.loadData() } else { if (termAdapter?.isEmpty == false) { - recyclerAdapter.loadAssignmentsForGradingPeriod( + recyclerAdapter?.loadAssignmentsForGradingPeriod( gradingPeriodID = termAdapter?.getItem(position)?.id.orDefault(), refreshFirst = true, forceNetwork = true @@ -290,8 +298,10 @@ class GradesListFragment : ParentFragment(), Bookmarkable { } // If we have a "current" grading period select it - if (recyclerAdapter.currentGradingPeriod != null) { - val position = termAdapter?.getPositionForId(recyclerAdapter.currentGradingPeriod?.id ?: -1) ?: -1 + if (recyclerAdapter?.currentGradingPeriod != null) { + val position = recyclerAdapter?.let { + termAdapter?.getPositionForId(it.currentGradingPeriod?.id ?: -1) ?: -1 + } ?: -1 if (position != -1) { termSpinner.setSelection(position) } else { @@ -335,7 +345,7 @@ class GradesListFragment : ParentFragment(), Bookmarkable { } private fun lockGrade(isLocked: Boolean) { - if (isLocked || recyclerAdapter.isAllGradingPeriodsSelected && !course.isTotalsForAllGradingPeriodsEnabled) { + if (isLocked || recyclerAdapter?.isAllGradingPeriodsSelected == true && !course.isTotalsForAllGradingPeriodsEnabled) { binding.txtOverallGrade.setInvisible() binding.lockedGradeImage.setVisible() binding.gradeToggleView.setGone() @@ -351,25 +361,27 @@ class GradesListFragment : ParentFragment(), Bookmarkable { private fun computeGrades(isShowTotalGrade: Boolean, lastPositionChanged: Int) { computeGradesJob = weave { val result = inBackground { - if (!isShowTotalGrade) { - if (course.isApplyAssignmentGroupWeights) { - calcGradesTotal(recyclerAdapter.assignmentGroups) - } else { - calcGradesTotalNoWeight(recyclerAdapter.assignmentGroups) - } - } else { //Calculates grade based on only graded assignments - if (course.isApplyAssignmentGroupWeights) { - calcGradesGraded(recyclerAdapter.assignmentGroups) - } else { - calcGradesGradedNoWeight(recyclerAdapter.assignmentGroups) + recyclerAdapter?.let { recyclerAdapter -> + if (!isShowTotalGrade) { + if (course.isApplyAssignmentGroupWeights) { + calcGradesTotal(recyclerAdapter.assignmentGroups) + } else { + calcGradesTotalNoWeight(recyclerAdapter.assignmentGroups) + } + } else { //Calculates grade based on only graded assignments + if (course.isApplyAssignmentGroupWeights) { + calcGradesGraded(recyclerAdapter.assignmentGroups) + } else { + calcGradesGradedNoWeight(recyclerAdapter.assignmentGroups) + } } } } - recyclerAdapter.whatIfGrade = result + recyclerAdapter?.whatIfGrade = result binding.txtOverallGrade.text = NumberHelper.doubleToPercentage(result) - if(lastPositionChanged >= 0) recyclerAdapter.notifyItemChanged(lastPositionChanged) + if(lastPositionChanged >= 0) recyclerAdapter?.notifyItemChanged(lastPositionChanged) } } @@ -392,7 +404,7 @@ class GradesListFragment : ParentFragment(), Bookmarkable { var totalPoints = 0.0 val weight = g.groupWeight for (a in g.assignments) { - val tempAssignment = recyclerAdapter.assignmentsHash[a.id].takeIf { !it?.omitFromFinalGrade.orDefault() } + val tempAssignment = recyclerAdapter?.assignmentsHash?.get(a.id).takeIf { !it?.omitFromFinalGrade.orDefault() } val tempSub = tempAssignment?.submission if (tempSub?.grade != null && tempAssignment.submissionTypesRaw.isNotEmpty()) { earnedPoints += tempSub.score @@ -427,7 +439,7 @@ class GradesListFragment : ParentFragment(), Bookmarkable { val weight = g.groupWeight var assignCount = 0 for (a in g.assignments) { - val tempAssignment = recyclerAdapter.assignmentsHash[a.id].takeIf { !it?.omitFromFinalGrade.orDefault() } + val tempAssignment = recyclerAdapter?.assignmentsHash?.get(a.id).takeIf { !it?.omitFromFinalGrade.orDefault() } val tempSub = tempAssignment?.submission if (tempSub?.grade != null && tempAssignment.submissionTypesRaw.isNotEmpty() && Const.PENDING_REVIEW != tempSub.workflowState) { assignCount++ // Determines if a group contains assignments @@ -476,7 +488,7 @@ class GradesListFragment : ParentFragment(), Bookmarkable { var totalPoints = 0.0 for (g in groups) { for (a in g.assignments) { - val tempAssignment = recyclerAdapter.assignmentsHash[a.id].takeIf { !it?.omitFromFinalGrade.orDefault() } + val tempAssignment = recyclerAdapter?.assignmentsHash?.get(a.id).takeIf { !it?.omitFromFinalGrade.orDefault() } val tempSub = tempAssignment?.submission if (tempSub?.grade != null && tempAssignment.submissionTypesRaw.isNotEmpty() && Const.PENDING_REVIEW != tempSub.workflowState) { earnedPoints += tempSub.score @@ -510,7 +522,7 @@ class GradesListFragment : ParentFragment(), Bookmarkable { var earnedPoints = 0.0 for (g in groups) { for (a in g.assignments) { - val tempAssignment = recyclerAdapter.assignmentsHash[a.id].takeIf { !it?.omitFromFinalGrade.orDefault() } + val tempAssignment = recyclerAdapter?.assignmentsHash?.get(a.id).takeIf { !it?.omitFromFinalGrade.orDefault() } val tempSub = tempAssignment?.submission if (tempSub?.grade != null && tempAssignment.submissionTypesRaw.isNotEmpty()) { totalPoints += tempAssignment.pointsPossible diff --git a/apps/student/src/main/java/com/instructure/student/features/modules/list/ModuleListFragment.kt b/apps/student/src/main/java/com/instructure/student/features/modules/list/ModuleListFragment.kt index 134ea38aea..1d9e96862c 100644 --- a/apps/student/src/main/java/com/instructure/student/features/modules/list/ModuleListFragment.kt +++ b/apps/student/src/main/java/com/instructure/student/features/modules/list/ModuleListFragment.kt @@ -64,7 +64,7 @@ class ModuleListFragment : ParentFragment(), Bookmarkable { private lateinit var recyclerBinding: PandaRecyclerRefreshLayoutBinding private var canvasContext: CanvasContext by ParcelableArg(key = Const.CANVAS_CONTEXT) - private lateinit var recyclerAdapter: ModuleListRecyclerAdapter + private var recyclerAdapter: ModuleListRecyclerAdapter? = null @Inject lateinit var repository: ModuleListRepository @@ -85,7 +85,7 @@ class ModuleListFragment : ParentFragment(), Bookmarkable { } override fun onDestroy() { - recyclerAdapter.cancel() + recyclerAdapter?.cancel() super.onDestroy() } @@ -105,7 +105,7 @@ class ModuleListFragment : ParentFragment(), Bookmarkable { override fun onConfigurationChanged(newConfig: Configuration) { super.onConfigurationChanged(newConfig) - if (recyclerAdapter.size() == 0) { + if (recyclerAdapter?.size() == 0) { recyclerBinding.emptyView.changeTextSize() if (resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT) { if (isTablet) { @@ -160,9 +160,9 @@ class ModuleListFragment : ParentFragment(), Bookmarkable { if (isLocked) return // Remove all the subheaders and stuff. - val groups = recyclerAdapter.groups + val groups = recyclerAdapter?.groups ?: arrayListOf() - val moduleItemsArray = groups.indices.mapTo(ArrayList()) { recyclerAdapter.getItems(groups[it]) } + val moduleItemsArray = groups.indices.mapTo(ArrayList()) { recyclerAdapter?.getItems(groups[it]) ?: arrayListOf() } val moduleHelper = ModuleProgressionUtility.prepareModulesForCourseProgression( requireContext(), moduleItem.id, groups, moduleItemsArray ) @@ -184,16 +184,16 @@ class ModuleListFragment : ParentFragment(), Bookmarkable { // We need to force the empty view to be visible to use it for errors on refresh recyclerBinding.emptyView.setVisible() setEmptyView(recyclerBinding.emptyView, R.drawable.ic_panda_nomodules, R.string.modulesLocked, R.string.modulesLockedSubtext) - } else if (recyclerAdapter.size() == 0) { + } else if (recyclerAdapter?.size() == 0) { setEmptyView(recyclerBinding.emptyView, R.drawable.ic_panda_nomodules, R.string.noModules, R.string.noModulesSubtext) } else if (!arguments?.getString(MODULE_ID).isNullOrEmpty()) { // We need to delay scrolling until the expand animation has completed, otherwise modules // that appear near the end of the list will not have the extra 'expanded' space needed // to scroll as far as possible toward the top recyclerBinding.listView.postDelayed({ - val groupPosition = recyclerAdapter.getGroupItemPosition(arguments!!.getString( + val groupPosition = recyclerAdapter?.getGroupItemPosition(arguments!!.getString( MODULE_ID - )!!.toLong()) + )!!.toLong()) ?: -1 if (groupPosition >= 0) { val lm = recyclerBinding.listView.layoutManager as? LinearLayoutManager lm?.scrollToPositionWithOffset(groupPosition, 0) @@ -203,22 +203,24 @@ class ModuleListFragment : ParentFragment(), Bookmarkable { } } }) - configureRecyclerView(requireView(), requireContext(), recyclerAdapter, R.id.swipeRefreshLayout, R.id.emptyView, R.id.listView) + recyclerAdapter?.let { + configureRecyclerView(requireView(), requireContext(), it, R.id.swipeRefreshLayout, R.id.emptyView, R.id.listView) + } } fun notifyOfItemChanged(`object`: ModuleObject?, item: ModuleItem?) { if (item == null || `object` == null) return - recyclerAdapter.addOrUpdateItem(`object`, item) + recyclerAdapter?.addOrUpdateItem(`object`, item) } - fun refreshModuleList() = recyclerAdapter.updateMasteryPathItems() + fun refreshModuleList() = recyclerAdapter?.updateMasteryPathItems() /** * Update the list without clearing the data or collapsing headers. Used to update possibly updated * items (like a page that has now been viewed) */ - private fun updateList(moduleObject: ModuleObject) = recyclerAdapter.updateWithoutResettingViews(moduleObject) + private fun updateList(moduleObject: ModuleObject) = recyclerAdapter?.updateWithoutResettingViews(moduleObject) // region Bus Events @@ -227,7 +229,7 @@ class ModuleListFragment : ParentFragment(), Bookmarkable { fun onModuleUpdated(event: ModuleUpdatedEvent) { event.once(javaClass.simpleName) { updateList(it) - recyclerAdapter.notifyDataSetChanged() + recyclerAdapter?.notifyDataSetChanged() } } // endregion diff --git a/apps/student/src/main/java/com/instructure/student/features/pages/list/PageListFragment.kt b/apps/student/src/main/java/com/instructure/student/features/pages/list/PageListFragment.kt index 811db00ae2..740d0c4816 100644 --- a/apps/student/src/main/java/com/instructure/student/features/pages/list/PageListFragment.kt +++ b/apps/student/src/main/java/com/instructure/student/features/pages/list/PageListFragment.kt @@ -62,7 +62,7 @@ class PageListFragment : ParentFragment(), Bookmarkable { private var canvasContext: CanvasContext by ParcelableArg(key = Const.CANVAS_CONTEXT) - private lateinit var recyclerAdapter: PageListRecyclerAdapter + private var recyclerAdapter: PageListRecyclerAdapter? = null private var defaultSelectedPageTitle = PageListRecyclerAdapter.FRONT_PAGE_DETERMINER // blank string is used to determine front page private var isShowFrontPage by BooleanArg(key = SHOW_FRONT_PAGE) @@ -73,7 +73,7 @@ class PageListFragment : ParentFragment(), Bookmarkable { @Subscribe fun onUpdatePage(event: PageUpdatedEvent) { event.once(javaClass.simpleName) { - recyclerAdapter.refresh() + recyclerAdapter?.refresh() } } @@ -88,7 +88,7 @@ class PageListFragment : ParentFragment(), Bookmarkable { } override fun onDestroyView() { - recyclerAdapter.cancel() + recyclerAdapter?.cancel() super.onDestroyView() } @@ -120,7 +120,9 @@ class PageListFragment : ParentFragment(), Bookmarkable { } }, defaultSelectedPageTitle) - configureRecyclerView(rootView!!, requireContext(), recyclerAdapter, R.id.swipeRefreshLayout, R.id.emptyView, R.id.listView) + recyclerAdapter?.let { + configureRecyclerView(rootView!!, requireContext(), it, R.id.swipeRefreshLayout, R.id.emptyView, R.id.listView) + } } override fun onActivityCreated(savedInstanceState: Bundle?) { @@ -137,7 +139,9 @@ class PageListFragment : ParentFragment(), Bookmarkable { override fun onConfigurationChanged(newConfig: Configuration) { super.onConfigurationChanged(newConfig) - configureRecyclerView(rootView!!, requireContext(), recyclerAdapter, R.id.swipeRefreshLayout, R.id.emptyView, R.id.listView) + recyclerAdapter?.let { + configureRecyclerView(rootView!!, requireContext(), it, R.id.swipeRefreshLayout, R.id.emptyView, R.id.listView) + } recyclerBinding.emptyView.changeTextSize() if (resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT) { if (isTablet) { @@ -172,7 +176,7 @@ class PageListFragment : ParentFragment(), Bookmarkable { } else { recyclerBinding.emptyView.emptyViewText(getString(R.string.noItemsMatchingQuery, query)) } - recyclerAdapter.searchQuery = query + recyclerAdapter?.searchQuery = query } ViewStyler.themeToolbarColored(requireActivity(), toolbar, canvasContext) } diff --git a/apps/student/src/main/java/com/instructure/student/fragment/NotificationListFragment.kt b/apps/student/src/main/java/com/instructure/student/fragment/NotificationListFragment.kt index cdb1024802..d1adbbfd09 100644 --- a/apps/student/src/main/java/com/instructure/student/fragment/NotificationListFragment.kt +++ b/apps/student/src/main/java/com/instructure/student/fragment/NotificationListFragment.kt @@ -65,19 +65,19 @@ class NotificationListFragment : ParentFragment(), Bookmarkable, FragmentManager private var canvasContext by ParcelableArg(key = Const.CANVAS_CONTEXT) - private lateinit var recyclerAdapter: NotificationListRecyclerAdapter + private var recyclerAdapter: NotificationListRecyclerAdapter? = null private var adapterToFragmentCallback: NotificationAdapterToFragmentCallback = object : NotificationAdapterToFragmentCallback { override fun onRowClicked(streamItem: StreamItem, position: Int, isOpenDetail: Boolean) { - recyclerAdapter.setSelectedPosition(position) + recyclerAdapter?.setSelectedPosition(position) onRowClick(streamItem) } override fun onRefreshFinished() { setRefreshing(false) binding.editOptions.setGone() - if (recyclerAdapter.size() == 0) { + if (recyclerAdapter?.size() == 0) { setEmptyView(recyclerBinding.emptyView, R.drawable.ic_panda_noalerts, R.string.noNotifications, R.string.noNotificationsSubtext) if (resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE) { recyclerBinding.emptyView.setGuidelines(.2f, .7f, .74f, .15f, .85f) @@ -112,21 +112,23 @@ class NotificationListFragment : ParentFragment(), Bookmarkable, FragmentManager override fun onViewCreated(view: View, savedInstanceState: Bundle?) { recyclerBinding = PandaRecyclerRefreshLayoutBinding.bind(binding.root) recyclerAdapter = NotificationListRecyclerAdapter(requireContext(), canvasContext, adapterToFragmentCallback) - configureRecyclerView( - view, - requireContext(), - recyclerAdapter, - R.id.swipeRefreshLayout, - R.id.emptyView, - R.id.listView - ) + recyclerAdapter?.let { + configureRecyclerView( + view, + requireContext(), + it, + R.id.swipeRefreshLayout, + R.id.emptyView, + R.id.listView + ) + } recyclerBinding.listView.isSelectionEnabled = false binding.confirmButton.text = getString(R.string.delete) - binding.confirmButton.setOnClickListener { recyclerAdapter.confirmButtonClicked() } + binding.confirmButton.setOnClickListener { recyclerAdapter?.confirmButtonClicked() } binding.cancelButton.text = getString(R.string.cancel) - binding.cancelButton.setOnClickListener { recyclerAdapter.cancelButtonClicked() } + binding.cancelButton.setOnClickListener { recyclerAdapter?.cancelButtonClicked() } applyTheme() @@ -139,14 +141,14 @@ class NotificationListFragment : ParentFragment(), Bookmarkable, FragmentManager if (activity?.supportFragmentManager?.fragments?.lastOrNull()?.javaClass == this.javaClass) { if (shouldRefreshOnResume) { recyclerBinding.swipeRefreshLayout.isRefreshing = true - recyclerAdapter.refresh() + recyclerAdapter?.refresh() shouldRefreshOnResume = false } } } override fun onDestroyView() { - recyclerAdapter.cancel() + recyclerAdapter?.cancel() activity?.supportFragmentManager?.removeOnBackStackChangedListener(this) super.onDestroyView() } @@ -170,16 +172,18 @@ class NotificationListFragment : ParentFragment(), Bookmarkable, FragmentManager override fun onConfigurationChanged(newConfig: Configuration) { super.onConfigurationChanged(newConfig) - configureRecyclerView( - requireView(), - requireContext(), - recyclerAdapter, - R.id.swipeRefreshLayout, - R.id.emptyView, - R.id.listView, + recyclerAdapter?.let { + configureRecyclerView( + requireView(), + requireContext(), + it, + R.id.swipeRefreshLayout, + R.id.emptyView, + R.id.listView, R.string.noNotifications - ) - if (recyclerAdapter.size() == 0) { + ) + } + if (recyclerAdapter?.size() == 0) { recyclerBinding.emptyView.changeTextSize() if (resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT) { if (isTablet) { diff --git a/apps/student/src/main/java/com/instructure/student/fragment/ToDoListFragment.kt b/apps/student/src/main/java/com/instructure/student/fragment/ToDoListFragment.kt index dc9b95e04f..0ffb0d1a1d 100644 --- a/apps/student/src/main/java/com/instructure/student/fragment/ToDoListFragment.kt +++ b/apps/student/src/main/java/com/instructure/student/fragment/ToDoListFragment.kt @@ -51,11 +51,11 @@ class ToDoListFragment : ParentFragment() { private var canvasContext by ParcelableArg(key = Const.CANVAS_CONTEXT) - private lateinit var recyclerAdapter: TodoListRecyclerAdapter + private var recyclerAdapter: TodoListRecyclerAdapter? = null private var adapterToFragmentCallback: NotificationAdapterToFragmentCallback = object : NotificationAdapterToFragmentCallback { override fun onRowClicked(todo: ToDo, position: Int, isOpenDetail: Boolean) { - recyclerAdapter.setSelectedPosition(position) + recyclerAdapter?.setSelectedPosition(position) onRowClick(todo) } @@ -63,7 +63,7 @@ class ToDoListFragment : ParentFragment() { if (!isAdded) return setRefreshing(false) binding.editOptions.setGone() - if (recyclerAdapter.size() == 0) { + if (recyclerAdapter?.size() == 0) { setEmptyView(recyclerViewBinding.emptyView, R.drawable.ic_panda_sleeping, R.string.noTodos, R.string.noTodosSubtext) } } @@ -91,26 +91,28 @@ class ToDoListFragment : ParentFragment() { } } recyclerAdapter = TodoListRecyclerAdapter(requireContext(), canvasContext, adapterToFragmentCallback) - configureRecyclerView( - view, - requireContext(), - recyclerAdapter, - R.id.swipeRefreshLayout, - R.id.emptyView, - R.id.listView - ) + recyclerAdapter?.let { + configureRecyclerView( + view, + requireContext(), + it, + R.id.swipeRefreshLayout, + R.id.emptyView, + R.id.listView + ) + } recyclerViewBinding.listView.isSelectionEnabled = false binding.confirmButton.text = getString(R.string.markAsDone) - binding.confirmButton.setOnClickListener { recyclerAdapter.confirmButtonClicked() } + binding.confirmButton.setOnClickListener { recyclerAdapter?.confirmButtonClicked() } binding.cancelButton.setText(R.string.cancel) - binding.cancelButton.setOnClickListener { recyclerAdapter.cancelButtonClicked() } + binding.cancelButton.setOnClickListener { recyclerAdapter?.cancelButtonClicked() } - updateFilterTitle(recyclerAdapter.getFilterMode()) + updateFilterTitle(recyclerAdapter?.getFilterMode() ?: NoFilter) binding.clearFilterTextView.setOnClickListener { - recyclerAdapter.loadDataWithFilter(NoFilter) - updateFilterTitle(recyclerAdapter.getFilterMode()) + recyclerAdapter?.loadDataWithFilter(NoFilter) + updateFilterTitle(recyclerAdapter?.getFilterMode() ?: NoFilter) } } @@ -132,16 +134,18 @@ class ToDoListFragment : ParentFragment() { override fun onConfigurationChanged(newConfig: Configuration) { super.onConfigurationChanged(newConfig) - configureRecyclerView( - requireView(), - requireContext(), - recyclerAdapter, - R.id.swipeRefreshLayout, - R.id.emptyView, - R.id.listView, + recyclerAdapter?.let { + configureRecyclerView( + requireView(), + requireContext(), + it, + R.id.swipeRefreshLayout, + R.id.emptyView, + R.id.listView, R.string.noTodos - ) - if (recyclerAdapter.size() == 0) { + ) + } + if (recyclerAdapter?.size() == 0) { recyclerViewBinding.emptyView.changeTextSize() if (resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT) { if (isTablet) { @@ -184,15 +188,15 @@ class ToDoListFragment : ParentFragment() { private fun showCourseFilterDialog() { val choices = arrayOf(getString(R.string.favoritedCoursesLabel)) - var checkedItem = choices.indexOf(getString(recyclerAdapter.getFilterMode().titleId)) + var checkedItem = choices.indexOf(getString(recyclerAdapter?.getFilterMode()?.titleId ?: NoFilter.titleId)) val dialog = AlertDialog.Builder(requireContext()) .setTitle(R.string.filterByEllipsis) .setSingleChoiceItems(choices, checkedItem) { _, index -> checkedItem = index }.setPositiveButton(android.R.string.ok) { _, _ -> - if (checkedItem >= 0) recyclerAdapter.loadDataWithFilter(convertFilterChoiceToMode(choices[checkedItem])) - updateFilterTitle(recyclerAdapter.getFilterMode()) + if (checkedItem >= 0) recyclerAdapter?.loadDataWithFilter(convertFilterChoiceToMode(choices[checkedItem])) + updateFilterTitle(recyclerAdapter?.getFilterMode() ?: NoFilter) }.setNegativeButton(android.R.string.cancel, null) .create() @@ -216,7 +220,7 @@ class ToDoListFragment : ParentFragment() { override fun onDestroyView() { super.onDestroyView() - recyclerAdapter.cancel() + recyclerAdapter?.cancel() } companion object { From 2899517753b3b22474444d31af9eea6386899fa4 Mon Sep 17 00:00:00 2001 From: Akos Hermann <72087159+hermannakos@users.noreply.github.com> Date: Tue, 14 Nov 2023 16:10:56 +0100 Subject: [PATCH 007/132] [MBL-17180][Student] Sync fixes (#2247) Test plan: Test the sync. Check the background progresses after cancel, restart, etc. refs: MBL-17180 affects: Student release note: none --- .../student/tasks/StudentLogoutTask.kt | 5 +- .../1.json | 12 +- .../offline/daos/CourseSyncProgressDaoTest.kt | 17 +- .../offline/daos/FileSyncProgressDaoTest.kt | 5 +- .../pandautils/di/OfflineSyncModule.kt | 105 ++++++++++++ .../offline/sync/AggregateProgressObserver.kt | 4 +- .../{CourseSyncWorker.kt => CourseSync.kt} | 157 ++++++++++-------- .../features/offline/sync/FileSync.kt | 1 + .../offline/sync/OfflineSyncHelper.kt | 87 +++++++--- .../offline/sync/OfflineSyncWorker.kt | 68 +++----- .../sync/progress/SyncProgressViewData.kt | 7 +- .../sync/progress/SyncProgressViewModel.kt | 13 +- .../AdditionalFilesProgressItemViewModel.kt | 2 +- .../CourseProgressItemViewModel.kt | 8 +- .../FilesTabProgressItemViewModel.kt | 2 +- .../TabProgressItemViewModel.kt | 2 +- .../offline/daos/CourseSyncProgressDao.kt | 4 +- .../entities/CourseSyncProgressEntity.kt | 1 - .../offline/sync/OfflineSyncHelperTest.kt | 120 ++++++++++++- .../progress/AggregateProgressObserverTest.kt | 27 +-- .../progress/SyncProgressViewModelTest.kt | 36 ++-- ...dditionalFilesProgressItemViewModelTest.kt | 32 ++-- .../CourseProgressItemViewModelTest.kt | 44 ++--- .../FilesTabProgressItemViewModelTest.kt | 29 ++-- .../TabProgressItemViewModelTest.kt | 10 +- 25 files changed, 489 insertions(+), 309 deletions(-) rename libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/sync/{CourseSyncWorker.kt => CourseSync.kt} (82%) diff --git a/apps/student/src/main/java/com/instructure/student/tasks/StudentLogoutTask.kt b/apps/student/src/main/java/com/instructure/student/tasks/StudentLogoutTask.kt index ec829ffbc1..59360f590b 100644 --- a/apps/student/src/main/java/com/instructure/student/tasks/StudentLogoutTask.kt +++ b/apps/student/src/main/java/com/instructure/student/tasks/StudentLogoutTask.kt @@ -25,7 +25,6 @@ import com.heapanalytics.android.Heap import com.instructure.canvasapi2.utils.ContextKeeper import com.instructure.canvasapi2.utils.tryOrNull import com.instructure.loginapi.login.tasks.LogoutTask -import com.instructure.pandautils.features.offline.sync.CourseSyncWorker import com.instructure.pandautils.features.offline.sync.OfflineSyncWorker import com.instructure.pandautils.room.offline.DatabaseProvider import com.instructure.pandautils.typeface.TypefaceBehavior @@ -79,8 +78,8 @@ class StudentLogoutTask( override fun stopOfflineSync() { val workManager = WorkManager.getInstance(ContextKeeper.appContext) workManager.apply { - cancelAllWorkByTag(CourseSyncWorker.TAG) - cancelAllWorkByTag(OfflineSyncWorker.TAG) + cancelAllWorkByTag(OfflineSyncWorker.PERIODIC_TAG) + cancelAllWorkByTag(OfflineSyncWorker.ONE_TIME_TAG) } } } diff --git a/libs/pandautils/schemas/com.instructure.pandautils.room.offline.OfflineDatabase/1.json b/libs/pandautils/schemas/com.instructure.pandautils.room.offline.OfflineDatabase/1.json index 31ef763ae0..7c8f1b4539 100644 --- a/libs/pandautils/schemas/com.instructure.pandautils.room.offline.OfflineDatabase/1.json +++ b/libs/pandautils/schemas/com.instructure.pandautils.room.offline.OfflineDatabase/1.json @@ -2,7 +2,7 @@ "formatVersion": 1, "database": { "version": 1, - "identityHash": "8fa86dcf6c28df2a7e77dee7a0e3c7c6", + "identityHash": "a5fc40aae4d227313a98d9a89f41cfdd", "entities": [ { "tableName": "AssignmentDueDateEntity", @@ -5393,7 +5393,7 @@ }, { "tableName": "CourseSyncProgressEntity", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`courseId` INTEGER NOT NULL, `workerId` TEXT NOT NULL, `courseName` TEXT NOT NULL, `tabs` TEXT NOT NULL, `additionalFilesStarted` INTEGER NOT NULL, `progressState` TEXT NOT NULL, PRIMARY KEY(`courseId`))", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`courseId` INTEGER NOT NULL, `courseName` TEXT NOT NULL, `tabs` TEXT NOT NULL, `additionalFilesStarted` INTEGER NOT NULL, `progressState` TEXT NOT NULL, PRIMARY KEY(`courseId`))", "fields": [ { "fieldPath": "courseId", @@ -5401,12 +5401,6 @@ "affinity": "INTEGER", "notNull": true }, - { - "fieldPath": "workerId", - "columnName": "workerId", - "affinity": "TEXT", - "notNull": true - }, { "fieldPath": "courseName", "columnName": "courseName", @@ -5513,7 +5507,7 @@ "views": [], "setupQueries": [ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '8fa86dcf6c28df2a7e77dee7a0e3c7c6')" + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'a5fc40aae4d227313a98d9a89f41cfdd')" ] } } \ No newline at end of file diff --git a/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/CourseSyncProgressDaoTest.kt b/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/CourseSyncProgressDaoTest.kt index 4f6432bacf..f16738ee82 100644 --- a/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/CourseSyncProgressDaoTest.kt +++ b/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/CourseSyncProgressDaoTest.kt @@ -66,7 +66,6 @@ class CourseSyncProgressDaoTest { fun testInsertError() = runTest { val entity = CourseSyncProgressEntity( 1L, - UUID.randomUUID().toString(), "Course 1", CourseSyncSettingsEntity.TABS.associateWith { TabSyncData(it, ProgressState.IN_PROGRESS) }, progressState = ProgressState.IN_PROGRESS @@ -82,14 +81,12 @@ class CourseSyncProgressDaoTest { val entities = listOf( CourseSyncProgressEntity( 1L, - UUID.randomUUID().toString(), "Course 1", CourseSyncSettingsEntity.TABS.associateWith { TabSyncData(it, ProgressState.IN_PROGRESS) }, progressState = ProgressState.IN_PROGRESS ), CourseSyncProgressEntity( 2L, - UUID.randomUUID().toString(), "Course 2", CourseSyncSettingsEntity.TABS.associateWith { TabSyncData(it, ProgressState.IN_PROGRESS) }, progressState = ProgressState.IN_PROGRESS @@ -110,14 +107,12 @@ class CourseSyncProgressDaoTest { val entities = listOf( CourseSyncProgressEntity( 1L, - UUID.randomUUID().toString(), "Course 1", CourseSyncSettingsEntity.TABS.associateWith { TabSyncData(it, ProgressState.IN_PROGRESS) }, progressState = ProgressState.IN_PROGRESS ), CourseSyncProgressEntity( 2L, - UUID.randomUUID().toString(), "Course 2", CourseSyncSettingsEntity.TABS.associateWith { TabSyncData(it, ProgressState.IN_PROGRESS) }, progressState = ProgressState.IN_PROGRESS @@ -135,14 +130,12 @@ class CourseSyncProgressDaoTest { val entities = listOf( CourseSyncProgressEntity( 1L, - UUID.randomUUID().toString(), "Course 1", CourseSyncSettingsEntity.TABS.associateWith { TabSyncData(it, ProgressState.IN_PROGRESS) }, progressState = ProgressState.IN_PROGRESS ), CourseSyncProgressEntity( 2L, - UUID.randomUUID().toString(), "Course 2", CourseSyncSettingsEntity.TABS.associateWith { TabSyncData(it, ProgressState.IN_PROGRESS) }, progressState = ProgressState.IN_PROGRESS @@ -161,14 +154,12 @@ class CourseSyncProgressDaoTest { val entities = listOf( CourseSyncProgressEntity( 1L, - UUID.randomUUID().toString(), "Course 1", CourseSyncSettingsEntity.TABS.associateWith { TabSyncData(it, ProgressState.IN_PROGRESS) }, progressState = ProgressState.IN_PROGRESS ), CourseSyncProgressEntity( 2L, - UUID.randomUUID().toString(), "Course 2", CourseSyncSettingsEntity.TABS.associateWith { TabSyncData(it, ProgressState.IN_PROGRESS) }, progressState = ProgressState.IN_PROGRESS @@ -188,14 +179,12 @@ class CourseSyncProgressDaoTest { val entities = listOf( CourseSyncProgressEntity( 1L, - UUID.randomUUID().toString(), "Course 1", CourseSyncSettingsEntity.TABS.associateWith { TabSyncData(it, ProgressState.IN_PROGRESS) }, progressState = ProgressState.IN_PROGRESS ), CourseSyncProgressEntity( 2L, - UUID.randomUUID().toString(), "Course 2", CourseSyncSettingsEntity.TABS.associateWith { TabSyncData(it, ProgressState.IN_PROGRESS) }, progressState = ProgressState.IN_PROGRESS @@ -211,18 +200,16 @@ class CourseSyncProgressDaoTest { } @Test - fun testFindByWorkerIdLiveData() = runTest { + fun testFindByCourseIdLiveData() = runTest { val entities = listOf( CourseSyncProgressEntity( 1L, - UUID.randomUUID().toString(), "Course 1", CourseSyncSettingsEntity.TABS.associateWith { TabSyncData(it, ProgressState.IN_PROGRESS) }, progressState = ProgressState.IN_PROGRESS ), CourseSyncProgressEntity( 2L, - UUID.randomUUID().toString(), "Course 2", CourseSyncSettingsEntity.TABS.associateWith { TabSyncData(it, ProgressState.IN_PROGRESS) }, progressState = ProgressState.IN_PROGRESS @@ -231,7 +218,7 @@ class CourseSyncProgressDaoTest { courseSyncProgressDao.insertAll(entities) - val result = courseSyncProgressDao.findByWorkerIdLiveData(entities[1].workerId) + val result = courseSyncProgressDao.findByCourseIdLiveData(entities[1].courseId) result.observeForever { } assertEquals(entities[1], result.value) diff --git a/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/FileSyncProgressDaoTest.kt b/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/FileSyncProgressDaoTest.kt index 6f8f1d177d..6802ae1119 100644 --- a/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/FileSyncProgressDaoTest.kt +++ b/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/FileSyncProgressDaoTest.kt @@ -57,7 +57,6 @@ class FileSyncProgressDaoTest { courseSyncProgressDao.insert( CourseSyncProgressEntity( - workerId = "workerId", courseId = 1L, courseName = "Course 1" ) @@ -192,7 +191,7 @@ class FileSyncProgressDaoTest { @Test fun testFindByCourseIdLiveData() = runTest { - courseSyncProgressDao.insert(CourseSyncProgressEntity(2L, "workerId2", "Course 2")) + courseSyncProgressDao.insert(CourseSyncProgressEntity(2L, "Course 2")) val entities = listOf( FileSyncProgressEntity( courseId = 1L, @@ -229,7 +228,7 @@ class FileSyncProgressDaoTest { @Test fun testFindAllLiveData() = runTest { - courseSyncProgressDao.insert(CourseSyncProgressEntity(2L, "workerId2", "Course 2")) + courseSyncProgressDao.insert(CourseSyncProgressEntity(2L, "Course 2")) val entities = listOf( FileSyncProgressEntity( courseId = 1L, diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/di/OfflineSyncModule.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/di/OfflineSyncModule.kt index 906328a7d6..586864e0ad 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/di/OfflineSyncModule.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/di/OfflineSyncModule.kt @@ -20,18 +20,48 @@ package com.instructure.pandautils.di import android.content.Context import androidx.work.WorkManager import com.google.firebase.crashlytics.FirebaseCrashlytics +import com.instructure.canvasapi2.apis.AnnouncementAPI +import com.instructure.canvasapi2.apis.AssignmentAPI +import com.instructure.canvasapi2.apis.CalendarEventAPI +import com.instructure.canvasapi2.apis.ConferencesApi +import com.instructure.canvasapi2.apis.CourseAPI +import com.instructure.canvasapi2.apis.DiscussionAPI +import com.instructure.canvasapi2.apis.EnrollmentAPI +import com.instructure.canvasapi2.apis.FeaturesAPI import com.instructure.canvasapi2.apis.FileDownloadAPI import com.instructure.canvasapi2.apis.FileFolderAPI +import com.instructure.canvasapi2.apis.GroupAPI +import com.instructure.canvasapi2.apis.ModuleAPI +import com.instructure.canvasapi2.apis.PageAPI +import com.instructure.canvasapi2.apis.QuizAPI +import com.instructure.canvasapi2.apis.UserAPI import com.instructure.canvasapi2.utils.ApiPrefs +import com.instructure.pandautils.features.offline.offlinecontent.CourseFileSharedRepository import com.instructure.pandautils.features.offline.sync.AggregateProgressObserver +import com.instructure.pandautils.features.offline.sync.CourseSync import com.instructure.pandautils.features.offline.sync.FileSync +import com.instructure.pandautils.features.offline.sync.HtmlParser import com.instructure.pandautils.features.offline.sync.OfflineSyncHelper +import com.instructure.pandautils.room.offline.daos.CourseFeaturesDao import com.instructure.pandautils.room.offline.daos.CourseSyncProgressDao +import com.instructure.pandautils.room.offline.daos.CourseSyncSettingsDao import com.instructure.pandautils.room.offline.daos.FileFolderDao import com.instructure.pandautils.room.offline.daos.FileSyncProgressDao import com.instructure.pandautils.room.offline.daos.FileSyncSettingsDao import com.instructure.pandautils.room.offline.daos.LocalFileDao +import com.instructure.pandautils.room.offline.daos.PageDao +import com.instructure.pandautils.room.offline.daos.QuizDao +import com.instructure.pandautils.room.offline.facade.AssignmentFacade +import com.instructure.pandautils.room.offline.facade.ConferenceFacade +import com.instructure.pandautils.room.offline.facade.CourseFacade +import com.instructure.pandautils.room.offline.facade.DiscussionTopicFacade +import com.instructure.pandautils.room.offline.facade.DiscussionTopicHeaderFacade +import com.instructure.pandautils.room.offline.facade.GroupFacade +import com.instructure.pandautils.room.offline.facade.ModuleFacade +import com.instructure.pandautils.room.offline.facade.PageFacade +import com.instructure.pandautils.room.offline.facade.ScheduleItemFacade import com.instructure.pandautils.room.offline.facade.SyncSettingsFacade +import com.instructure.pandautils.room.offline.facade.UserFacade import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -76,4 +106,79 @@ class OfflineSyncModule { fileFolderApi ) } + + @Provides + fun provideCourseSync( + courseApi: CourseAPI.CoursesInterface, + pageApi: PageAPI.PagesInterface, + userApi: UserAPI.UsersInterface, + assignmentApi: AssignmentAPI.AssignmentInterface, + calendarEventApi: CalendarEventAPI.CalendarEventInterface, + courseSyncSettingsDao: CourseSyncSettingsDao, + pageFacade: PageFacade, + userFacade: UserFacade, + courseFacade: CourseFacade, + assignmentFacade: AssignmentFacade, + quizDao: QuizDao, + quizApi: QuizAPI.QuizInterface, + scheduleItemFacade: ScheduleItemFacade, + conferencesApi: ConferencesApi.ConferencesInterface, + conferenceFacade: ConferenceFacade, + discussionApi: DiscussionAPI.DiscussionInterface, + discussionTopicHeaderFacade: DiscussionTopicHeaderFacade, + announcementApi: AnnouncementAPI.AnnouncementInterface, + moduleApi: ModuleAPI.ModuleInterface, + moduleFacade: ModuleFacade, + featuresApi: FeaturesAPI.FeaturesInterface, + courseFeaturesDao: CourseFeaturesDao, + courseFileSharedRepository: CourseFileSharedRepository, + fileFolderDao: FileFolderDao, + discussionTopicFacade: DiscussionTopicFacade, + groupApi: GroupAPI.GroupInterface, + groupFacade: GroupFacade, + enrollmentsApi: EnrollmentAPI.EnrollmentInterface, + courseSyncProgressDao: CourseSyncProgressDao, + htmlParser: HtmlParser, + fileFolderApi: FileFolderAPI.FilesFoldersInterface, + pageDao: PageDao, + firebaseCrashlytics: FirebaseCrashlytics, + fileSync: FileSync + ): CourseSync { + return CourseSync( + courseApi, + pageApi, + userApi, + assignmentApi, + calendarEventApi, + courseSyncSettingsDao, + pageFacade, + userFacade, + courseFacade, + assignmentFacade, + quizDao, + quizApi, + scheduleItemFacade, + conferencesApi, + conferenceFacade, + discussionApi, + discussionTopicHeaderFacade, + announcementApi, + moduleApi, + moduleFacade, + featuresApi, + courseFeaturesDao, + courseFileSharedRepository, + fileFolderDao, + discussionTopicFacade, + groupApi, + groupFacade, + enrollmentsApi, + courseSyncProgressDao, + htmlParser, + fileFolderApi, + pageDao, + firebaseCrashlytics, + fileSync + ) + } } \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/sync/AggregateProgressObserver.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/sync/AggregateProgressObserver.kt index dcd38aaf6d..a1e8acaca3 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/sync/AggregateProgressObserver.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/sync/AggregateProgressObserver.kt @@ -42,11 +42,11 @@ class AggregateProgressObserver( private var courseProgressLiveData: LiveData>? = null private var fileProgressLiveData: LiveData>? = null - private var courseProgresses = mutableMapOf() + private var courseProgresses = mutableMapOf() private var fileProgresses = mutableMapOf() private val courseProgressObserver = Observer> { - courseProgresses = it.associateBy { it.workerId }.toMutableMap() + courseProgresses = it.associateBy { it.courseId }.toMutableMap() calculateProgress() } diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/sync/CourseSyncWorker.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/sync/CourseSync.kt similarity index 82% rename from libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/sync/CourseSyncWorker.kt rename to libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/sync/CourseSync.kt index a58c657948..2cfe45356d 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/sync/CourseSyncWorker.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/sync/CourseSync.kt @@ -18,35 +18,63 @@ package com.instructure.pandautils.features.offline.sync -import android.content.Context -import android.net.Uri -import androidx.hilt.work.HiltWorker -import androidx.work.* import com.google.firebase.crashlytics.FirebaseCrashlytics -import com.instructure.canvasapi2.apis.* +import com.instructure.canvasapi2.apis.AnnouncementAPI +import com.instructure.canvasapi2.apis.AssignmentAPI +import com.instructure.canvasapi2.apis.CalendarEventAPI +import com.instructure.canvasapi2.apis.ConferencesApi +import com.instructure.canvasapi2.apis.CourseAPI +import com.instructure.canvasapi2.apis.DiscussionAPI +import com.instructure.canvasapi2.apis.EnrollmentAPI +import com.instructure.canvasapi2.apis.FeaturesAPI +import com.instructure.canvasapi2.apis.FileFolderAPI +import com.instructure.canvasapi2.apis.GroupAPI +import com.instructure.canvasapi2.apis.ModuleAPI +import com.instructure.canvasapi2.apis.PageAPI +import com.instructure.canvasapi2.apis.QuizAPI +import com.instructure.canvasapi2.apis.UserAPI import com.instructure.canvasapi2.builders.RestParams -import com.instructure.canvasapi2.models.* +import com.instructure.canvasapi2.models.AssignmentGroup +import com.instructure.canvasapi2.models.CanvasContext +import com.instructure.canvasapi2.models.Conference +import com.instructure.canvasapi2.models.Course +import com.instructure.canvasapi2.models.DiscussionEntry +import com.instructure.canvasapi2.models.DiscussionTopic +import com.instructure.canvasapi2.models.DiscussionTopicHeader +import com.instructure.canvasapi2.models.ModuleItem +import com.instructure.canvasapi2.models.ScheduleItem +import com.instructure.canvasapi2.models.Tab import com.instructure.canvasapi2.utils.ApiPrefs import com.instructure.canvasapi2.utils.DataResult import com.instructure.canvasapi2.utils.depaginate import com.instructure.pandautils.features.offline.offlinecontent.CourseFileSharedRepository -import com.instructure.pandautils.room.offline.daos.* -import com.instructure.pandautils.room.offline.entities.* -import com.instructure.pandautils.room.offline.facade.* +import com.instructure.pandautils.room.offline.daos.CourseFeaturesDao +import com.instructure.pandautils.room.offline.daos.CourseSyncProgressDao +import com.instructure.pandautils.room.offline.daos.CourseSyncSettingsDao +import com.instructure.pandautils.room.offline.daos.FileFolderDao +import com.instructure.pandautils.room.offline.daos.PageDao +import com.instructure.pandautils.room.offline.daos.QuizDao +import com.instructure.pandautils.room.offline.entities.CourseFeaturesEntity +import com.instructure.pandautils.room.offline.entities.CourseSyncProgressEntity +import com.instructure.pandautils.room.offline.entities.CourseSyncSettingsEntity +import com.instructure.pandautils.room.offline.entities.FileFolderEntity +import com.instructure.pandautils.room.offline.entities.QuizEntity +import com.instructure.pandautils.room.offline.facade.AssignmentFacade +import com.instructure.pandautils.room.offline.facade.ConferenceFacade +import com.instructure.pandautils.room.offline.facade.CourseFacade +import com.instructure.pandautils.room.offline.facade.DiscussionTopicFacade +import com.instructure.pandautils.room.offline.facade.DiscussionTopicHeaderFacade +import com.instructure.pandautils.room.offline.facade.GroupFacade +import com.instructure.pandautils.room.offline.facade.ModuleFacade +import com.instructure.pandautils.room.offline.facade.PageFacade +import com.instructure.pandautils.room.offline.facade.ScheduleItemFacade +import com.instructure.pandautils.room.offline.facade.UserFacade import com.instructure.pandautils.room.offline.model.CourseSyncSettingsWithFiles -import dagger.assisted.Assisted -import dagger.assisted.AssistedInject import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.coroutineScope -import java.io.File -import java.lang.IllegalStateException -import java.util.Date - -@HiltWorker -class CourseSyncWorker @AssistedInject constructor( - @Assisted private val context: Context, - @Assisted private val workerParameters: WorkerParameters, + +class CourseSync( private val courseApi: CourseAPI.CoursesInterface, private val pageApi: PageAPI.PagesInterface, private val userApi: UserAPI.UsersInterface, @@ -81,20 +109,33 @@ class CourseSyncWorker @AssistedInject constructor( private val pageDao: PageDao, private val firebaseCrashlytics: FirebaseCrashlytics, private val fileSync: FileSync -) : CoroutineWorker(context, workerParameters) { +) { - private val additionalFileIdsToSync = mutableSetOf() - private val externalFilesToSync = mutableSetOf() + private val additionalFileIdsToSync = mutableMapOf>() + private val externalFilesToSync = mutableMapOf>() - private var courseId = -1L + private var isStopped = false + set(value) = synchronized(this) { + field = value + } - override suspend fun doWork(): Result { + suspend fun syncCourses(courseIds: List) { + coroutineScope { + courseIds.map { + async { syncCourse(it) } + }.awaitAll() + } + } + + private suspend fun syncCourse(courseId: Long) { val courseSettingsWithFiles = - courseSyncSettingsDao.findWithFilesById(inputData.getLong(COURSE_ID, -1)) ?: return Result.failure() + courseSyncSettingsDao.findWithFilesById(courseId) ?: return val courseSettings = courseSettingsWithFiles.courseSyncSettings - courseId = courseSettings.courseId val course = fetchCourseDetails(courseId) + additionalFileIdsToSync[courseId] = emptySet() + externalFilesToSync[courseId] = emptySet() + initProgress(courseSettings, course) if (courseSettings.fullFileSync || courseSettingsWithFiles.files.isNotEmpty()) { @@ -108,7 +149,7 @@ class CourseSyncWorker @AssistedInject constructor( listOf(contentDeferred, filesDeferred).awaitAll() } - fileSync.syncAdditionalFiles(courseSettings, additionalFileIdsToSync, externalFilesToSync) + fileSync.syncAdditionalFiles(courseSettings, additionalFileIdsToSync[courseId].orEmpty(), externalFilesToSync[courseId].orEmpty()) val progress = courseSyncProgressDao.findByCourseId(courseId) progress @@ -116,8 +157,6 @@ class CourseSyncWorker @AssistedInject constructor( ?.let { courseSyncProgressDao.update(it) } - - return Result.success() } private suspend fun fetchCourseContent(courseSettingsWithFiles: CourseSyncSettingsWithFiles, course: Course) { @@ -182,7 +221,7 @@ class CourseSyncWorker @AssistedInject constructor( } private suspend fun fetchSyllabus(courseId: Long) { - fetchTab(Tab.SYLLABUS_ID) { + fetchTab(courseId, Tab.SYLLABUS_ID) { val calendarEvents = fetchCalendarEvents(courseId) val assignmentEvents = fetchCalendarAssignments(courseId) val scheduleItems = mutableListOf() @@ -231,7 +270,7 @@ class CourseSyncWorker @AssistedInject constructor( } private suspend fun fetchPages(courseId: Long) { - fetchTab(Tab.PAGES_ID) { + fetchTab(courseId, Tab.PAGES_ID) { val params = RestParams(usePerPageQueryParam = true, isForceReadFromNetwork = true) val pages = pageApi.getFirstPagePagesWithBody(courseId, CanvasContext.Type.COURSE.apiString, params) .depaginate { nextUrl -> @@ -263,7 +302,7 @@ class CourseSyncWorker @AssistedInject constructor( } private suspend fun fetchAssignments(courseId: Long) { - fetchTab(Tab.ASSIGNMENTS_ID, Tab.GRADES_ID) { + fetchTab(courseId, Tab.ASSIGNMENTS_ID, Tab.GRADES_ID) { val restParams = RestParams(usePerPageQueryParam = true, isForceReadFromNetwork = true) val assignmentGroups = assignmentApi.getFirstPageAssignmentGroupListWithAssignments(courseId, restParams) .depaginate { nextUrl -> @@ -303,7 +342,7 @@ class CourseSyncWorker @AssistedInject constructor( } private suspend fun fetchUsers(courseId: Long) { - fetchTab(Tab.PEOPLE_ID) { + fetchTab(courseId, Tab.PEOPLE_ID) { val restParams = RestParams(usePerPageQueryParam = true, isForceReadFromNetwork = true) val users = userApi.getFirstPagePeopleList(courseId, CanvasContext.Type.COURSE.apiString, restParams) .depaginate { userApi.getNextPagePeopleList(it, restParams) }.dataOrThrow @@ -328,7 +367,7 @@ class CourseSyncWorker @AssistedInject constructor( } private suspend fun fetchAllQuizzes(contextType: String, courseId: Long) { - fetchTab(Tab.QUIZZES_ID) { + fetchTab(courseId, Tab.QUIZZES_ID) { val params = RestParams(usePerPageQueryParam = true, isForceReadFromNetwork = true) val quizzes = quizApi.getFirstPageQuizzesList(contextType, courseId, params).depaginate { nextUrl -> quizApi.getNextPageQuizzesList(nextUrl, params) @@ -343,7 +382,7 @@ class CourseSyncWorker @AssistedInject constructor( } private suspend fun fetchConferences(courseId: Long) { - fetchTab(Tab.CONFERENCES_ID) { + fetchTab(courseId, Tab.CONFERENCES_ID) { val conferences = getConferencesForContext(CanvasContext.emptyCourseContext(courseId), true).dataOrThrow conferenceFacade.insertConferences(conferences, courseId) @@ -363,7 +402,7 @@ class CourseSyncWorker @AssistedInject constructor( } private suspend fun fetchDiscussions(courseId: Long) { - fetchTab(Tab.DISCUSSIONS_ID) { + fetchTab(courseId, Tab.DISCUSSIONS_ID) { val params = RestParams(usePerPageQueryParam = true, isForceReadFromNetwork = true) val discussions = discussionApi.getFirstPageDiscussionTopicHeaders(CanvasContext.Type.COURSE.apiString, courseId, params) @@ -380,7 +419,7 @@ class CourseSyncWorker @AssistedInject constructor( } private suspend fun fetchAnnouncements(courseId: Long) { - fetchTab(Tab.ANNOUNCEMENTS_ID) { + fetchTab(courseId, Tab.ANNOUNCEMENTS_ID) { val params = RestParams(usePerPageQueryParam = true, isForceReadFromNetwork = true) val announcements = announcementApi.getFirstPageAnnouncementsList(CanvasContext.Type.COURSE.apiString, courseId, params) @@ -440,7 +479,7 @@ class CourseSyncWorker @AssistedInject constructor( } private suspend fun fetchModules(courseId: Long, courseSettings: CourseSyncSettingsWithFiles) { - fetchTab(Tab.MODULES_ID) { + fetchTab(courseId, Tab.MODULES_ID) { val params = RestParams(usePerPageQueryParam = true, isForceReadFromNetwork = true) val moduleObjects = moduleApi.getFirstPageModuleObjects( CanvasContext.Type.COURSE.apiString, courseId, params @@ -471,14 +510,14 @@ class CourseSyncWorker @AssistedInject constructor( } } - private suspend fun fetchTab(vararg tabIds: String, fetchBlock: suspend () -> Unit) { + private suspend fun fetchTab(courseId: Long, vararg tabIds: String, fetchBlock: suspend () -> Unit) { if (isStopped) return try { fetchBlock() - updateTabSuccess(*tabIds) + updateTabSuccess(courseId, *tabIds) } catch (e: Exception) { e.printStackTrace() - updateTabError(*tabIds) + updateTabError(courseId, *tabIds) firebaseCrashlytics.recordException(e) } } @@ -506,7 +545,9 @@ class CourseSyncWorker @AssistedInject constructor( val file = fileFolderApi.getCourseFile(courseId, it.contentId, params).dataOrNull if (file?.id != null) { - additionalFileIdsToSync.add(file.id) + additionalFileIdsToSync[courseId]?.let { + additionalFileIdsToSync[courseId] = it + file.id + } } } @@ -524,8 +565,12 @@ class CourseSyncWorker @AssistedInject constructor( private suspend fun parseHtmlContent(htmlContent: String?, courseId: Long): String? { val htmlParsingResult = htmlParser.createHtmlStringWithLocalFiles(htmlContent, courseId) - additionalFileIdsToSync.addAll(htmlParsingResult.internalFileIds) - externalFilesToSync.addAll(htmlParsingResult.externalFileUrls) + additionalFileIdsToSync[courseId]?.let { + additionalFileIdsToSync[courseId] = it + htmlParsingResult.internalFileIds + } + externalFilesToSync[courseId]?.let { + externalFilesToSync[courseId] = it + htmlParsingResult.externalFileUrls + } return htmlParsingResult.htmlWithLocalFileLinks } @@ -557,7 +602,6 @@ class CourseSyncWorker @AssistedInject constructor( private suspend fun createNewProgress(courseSettings: CourseSyncSettingsEntity): CourseSyncProgressEntity { val newProgress = CourseSyncProgressEntity( - workerId = workerParameters.id.toString(), courseId = courseSettings.courseId, courseName = courseSettings.courseName, progressState = ProgressState.STARTING, @@ -566,7 +610,7 @@ class CourseSyncWorker @AssistedInject constructor( return newProgress } - private suspend fun updateTabError(vararg tabIds: String) { + private suspend fun updateTabError(courseId: Long, vararg tabIds: String) { val progress = courseSyncProgressDao.findByCourseId(courseId) progress?.copy( tabs = progress.tabs.toMutableMap().apply { @@ -582,7 +626,7 @@ class CourseSyncWorker @AssistedInject constructor( } } - private suspend fun updateTabSuccess(vararg tabIds: String) { + private suspend fun updateTabSuccess(courseId: Long, vararg tabIds: String) { val progress = courseSyncProgressDao.findByCourseId(courseId) progress?.copy( tabs = progress.tabs.toMutableMap().apply { @@ -596,23 +640,4 @@ class CourseSyncWorker @AssistedInject constructor( courseSyncProgressDao.update(it) } } - - companion object { - const val COURSE_ID = "course_id" - const val TAG = "CourseSyncWorker" - - fun createOnTimeWork(courseId: Long, wifiOnly: Boolean): OneTimeWorkRequest { - val data = workDataOf(COURSE_ID to courseId) - return OneTimeWorkRequestBuilder() - .addTag(TAG) - .setInputData(data) - .setConstraints( - Constraints.Builder() - .setRequiredNetworkType(if (wifiOnly) NetworkType.UNMETERED else NetworkType.CONNECTED) - .setRequiresBatteryNotLow(true) - .build() - ) - .build() - } - } } \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/sync/FileSync.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/sync/FileSync.kt index 307264fc05..f67aa666f0 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/sync/FileSync.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/sync/FileSync.kt @@ -20,6 +20,7 @@ package com.instructure.pandautils.features.offline.sync import android.content.Context import android.net.Uri +import android.util.Log import com.google.firebase.crashlytics.FirebaseCrashlytics import com.instructure.canvasapi2.apis.DownloadState import com.instructure.canvasapi2.apis.FileDownloadAPI diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/sync/OfflineSyncHelper.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/sync/OfflineSyncHelper.kt index 7fdf30f45a..eb80412b8b 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/sync/OfflineSyncHelper.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/sync/OfflineSyncHelper.kt @@ -17,11 +17,18 @@ package com.instructure.pandautils.features.offline.sync -import androidx.work.* +import androidx.work.Constraints +import androidx.work.Data +import androidx.work.ExistingPeriodicWorkPolicy +import androidx.work.NetworkType +import androidx.work.OneTimeWorkRequest +import androidx.work.PeriodicWorkRequest +import androidx.work.WorkInfo +import androidx.work.WorkManager +import androidx.work.await import com.instructure.canvasapi2.utils.ApiPrefs import com.instructure.pandautils.features.offline.sync.settings.SyncFrequency import com.instructure.pandautils.room.offline.facade.SyncSettingsFacade -import java.util.* import java.util.concurrent.TimeUnit class OfflineSyncHelper( @@ -31,11 +38,22 @@ class OfflineSyncHelper( ) { suspend fun syncCourses(courseIds: List) { - cancelRunningWorkers() - if (isWorkScheduled() || !syncSettingsFacade.getSyncSettings().autoSyncEnabled) { - syncOnce(courseIds) - } else { - scheduleWork() + when { + isPeriodicWorkRunning() -> { + scheduleWork(ExistingPeriodicWorkPolicy.CANCEL_AND_REENQUEUE) + } + + isWorkScheduled() || !syncSettingsFacade.getSyncSettings().autoSyncEnabled -> { + val runningInfo = getRunningOneTimeWorkInfo() + if (runningInfo != null) { + workManager.cancelWorkById(runningInfo.id) + } + syncOnce(courseIds) + } + + else -> { + scheduleWork() + } } } @@ -44,58 +62,79 @@ class OfflineSyncHelper( } suspend fun updateWork() { - val id = workManager.getWorkInfosForUniqueWork(apiPrefs.user?.id.toString()).await().firstOrNull()?.id - val workRequest = createWorkRequest(id) + val workRequest = createPeriodicWorkRequest() workManager.updateWork(workRequest) } - suspend fun scheduleWork() { - val workRequest = createWorkRequest() + suspend fun scheduleWork(policy: ExistingPeriodicWorkPolicy = ExistingPeriodicWorkPolicy.UPDATE, addDelay: Boolean = false) { + val workRequest = createPeriodicWorkRequest(addDelay) workManager.enqueueUniquePeriodicWork( apiPrefs.user?.id.toString(), - ExistingPeriodicWorkPolicy.UPDATE, + policy, workRequest ) } - fun syncOnce(courseIds: List) { + suspend fun syncOnce(courseIds: List) { + val syncSettings = syncSettingsFacade.getSyncSettings() + val constraints = Constraints.Builder() + .setRequiredNetworkType(if (syncSettings.wifiOnly) NetworkType.UNMETERED else NetworkType.CONNECTED) + .setRequiresBatteryNotLow(true) + .build() val inputData = Data.Builder() .putLongArray(COURSE_IDS, courseIds.toLongArray()) .build() val workRequest = OneTimeWorkRequest.Builder(OfflineSyncWorker::class.java) - .addTag(OfflineSyncWorker.TAG) + .addTag(OfflineSyncWorker.ONE_TIME_TAG) .setInputData(inputData) + .setConstraints(constraints) .build() workManager.enqueue(workRequest) } - fun cancelRunningWorkers() { - workManager.cancelAllWorkByTag(CourseSyncWorker.TAG) + suspend fun cancelRunningWorkers() { + if (isPeriodicWorkRunning()) { + scheduleWork(ExistingPeriodicWorkPolicy.CANCEL_AND_REENQUEUE, true) + } + + val runningWorkInfo = getRunningOneTimeWorkInfo() + if (runningWorkInfo != null) { + workManager.cancelWorkById(runningWorkInfo.id) + } } private suspend fun isWorkScheduled(): Boolean { return workManager.getWorkInfosForUniqueWork(apiPrefs.user?.id.toString()).await() - .filter { it.state != WorkInfo.State.CANCELLED } - .isNotEmpty() + .any { it.state != WorkInfo.State.CANCELLED } + } + + private suspend fun isPeriodicWorkRunning(): Boolean { + return workManager.getWorkInfosForUniqueWork(apiPrefs.user?.id.toString()).await() + .any { it.state == WorkInfo.State.RUNNING } } - private suspend fun createWorkRequest(id: UUID? = null): PeriodicWorkRequest { + private suspend fun getRunningOneTimeWorkInfo(): WorkInfo? { + return workManager.getWorkInfosByTag(OfflineSyncWorker.ONE_TIME_TAG).await() + .firstOrNull { it.state == WorkInfo.State.RUNNING } + } + + private suspend fun createPeriodicWorkRequest(setDelay: Boolean = false): PeriodicWorkRequest { val syncSettings = syncSettingsFacade.getSyncSettings() val constraints = Constraints.Builder() .setRequiredNetworkType(if (syncSettings.wifiOnly) NetworkType.UNMETERED else NetworkType.CONNECTED) .setRequiresBatteryNotLow(true) .build() + val frequency: Long = if (syncSettings.syncFrequency == SyncFrequency.DAILY) 1 else 7 + val workRequestBuilder = PeriodicWorkRequest.Builder( OfflineSyncWorker::class.java, - if (syncSettings.syncFrequency == SyncFrequency.DAILY) 1 else 7, TimeUnit.DAYS + frequency, TimeUnit.DAYS ) - .addTag(OfflineSyncWorker.TAG) + .addTag(OfflineSyncWorker.PERIODIC_TAG) .setConstraints(constraints) - id?.let { - workRequestBuilder.setId(it) - } + if (setDelay) workRequestBuilder.setInitialDelay(frequency, TimeUnit.DAYS) return workRequestBuilder.build() } diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/sync/OfflineSyncWorker.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/sync/OfflineSyncWorker.kt index 86365c3986..c659a6f86b 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/sync/OfflineSyncWorker.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/sync/OfflineSyncWorker.kt @@ -23,9 +23,6 @@ import android.content.Context import androidx.core.app.NotificationCompat import androidx.hilt.work.HiltWorker import androidx.work.CoroutineWorker -import androidx.work.WorkInfo -import androidx.work.WorkManager -import androidx.work.WorkQuery import androidx.work.WorkerParameters import com.instructure.canvasapi2.apis.CourseAPI import com.instructure.canvasapi2.builders.RestParams @@ -45,13 +42,10 @@ import com.instructure.pandautils.room.offline.entities.CourseSyncProgressEntity import com.instructure.pandautils.room.offline.entities.DashboardCardEntity import com.instructure.pandautils.room.offline.entities.EditDashboardItemEntity import com.instructure.pandautils.room.offline.entities.EnrollmentState -import com.instructure.pandautils.room.offline.facade.SyncSettingsFacade import com.instructure.pandautils.utils.FeatureFlagProvider import dagger.assisted.Assisted import dagger.assisted.AssistedInject -import kotlinx.coroutines.delay import java.io.File -import java.util.UUID import kotlin.random.Random const val COURSE_IDS = "course-ids" @@ -60,12 +54,10 @@ const val COURSE_IDS = "course-ids" class OfflineSyncWorker @AssistedInject constructor( @Assisted private val context: Context, @Assisted workerParameters: WorkerParameters, - private val workManager: WorkManager, private val featureFlagProvider: FeatureFlagProvider, private val courseApi: CourseAPI.CoursesInterface, private val dashboardCardDao: DashboardCardDao, private val courseSyncSettingsDao: CourseSyncSettingsDao, - private val syncSettingsFacade: SyncSettingsFacade, private val editDashboardItemDao: EditDashboardItemDao, private val courseDao: CourseDao, private val courseSyncProgressDao: CourseSyncProgressDao, @@ -74,13 +66,17 @@ class OfflineSyncWorker @AssistedInject constructor( private val fileFolderDao: FileFolderDao, private val localFileDao: LocalFileDao, private val syncRouter: SyncRouter, + private val courseSync: CourseSync ) : CoroutineWorker(context, workerParameters) { private val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager private val notificationId = Random.nextInt() override suspend fun doWork(): Result { - if (!featureFlagProvider.offlineEnabled() && apiPrefs.user == null) return Result.success() + courseSyncProgressDao.deleteAll() + fileSyncProgressDao.deleteAll() + + if (!featureFlagProvider.offlineEnabled() || apiPrefs.user == null) return Result.success() val dashboardCards = courseApi.getDashboardCourses(RestParams(isForceReadFromNetwork = true)).dataOrNull.orEmpty() @@ -119,52 +115,33 @@ class OfflineSyncWorker @AssistedInject constructor( } editDashboardItemDao.updateEntities(allCourses) - val courseIds = inputData.getLongArray(COURSE_IDS) - val courses = courseIds?.let { - courseSyncSettingsDao.findByIds(courseIds.toList()) - } ?: courseSyncSettingsDao.findAll() + val courseIds = inputData.getLongArray(COURSE_IDS)?.toSet() + val courses = courseSyncSettingsDao.findAll() - val courseIdsToRemove = courseSyncSettingsDao.findAll().filter { !it.anySyncEnabled }.map { it.courseId } + var (coursesToSync, coursesToDelete) = courses.partition { it.anySyncEnabled } + + val courseIdsToRemove = coursesToDelete.map { it.courseId } courseDao.deleteByIds(courseIdsToRemove) courseIdsToRemove.forEach { cleanupFiles(it) } - val settingsMap = courses.associateBy { it.courseId } - - val courseWorkers = courses.filter { it.anySyncEnabled } - .map { CourseSyncWorker.createOnTimeWork(it.courseId, syncSettingsFacade.getSyncSettings().wifiOnly) } + coursesToSync = coursesToSync.filter { courseIds?.contains(it.courseId) ?: true } - val courseProgresses = courseWorkers.map { - val courseId = it.workSpec.input.getLong(CourseSyncWorker.COURSE_ID, 0) - CourseSyncProgressEntity( - workerId = it.id.toString(), - courseId = courseId, - courseName = settingsMap[courseId]?.courseName.orEmpty(), - ) + coursesToSync.map { CourseSyncProgressEntity(it.courseId, it.courseName) }.let { + courseSyncProgressDao.insertAll(it) } - courseSyncProgressDao.deleteAll() - fileSyncProgressDao.deleteAll() - courseSyncProgressDao.insertAll(courseProgresses) - - workManager.beginWith(courseWorkers) - .enqueue() - - val workerIds = courseWorkers.map { it.id } - - while (true) { - delay(1000) - val infos = workManager.getWorkInfos(WorkQuery.fromIds(workerIds)).get() - - if (infos.isEmpty()) break + courseSync.syncCourses(coursesToSync.map { it.courseId }) - if (infos.any { it.state == WorkInfo.State.CANCELLED }) break + val courseProgresses = courseSyncProgressDao.findAll() + val fileProgresses = fileSyncProgressDao.findAll() - if (infos.all { it.state.isFinished }) { - showNotification(infos.size, infos.all { it.state == WorkInfo.State.SUCCEEDED }) - break - } + if (courseProgresses.isNotEmpty() && fileProgresses.isNotEmpty()) { + showNotification( + courseProgresses.size, + courseProgresses.all { it.progressState == ProgressState.COMPLETED } + && fileProgresses.all { it.progressState == ProgressState.COMPLETED }) } return Result.success() @@ -224,6 +201,7 @@ class OfflineSyncWorker @AssistedInject constructor( companion object { const val CHANNEL_ID = "syncChannel" - const val TAG = "OfflineSyncWorker" + const val PERIODIC_TAG = "OfflineSyncWorkerPeriodic" + const val ONE_TIME_TAG = "OfflineSyncWorkerOneTime" } } \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/sync/progress/SyncProgressViewData.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/sync/progress/SyncProgressViewData.kt index d725359345..c67df78fd6 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/sync/progress/SyncProgressViewData.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/sync/progress/SyncProgressViewData.kt @@ -34,7 +34,6 @@ data class SyncProgressViewData(val items: List) data class CourseProgressViewData( val courseName: String, val courseId: Long, - val workerId: String, val files: FilesTabProgressItemViewModel?, val additionalFiles: AdditionalFilesProgressItemViewModel, @Bindable var tabs: List? = null, @@ -62,7 +61,7 @@ data class CourseProgressViewData( data class TabProgressViewData( val tabId: String, val tabName: String, - val workerId: String, + val courseId: Long, @Bindable var state: ProgressState = ProgressState.IN_PROGRESS ) : BaseObservable() { @@ -92,7 +91,7 @@ data class FileSyncProgressViewData( } data class FileTabProgressViewData( - val courseWorkerId: String, + val courseId: Long, var items: List, @Bindable var totalSize: String = "", @Bindable var progress: Int = 0, @@ -112,7 +111,7 @@ data class FileTabProgressViewData( } data class AdditionalFilesProgressViewData( - val courseWorkerId: String, + val courseId: Long, @Bindable var totalSize: String = "", @Bindable var state: ProgressState = ProgressState.IN_PROGRESS ) : BaseObservable() { diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/sync/progress/SyncProgressViewModel.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/sync/progress/SyncProgressViewModel.kt index 9354b88e83..8381671c68 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/sync/progress/SyncProgressViewModel.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/sync/progress/SyncProgressViewModel.kt @@ -89,11 +89,10 @@ class SyncProgressViewModel @Inject constructor( val data = CourseProgressViewData( courseName = courseSyncProgressEntity.courseName, courseId = courseSyncProgressEntity.courseId, - workerId = courseSyncProgressEntity.workerId, size = context.getString(R.string.syncProgress_syncQueued), files = if (courseSyncSettings?.files?.isNotEmpty() == true || courseSyncSettings?.courseSyncSettings?.fullFileSync == true) { FilesTabProgressItemViewModel( - data = FileTabProgressViewData(courseWorkerId = courseSyncProgressEntity.workerId, items = emptyList()), + data = FileTabProgressViewData(courseId = courseSyncProgressEntity.courseId, items = emptyList()), context = context, courseSyncProgressDao = courseSyncProgressDao, fileSyncProgressDao = fileSyncProgressDao @@ -103,7 +102,7 @@ class SyncProgressViewModel @Inject constructor( }, additionalFiles = AdditionalFilesProgressItemViewModel( - data = AdditionalFilesProgressViewData(courseWorkerId = courseSyncProgressEntity.workerId), + data = AdditionalFilesProgressViewData(courseId = courseSyncProgressEntity.courseId), fileSyncProgressDao = fileSyncProgressDao, courseSyncProgressDao = courseSyncProgressDao, context = context @@ -114,15 +113,15 @@ class SyncProgressViewModel @Inject constructor( } fun cancel() { - offlineSyncHelper.cancelRunningWorkers() viewModelScope.launch { + offlineSyncHelper.cancelRunningWorkers() courseSyncProgressDao.deleteAll() fileSyncProgressDao.deleteAll() _events.postValue(Event(SyncProgressAction.Back)) } } - private fun retry() { + private suspend fun retry() { offlineSyncHelper.syncOnce(courseIds) } @@ -132,9 +131,9 @@ class SyncProgressViewModel @Inject constructor( viewModelScope.launch { courseSyncProgressDao.deleteAll() fileSyncProgressDao.deleteAll() + retry() + _events.postValue(Event(SyncProgressAction.Back)) } - retry() - _events.postValue(Event(SyncProgressAction.Back)) } ProgressState.IN_PROGRESS -> _events.postValue(Event(SyncProgressAction.CancelConfirmation)) diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/sync/progress/itemviewmodels/AdditionalFilesProgressItemViewModel.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/sync/progress/itemviewmodels/AdditionalFilesProgressItemViewModel.kt index 722ba63f68..7d279ee9f0 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/sync/progress/itemviewmodels/AdditionalFilesProgressItemViewModel.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/sync/progress/itemviewmodels/AdditionalFilesProgressItemViewModel.kt @@ -61,7 +61,7 @@ data class AdditionalFilesProgressItemViewModel( data.updateTotalSize(NumberHelper.readableFileSize(context, totalSize)) } - private val courseProgressLiveData = courseSyncProgressDao.findByWorkerIdLiveData(data.courseWorkerId) + private val courseProgressLiveData = courseSyncProgressDao.findByCourseIdLiveData(data.courseId) private var fileProgressLiveData: LiveData>? = null private val courseProgressObserver = Observer { progress -> diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/sync/progress/itemviewmodels/CourseProgressItemViewModel.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/sync/progress/itemviewmodels/CourseProgressItemViewModel.kt index 2948cfb67e..bec20f6462 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/sync/progress/itemviewmodels/CourseProgressItemViewModel.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/sync/progress/itemviewmodels/CourseProgressItemViewModel.kt @@ -68,27 +68,27 @@ data class CourseProgressItemViewModel( courseSyncProgressEntity = courseProgress if (data.tabs == null && courseProgress.tabs.isNotEmpty()) { - createTabs(courseProgress.tabs, courseProgress.workerId) + createTabs(courseProgress.tabs, courseProgress.courseId) } updateProgress() } init { - courseProgressLiveData = courseSyncProgressDao.findByWorkerIdLiveData(data.workerId) + courseProgressLiveData = courseSyncProgressDao.findByCourseIdLiveData(data.courseId) courseProgressLiveData?.observeForever(progressObserver) fileProgressLiveData = fileSyncProgressDao.findByCourseIdLiveData(data.courseId) fileProgressLiveData?.observeForever(fileProgressObserver) } - private fun createTabs(tabs: Map, courseWorkerId: String) { + private fun createTabs(tabs: Map, courseId: Long) { val tabViewModels = tabs.map { tabEntry -> TabProgressItemViewModel( TabProgressViewData( tabEntry.key, tabEntry.value.tabName, - courseWorkerId, + courseId, tabEntry.value.state ), courseSyncProgressDao diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/sync/progress/itemviewmodels/FilesTabProgressItemViewModel.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/sync/progress/itemviewmodels/FilesTabProgressItemViewModel.kt index 5309733a31..f0b66a1509 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/sync/progress/itemviewmodels/FilesTabProgressItemViewModel.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/sync/progress/itemviewmodels/FilesTabProgressItemViewModel.kt @@ -46,7 +46,7 @@ data class FilesTabProgressItemViewModel( override val viewType = ViewType.COURSE_FILE_TAB_PROGRESS.viewType private var fileProgressLiveData: LiveData>? = null - private val courseProgressLiveData = courseSyncProgressDao.findByWorkerIdLiveData(data.courseWorkerId) + private val courseProgressLiveData = courseSyncProgressDao.findByCourseIdLiveData(data.courseId) private val fileProgressObserver = Observer> { progresses -> if (progresses.isEmpty()) { diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/sync/progress/itemviewmodels/TabProgressItemViewModel.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/sync/progress/itemviewmodels/TabProgressItemViewModel.kt index 4f68a5ad9c..2a51b90f3b 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/sync/progress/itemviewmodels/TabProgressItemViewModel.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/sync/progress/itemviewmodels/TabProgressItemViewModel.kt @@ -34,7 +34,7 @@ data class TabProgressItemViewModel( override val viewType = ViewType.COURSE_TAB_PROGRESS.viewType - private val progressLiveData = courseSyncProgressDao.findByWorkerIdLiveData(data.workerId) + private val progressLiveData = courseSyncProgressDao.findByCourseIdLiveData(data.courseId) private val progressObserver = Observer { progress -> progress?.tabs?.get(data.tabId)?.let { tabProgress -> diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/CourseSyncProgressDao.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/CourseSyncProgressDao.kt index a3ad581587..9026a2722c 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/CourseSyncProgressDao.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/CourseSyncProgressDao.kt @@ -46,8 +46,8 @@ interface CourseSyncProgressDao { @Query("SELECT * FROM CourseSyncProgressEntity WHERE courseId = :courseId") suspend fun findByCourseId(courseId: Long): CourseSyncProgressEntity? - @Query("SELECT * FROM CourseSyncProgressEntity WHERE workerId = :workerId") - fun findByWorkerIdLiveData(workerId: String): LiveData + @Query("SELECT * FROM CourseSyncProgressEntity WHERE courseId = :courseId") + fun findByCourseIdLiveData(courseId: Long): LiveData @Query("DELETE FROM CourseSyncProgressEntity") suspend fun deleteAll() diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/CourseSyncProgressEntity.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/CourseSyncProgressEntity.kt index c7e4872a84..c30b1522e8 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/CourseSyncProgressEntity.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/CourseSyncProgressEntity.kt @@ -29,7 +29,6 @@ const val TAB_PROGRESS_SIZE = 100 * 1000 data class CourseSyncProgressEntity( @PrimaryKey val courseId: Long, - val workerId: String, val courseName: String, val tabs: Map = emptyMap(), val additionalFilesStarted: Boolean = false, diff --git a/libs/pandautils/src/test/java/com/instructure/pandautils/features/offline/sync/OfflineSyncHelperTest.kt b/libs/pandautils/src/test/java/com/instructure/pandautils/features/offline/sync/OfflineSyncHelperTest.kt index e4069f017a..d84a44897d 100644 --- a/libs/pandautils/src/test/java/com/instructure/pandautils/features/offline/sync/OfflineSyncHelperTest.kt +++ b/libs/pandautils/src/test/java/com/instructure/pandautils/features/offline/sync/OfflineSyncHelperTest.kt @@ -22,6 +22,7 @@ import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleRegistry import androidx.work.Data +import androidx.work.ExistingPeriodicWorkPolicy import androidx.work.NetworkType import androidx.work.OneTimeWorkRequest import androidx.work.PeriodicWorkRequest @@ -51,6 +52,7 @@ import org.junit.Before import org.junit.Rule import org.junit.Test import java.time.Duration +import java.util.UUID @ExperimentalCoroutinesApi class OfflineSyncHelperTest { @@ -78,6 +80,13 @@ class OfflineSyncHelperTest { every { workManager.enqueue(any()) } returns OperationImpl() every { workManager.enqueueUniquePeriodicWork(any(), any(), any()) } returns OperationImpl() + coEvery { syncSettingsFacade.getSyncSettings() } returns SyncSettingsEntity( + 1L, + false, + SyncFrequency.DAILY, + true + ) + offlineSyncHelper = OfflineSyncHelper(workManager, syncSettingsFacade, apiPrefs) } @@ -92,6 +101,9 @@ class OfflineSyncHelperTest { true ) every { workManager.getWorkInfosForUniqueWork(any()) } returns Futures.immediateFuture(mockk(relaxed = true)) + every { workManager.getWorkInfosByTag(OfflineSyncWorker.ONE_TIME_TAG) } returns Futures.immediateFuture( + emptyList() + ) offlineSyncHelper.syncCourses(courseIds) @@ -123,6 +135,9 @@ class OfflineSyncHelperTest { true ) every { workManager.getWorkInfosForUniqueWork(any()) } returns Futures.immediateFuture(emptyList()) + every { workManager.getWorkInfosByTag(OfflineSyncWorker.ONE_TIME_TAG) } returns Futures.immediateFuture( + emptyList() + ) offlineSyncHelper.syncCourses(courseIds) @@ -150,12 +165,14 @@ class OfflineSyncHelperTest { true ) every { workManager.getWorkInfosForUniqueWork(any()) } returns Futures.immediateFuture(emptyList()) + every { workManager.getWorkInfosByTag(OfflineSyncWorker.ONE_TIME_TAG) } returns Futures.immediateFuture( + emptyList() + ) offlineSyncHelper.syncCourses(courseIds) val captor = slot() coVerify(exactly = 1) { - workManager.cancelAllWorkByTag(CourseSyncWorker.TAG) workManager.enqueue(capture(captor)) } coVerify(exactly = 0) { workManager.enqueueUniquePeriodicWork(any(), any(), any()) } @@ -259,33 +276,118 @@ class OfflineSyncHelperTest { } @Test - fun `Cancel running workers`() { + fun `Cancel running one time workers`() = runTest { + val uuid = UUID.randomUUID() + every { workManager.getWorkInfosForUniqueWork(any()) } returns Futures.immediateFuture(emptyList()) + every { workManager.getWorkInfosByTag(OfflineSyncWorker.ONE_TIME_TAG) } returns Futures.immediateFuture( + listOf( + WorkInfo(uuid, WorkInfo.State.RUNNING, Data.EMPTY, emptyList(), Data.EMPTY, 1, 1) + ) + ) + offlineSyncHelper.cancelRunningWorkers() verify { - workManager.cancelAllWorkByTag(CourseSyncWorker.TAG) + workManager.cancelWorkById(uuid) } } @Test - fun `scheduleWorkAfterLogin should schedule work when auto sync is enabled and no work is already scheduled`() = runTest { - every { workManager.getWorkInfosForUniqueWork(any()) } returns Futures.immediateFuture(emptyList()) - coEvery { syncSettingsFacade.getSyncSettings() } returns SyncSettingsEntity(autoSyncEnabled = true, syncFrequency = SyncFrequency.DAILY, wifiOnly = true) + fun `Cancel running periodic workers`() = runTest { + val uuid = UUID.randomUUID() + every { workManager.getWorkInfosForUniqueWork(any()) } returns Futures.immediateFuture( + listOf( + WorkInfo(uuid, WorkInfo.State.RUNNING, Data.EMPTY, emptyList(), Data.EMPTY, 1, 1) + ) + ) + every { workManager.getWorkInfosByTag(OfflineSyncWorker.ONE_TIME_TAG) } returns Futures.immediateFuture( + emptyList() + ) - offlineSyncHelper.scheduleWorkAfterLogin() + offlineSyncHelper.cancelRunningWorkers() + + val captor = slot() + + verify { + workManager.enqueueUniquePeriodicWork(any(), any(), capture(captor)) + } - coVerify { workManager.enqueueUniquePeriodicWork(any(), any(), any()) } + val workRequest = captor.captured + + assertEquals(Duration.ofDays(1).toMillis(), workRequest.workSpec.initialDelay) } + @Test + fun `scheduleWorkAfterLogin should schedule work when auto sync is enabled and no work is already scheduled`() = + runTest { + every { workManager.getWorkInfosForUniqueWork(any()) } returns Futures.immediateFuture(emptyList()) + coEvery { syncSettingsFacade.getSyncSettings() } returns SyncSettingsEntity( + autoSyncEnabled = true, + syncFrequency = SyncFrequency.DAILY, + wifiOnly = true + ) + + offlineSyncHelper.scheduleWorkAfterLogin() + + coVerify { workManager.enqueueUniquePeriodicWork(any(), any(), any()) } + } + @Test fun `scheduleWorkAfterLogin should not schedule work when auto sync is disabled`() = runTest { - coEvery { syncSettingsFacade.getSyncSettings() } returns SyncSettingsEntity(autoSyncEnabled = false, syncFrequency = SyncFrequency.DAILY, wifiOnly = true) + coEvery { syncSettingsFacade.getSyncSettings() } returns SyncSettingsEntity( + autoSyncEnabled = false, + syncFrequency = SyncFrequency.DAILY, + wifiOnly = true + ) offlineSyncHelper.scheduleWorkAfterLogin() coVerify(exactly = 0) { workManager.enqueueUniquePeriodicWork(any(), any(), any()) } } + @Test + fun `Restart periodic worker`() = runTest { + val uuid = UUID.randomUUID() + every { workManager.getWorkInfosForUniqueWork(any()) } returns Futures.immediateFuture( + listOf( + WorkInfo(uuid, WorkInfo.State.RUNNING, Data.EMPTY, emptyList(), Data.EMPTY, 1, 1) + ) + ) + every { workManager.getWorkInfosByTag(OfflineSyncWorker.ONE_TIME_TAG) } returns Futures.immediateFuture( + emptyList() + ) + + offlineSyncHelper.syncCourses(listOf(1L, 2L, 3L)) + + val policyCaptor = slot() + val workRequestCaptor = slot() + verify { + workManager.enqueueUniquePeriodicWork(any(), capture(policyCaptor), capture(workRequestCaptor)) + } + + assertEquals(ExistingPeriodicWorkPolicy.CANCEL_AND_REENQUEUE, policyCaptor.captured) + assertEquals(0, workRequestCaptor.captured.workSpec.initialDelay) + } + + @Test + fun `Restart one time worker`() = runTest { + val uuid = UUID.randomUUID() + every { workManager.getWorkInfosForUniqueWork(any()) } returns Futures.immediateFuture( + emptyList() + ) + every { workManager.getWorkInfosByTag(OfflineSyncWorker.ONE_TIME_TAG) } returns Futures.immediateFuture( + listOf( + WorkInfo(uuid, WorkInfo.State.RUNNING, Data.EMPTY, emptyList(), Data.EMPTY, 1, 1) + ) + ) + + offlineSyncHelper.syncCourses(listOf(1L, 2L, 3L)) + + verify { + workManager.cancelWorkById(uuid) + } + } + @Test fun `scheduleWorkAfterLogin should not schedule work when work is already scheduled`() = runTest { every { workManager.getWorkInfosForUniqueWork(any()) } returns Futures.immediateFuture(mockk(relaxed = true)) diff --git a/libs/pandautils/src/test/java/com/instructure/pandautils/features/offline/sync/progress/AggregateProgressObserverTest.kt b/libs/pandautils/src/test/java/com/instructure/pandautils/features/offline/sync/progress/AggregateProgressObserverTest.kt index ebeab7b378..9e2d976fd2 100644 --- a/libs/pandautils/src/test/java/com/instructure/pandautils/features/offline/sync/progress/AggregateProgressObserverTest.kt +++ b/libs/pandautils/src/test/java/com/instructure/pandautils/features/offline/sync/progress/AggregateProgressObserverTest.kt @@ -30,13 +30,16 @@ import com.instructure.pandautils.room.offline.daos.FileSyncProgressDao import com.instructure.pandautils.room.offline.entities.CourseSyncProgressEntity import com.instructure.pandautils.room.offline.entities.CourseSyncSettingsEntity import com.instructure.pandautils.room.offline.entities.FileSyncProgressEntity -import io.mockk.* +import io.mockk.every +import io.mockk.mockk +import io.mockk.mockkObject +import io.mockk.slot +import io.mockk.unmockkAll import junit.framework.TestCase.assertEquals import org.junit.After import org.junit.Before import org.junit.Rule import org.junit.Test -import java.util.* class AggregateProgressObserverTest { @@ -65,10 +68,8 @@ class AggregateProgressObserverTest { @Test fun `Course update aggregate progress`() { - val course1UUID = UUID.randomUUID().toString() var courseProgress = CourseSyncProgressEntity( 1L, - course1UUID, "Course 1", CourseSyncSettingsEntity.TABS.associateWith { TabSyncData(it, ProgressState.IN_PROGRESS) }, progressState = ProgressState.IN_PROGRESS @@ -100,22 +101,14 @@ class AggregateProgressObserverTest { @Test fun `Aggregate progress updates`() { - val course1Id = UUID.randomUUID() - val course2Id = UUID.randomUUID() - - val file1Id = UUID.randomUUID() - val file2Id = UUID.randomUUID() - var course1 = CourseSyncProgressEntity( 1L, - course1Id.toString(), "Course 1", CourseSyncSettingsEntity.TABS.associateWith { TabSyncData(it, ProgressState.IN_PROGRESS) }, progressState = ProgressState.IN_PROGRESS ) var course2 = CourseSyncProgressEntity( 2L, - course2Id.toString(), "Course 2", CourseSyncSettingsEntity.TABS.associateWith { TabSyncData(it, ProgressState.IN_PROGRESS) }, progressState = ProgressState.IN_PROGRESS @@ -182,15 +175,8 @@ class AggregateProgressObserverTest { @Test fun `Update total size and progress with additional files`() { - val course1Id = UUID.randomUUID() - - val file1Id = UUID.randomUUID() - val file2Id = UUID.randomUUID() - val file3Id = UUID.randomUUID() - var course1Progress = CourseSyncProgressEntity( 1L, - course1Id.toString(), "Course 1", CourseSyncSettingsEntity.TABS.associateWith { TabSyncData(it, ProgressState.IN_PROGRESS) }, additionalFilesStarted = true, @@ -298,11 +284,8 @@ class AggregateProgressObserverTest { @Test fun `Error state`() { - val course1UUID = UUID.randomUUID().toString() - var course1 = CourseSyncProgressEntity( 1L, - course1UUID, "Course 1", CourseSyncSettingsEntity.TABS.associateWith { TabSyncData(it, ProgressState.IN_PROGRESS) }, progressState = ProgressState.IN_PROGRESS diff --git a/libs/pandautils/src/test/java/com/instructure/pandautils/features/offline/sync/progress/SyncProgressViewModelTest.kt b/libs/pandautils/src/test/java/com/instructure/pandautils/features/offline/sync/progress/SyncProgressViewModelTest.kt index e778047699..e1b6aa2028 100644 --- a/libs/pandautils/src/test/java/com/instructure/pandautils/features/offline/sync/progress/SyncProgressViewModelTest.kt +++ b/libs/pandautils/src/test/java/com/instructure/pandautils/features/offline/sync/progress/SyncProgressViewModelTest.kt @@ -106,12 +106,9 @@ class SyncProgressViewModelTest { @Test fun `Init state`() = runTest { - val course1UUID = UUID.randomUUID().toString() - val course2UUID = UUID.randomUUID().toString() - val courseProgresses = listOf( - CourseSyncProgressEntity(1L, course1UUID, "Course 1", emptyMap()), - CourseSyncProgressEntity(2L, course2UUID, "Course 2", emptyMap()) + CourseSyncProgressEntity(1L, "Course 1", emptyMap()), + CourseSyncProgressEntity(2L, "Course 2", emptyMap()) ) coEvery { courseSyncProgressDao.findAll() } returns courseProgresses @@ -134,22 +131,21 @@ class SyncProgressViewModelTest { courseName = "Course 1", courseId = 1L, files = - FilesTabProgressItemViewModel( - data = FileTabProgressViewData( - courseWorkerId = course1UUID, - items = emptyList(), - ), - context = context, - courseSyncProgressDao = courseSyncProgressDao, - fileSyncProgressDao = fileSyncProgressDao + FilesTabProgressItemViewModel( + data = FileTabProgressViewData( + courseId = 1L, + items = emptyList(), ), + context = context, + courseSyncProgressDao = courseSyncProgressDao, + fileSyncProgressDao = fileSyncProgressDao + ), additionalFiles = AdditionalFilesProgressItemViewModel( - data = AdditionalFilesProgressViewData(course1UUID), + data = AdditionalFilesProgressViewData(1L), context = context, courseSyncProgressDao = courseSyncProgressDao, fileSyncProgressDao = fileSyncProgressDao ), - workerId = course1UUID ), context = context, courseSyncProgressDao = courseSyncProgressDao, @@ -160,12 +156,11 @@ class SyncProgressViewModelTest { courseName = "Course 2", files = null, additionalFiles = AdditionalFilesProgressItemViewModel( - data = AdditionalFilesProgressViewData(course2UUID), + data = AdditionalFilesProgressViewData(2L), context = context, courseSyncProgressDao = courseSyncProgressDao, fileSyncProgressDao = fileSyncProgressDao ), - workerId = course2UUID, courseId = 2L ), context = context, @@ -179,8 +174,7 @@ class SyncProgressViewModelTest { @Test fun `Retry`() = runTest { - val course1UUID = UUID.randomUUID().toString() - val courseProgress = CourseSyncProgressEntity(1L, course1UUID, "Course 1", emptyMap(), progressState = ProgressState.ERROR) + val courseProgress = CourseSyncProgressEntity(1L, "Course 1", emptyMap(), progressState = ProgressState.ERROR) coEvery { courseSyncProgressDao.findAll() } returns listOf(courseProgress) @@ -213,8 +207,8 @@ class SyncProgressViewModelTest { @Test fun `Cancel`() = runTest { - val course1UUID = UUID.randomUUID().toString() - val syncProgress = CourseSyncProgressEntity(1L, course1UUID, "Course 1", emptyMap(), progressState = ProgressState.IN_PROGRESS) + val syncProgress = + CourseSyncProgressEntity(1L, "Course 1", emptyMap(), progressState = ProgressState.IN_PROGRESS) coEvery { courseSyncProgressDao.findAll() } returns listOf(syncProgress) diff --git a/libs/pandautils/src/test/java/com/instructure/pandautils/features/offline/sync/progress/itemviewmodels/AdditionalFilesProgressItemViewModelTest.kt b/libs/pandautils/src/test/java/com/instructure/pandautils/features/offline/sync/progress/itemviewmodels/AdditionalFilesProgressItemViewModelTest.kt index 2e8b112619..fbb2e470f8 100644 --- a/libs/pandautils/src/test/java/com/instructure/pandautils/features/offline/sync/progress/itemviewmodels/AdditionalFilesProgressItemViewModelTest.kt +++ b/libs/pandautils/src/test/java/com/instructure/pandautils/features/offline/sync/progress/itemviewmodels/AdditionalFilesProgressItemViewModelTest.kt @@ -59,10 +59,8 @@ class AdditionalFilesProgressItemViewModelTest { @Test fun `Success if no files`() { - val courseUUID = UUID.randomUUID() val courseProgress = CourseSyncProgressEntity( 1L, - courseUUID.toString(), "Course", emptyMap(), progressState = ProgressState.COMPLETED, @@ -70,17 +68,16 @@ class AdditionalFilesProgressItemViewModelTest { ) val courseLiveData = MutableLiveData(courseProgress) - every { courseSyncProgressDao.findByWorkerIdLiveData(courseUUID.toString()) } returns courseLiveData + every { courseSyncProgressDao.findByCourseIdLiveData(1L) } returns courseLiveData every { fileSyncProgressDao.findAdditionalFilesByCourseIdLiveData(1L) } returns MutableLiveData(emptyList()) - itemViewModel = createItemViewModel(courseUUID) + itemViewModel = createItemViewModel(1L) assertEquals(ProgressState.COMPLETED, itemViewModel.data.state) } @Test fun `Add internal files sizes to total size when progress is starting`() { - val courseUUID = UUID.randomUUID() val additionalFileSyncData = listOf( FileSyncProgressEntity( courseId = 1L, @@ -110,13 +107,13 @@ class AdditionalFilesProgressItemViewModelTest { val fileLiveData = MutableLiveData(additionalFileSyncData) val courseProgress = - CourseSyncProgressEntity(1L, courseUUID.toString(), "Course", emptyMap(), additionalFilesStarted = true) + CourseSyncProgressEntity(1L, "Course", emptyMap(), additionalFilesStarted = true) val courseLiveData = MutableLiveData(courseProgress) - every { courseSyncProgressDao.findByWorkerIdLiveData(courseUUID.toString()) } returns courseLiveData + every { courseSyncProgressDao.findByCourseIdLiveData(1L) } returns courseLiveData every { fileSyncProgressDao.findAdditionalFilesByCourseIdLiveData(1L) } returns fileLiveData - itemViewModel = createItemViewModel(courseUUID) + itemViewModel = createItemViewModel(1L) assertEquals(ProgressState.IN_PROGRESS, itemViewModel.data.state) assertEquals("3000 bytes", itemViewModel.data.totalSize) @@ -124,10 +121,6 @@ class AdditionalFilesProgressItemViewModelTest { @Test fun `Update total file size for external files and progress`() { - val courseUUID = UUID.randomUUID() - val file1UUID = UUID.randomUUID() - val file2UUID = UUID.randomUUID() - val file3UUID = UUID.randomUUID() val additionalFileSyncData = listOf( FileSyncProgressEntity( courseId = 1L, @@ -163,16 +156,15 @@ class AdditionalFilesProgressItemViewModelTest { val courseProgress = CourseSyncProgressEntity( 1L, - courseUUID.toString(), "Course", emptyMap(), additionalFilesStarted = true, progressState = ProgressState.IN_PROGRESS ) val courseLiveData = MutableLiveData(courseProgress) - every { courseSyncProgressDao.findByWorkerIdLiveData(courseUUID.toString()) } returns courseLiveData + every { courseSyncProgressDao.findByCourseIdLiveData(1L) } returns courseLiveData - itemViewModel = createItemViewModel(courseUUID) + itemViewModel = createItemViewModel(1L) assertEquals(ProgressState.IN_PROGRESS, itemViewModel.data.state) assertEquals("3000 bytes", itemViewModel.data.totalSize) @@ -192,7 +184,6 @@ class AdditionalFilesProgressItemViewModelTest { @Test fun `Error state`() { - val courseUUID = UUID.randomUUID() val additionalFileSyncData = listOf( FileSyncProgressEntity( courseId = 1L, @@ -227,23 +218,22 @@ class AdditionalFilesProgressItemViewModelTest { val courseProgress = CourseSyncProgressEntity( 1L, - courseUUID.toString(), "Course", emptyMap(), additionalFilesStarted = true, progressState = ProgressState.IN_PROGRESS ) val courseLiveData = MutableLiveData(courseProgress) - every { courseSyncProgressDao.findByWorkerIdLiveData(courseUUID.toString()) } returns courseLiveData + every { courseSyncProgressDao.findByCourseIdLiveData(1L) } returns courseLiveData - itemViewModel = createItemViewModel(courseUUID) + itemViewModel = createItemViewModel(1L) assertEquals(ProgressState.ERROR, itemViewModel.data.state) } - private fun createItemViewModel(uuid: UUID): AdditionalFilesProgressItemViewModel { + private fun createItemViewModel(courseId: Long): AdditionalFilesProgressItemViewModel { return AdditionalFilesProgressItemViewModel( - data = AdditionalFilesProgressViewData(courseWorkerId = uuid.toString()), + data = AdditionalFilesProgressViewData(courseId = courseId), context = context, fileSyncProgressDao = fileSyncProgressDao, courseSyncProgressDao = courseSyncProgressDao diff --git a/libs/pandautils/src/test/java/com/instructure/pandautils/features/offline/sync/progress/itemviewmodels/CourseProgressItemViewModelTest.kt b/libs/pandautils/src/test/java/com/instructure/pandautils/features/offline/sync/progress/itemviewmodels/CourseProgressItemViewModelTest.kt index aab9e9e88f..bb41aa0124 100644 --- a/libs/pandautils/src/test/java/com/instructure/pandautils/features/offline/sync/progress/itemviewmodels/CourseProgressItemViewModelTest.kt +++ b/libs/pandautils/src/test/java/com/instructure/pandautils/features/offline/sync/progress/itemviewmodels/CourseProgressItemViewModelTest.kt @@ -31,14 +31,17 @@ import com.instructure.pandautils.room.offline.daos.FileSyncProgressDao import com.instructure.pandautils.room.offline.entities.CourseSyncProgressEntity import com.instructure.pandautils.room.offline.entities.CourseSyncSettingsEntity import com.instructure.pandautils.room.offline.entities.FileSyncProgressEntity -import io.mockk.* +import io.mockk.every +import io.mockk.mockk +import io.mockk.mockkObject +import io.mockk.slot +import io.mockk.unmockkAll import junit.framework.TestCase.assertEquals import junit.framework.TestCase.assertFalse import org.junit.After import org.junit.Before import org.junit.Rule import org.junit.Test -import java.util.* class CourseProgressItemViewModelTest { @@ -65,20 +68,18 @@ class CourseProgressItemViewModelTest { @Test fun `Create tab items`() { - val uuid = UUID.randomUUID() val courseProgress = CourseSyncProgressEntity( 1L, - uuid.toString(), "Course", CourseSyncSettingsEntity.TABS.associateWith { TabSyncData(it, ProgressState.IN_PROGRESS) }, progressState = ProgressState.IN_PROGRESS ) val courseLiveData = MutableLiveData(courseProgress) - every { courseSyncProgressDao.findByWorkerIdLiveData(uuid.toString()) } returns courseLiveData + every { courseSyncProgressDao.findByCourseIdLiveData(1L) } returns courseLiveData every { fileSyncProgressDao.findByCourseIdLiveData(1L) } returns MutableLiveData(emptyList()) - courseProgressItemViewModel = createItemViewModel(uuid) + courseProgressItemViewModel = createItemViewModel(1L) assertEquals("1000000 bytes", courseProgressItemViewModel.data.size) @@ -90,10 +91,8 @@ class CourseProgressItemViewModelTest { @Test fun `Failed course sync`() { - val uuid = UUID.randomUUID() val courseProgress = CourseSyncProgressEntity( 1L, - uuid.toString(), "Course", CourseSyncSettingsEntity.TABS.associateWith { TabSyncData(it, ProgressState.ERROR) }, additionalFilesStarted = true, @@ -101,20 +100,18 @@ class CourseProgressItemViewModelTest { ) val courseLiveData = MutableLiveData(courseProgress) - every { courseSyncProgressDao.findByWorkerIdLiveData(uuid.toString()) } returns courseLiveData + every { courseSyncProgressDao.findByCourseIdLiveData(1L) } returns courseLiveData every { fileSyncProgressDao.findByCourseIdLiveData(1L) } returns MutableLiveData(emptyList()) - courseProgressItemViewModel = createItemViewModel(uuid) + courseProgressItemViewModel = createItemViewModel(1L) assertEquals(ProgressState.ERROR, courseProgressItemViewModel.data.state) } @Test fun `Failed file sync`() { - val uuid = UUID.randomUUID() val courseProgress = CourseSyncProgressEntity( 1L, - uuid.toString(), "Course", CourseSyncSettingsEntity.TABS.associateWith { TabSyncData(it, ProgressState.COMPLETED) }, progressState = ProgressState.COMPLETED @@ -149,7 +146,7 @@ class CourseProgressItemViewModelTest { val courseLiveData = MutableLiveData(courseProgress) val fileLiveData = MutableLiveData(fileProgresses) - every { courseSyncProgressDao.findByWorkerIdLiveData(uuid.toString()) } returns courseLiveData + every { courseSyncProgressDao.findByCourseIdLiveData(1L) } returns courseLiveData every { fileSyncProgressDao.findByCourseIdLiveData(1L) } returns fileLiveData fileProgresses = listOf( @@ -180,7 +177,7 @@ class CourseProgressItemViewModelTest { ) fileLiveData.postValue(fileProgresses) - courseProgressItemViewModel = createItemViewModel(uuid) + courseProgressItemViewModel = createItemViewModel(1L) assertEquals(ProgressState.IN_PROGRESS, courseProgressItemViewModel.data.state) fileProgresses = listOf( @@ -217,10 +214,8 @@ class CourseProgressItemViewModelTest { @Test fun `Sync success`() { - val uuid = UUID.randomUUID() val courseProgress = CourseSyncProgressEntity( 1L, - uuid.toString(), "Course", CourseSyncSettingsEntity.TABS.associateWith { TabSyncData(it, ProgressState.COMPLETED) }, progressState = ProgressState.COMPLETED @@ -255,10 +250,10 @@ class CourseProgressItemViewModelTest { val courseLiveData = MutableLiveData(courseProgress) val fileLiveData = MutableLiveData(fileProgresses) - every { courseSyncProgressDao.findByWorkerIdLiveData(uuid.toString()) } returns courseLiveData + every { courseSyncProgressDao.findByCourseIdLiveData(1L) } returns courseLiveData every { fileSyncProgressDao.findByCourseIdLiveData(1L) } returns fileLiveData - courseProgressItemViewModel = createItemViewModel(uuid) + courseProgressItemViewModel = createItemViewModel(1L) assertEquals(ProgressState.COMPLETED, courseProgressItemViewModel.data.state) assertFalse(courseProgressItemViewModel.data.failed) @@ -266,10 +261,8 @@ class CourseProgressItemViewModelTest { @Test fun `Update total size with additional files`() { - val uuid = UUID.randomUUID() val courseProgress = CourseSyncProgressEntity( 1L, - uuid.toString(), "Course", CourseSyncSettingsEntity.TABS.associateWith { TabSyncData(it, ProgressState.COMPLETED) }, additionalFilesStarted = true, @@ -277,7 +270,7 @@ class CourseProgressItemViewModelTest { ) val courseLiveData = MutableLiveData(courseProgress) - every { courseSyncProgressDao.findByWorkerIdLiveData(uuid.toString()) } returns courseLiveData + every { courseSyncProgressDao.findByCourseIdLiveData(1L) } returns courseLiveData val files = listOf( FileSyncProgressEntity( @@ -319,7 +312,7 @@ class CourseProgressItemViewModelTest { val filesLiveData = MutableLiveData(files) every { fileSyncProgressDao.findByCourseIdLiveData(1L) } returns filesLiveData - courseProgressItemViewModel = createItemViewModel(uuid) + courseProgressItemViewModel = createItemViewModel(1L) assertEquals(ProgressState.IN_PROGRESS, courseProgressItemViewModel.data.state) // We don't know the size of the external file yet, so we won't add this to the total size @@ -341,15 +334,14 @@ class CourseProgressItemViewModelTest { assertFalse(courseProgressItemViewModel.data.failed) } - private fun createItemViewModel(uuid: UUID): CourseProgressItemViewModel { + private fun createItemViewModel(courseId: Long): CourseProgressItemViewModel { return CourseProgressItemViewModel( data = CourseProgressViewData( courseName = "Course", - courseId = 1L, - workerId = uuid.toString(), + courseId = courseId, files = null, additionalFiles = AdditionalFilesProgressItemViewModel( - data = AdditionalFilesProgressViewData(uuid.toString()), + data = AdditionalFilesProgressViewData(courseId), context = context, fileSyncProgressDao = fileSyncProgressDao, courseSyncProgressDao = courseSyncProgressDao diff --git a/libs/pandautils/src/test/java/com/instructure/pandautils/features/offline/sync/progress/itemviewmodels/FilesTabProgressItemViewModelTest.kt b/libs/pandautils/src/test/java/com/instructure/pandautils/features/offline/sync/progress/itemviewmodels/FilesTabProgressItemViewModelTest.kt index 4da3b3777e..eda530d005 100644 --- a/libs/pandautils/src/test/java/com/instructure/pandautils/features/offline/sync/progress/itemviewmodels/FilesTabProgressItemViewModelTest.kt +++ b/libs/pandautils/src/test/java/com/instructure/pandautils/features/offline/sync/progress/itemviewmodels/FilesTabProgressItemViewModelTest.kt @@ -28,13 +28,16 @@ import com.instructure.pandautils.room.offline.daos.CourseSyncProgressDao import com.instructure.pandautils.room.offline.daos.FileSyncProgressDao import com.instructure.pandautils.room.offline.entities.CourseSyncProgressEntity import com.instructure.pandautils.room.offline.entities.FileSyncProgressEntity -import io.mockk.* +import io.mockk.every +import io.mockk.mockk +import io.mockk.mockkObject +import io.mockk.slot +import io.mockk.unmockkAll import junit.framework.TestCase.assertEquals import org.junit.After import org.junit.Before import org.junit.Rule import org.junit.Test -import java.util.* class FilesTabProgressItemViewModelTest { @@ -61,11 +64,9 @@ class FilesTabProgressItemViewModelTest { @Test fun `Success if no files`() { - val courseUUID = UUID.randomUUID() val courseProgress = CourseSyncProgressEntity( 1L, - courseUUID.toString(), "Course", emptyMap(), additionalFilesStarted = true, @@ -73,17 +74,16 @@ class FilesTabProgressItemViewModelTest { ) val courseLiveData = MutableLiveData(courseProgress) - every { courseSyncProgressDao.findByWorkerIdLiveData(courseUUID.toString()) } returns courseLiveData + every { courseSyncProgressDao.findByCourseIdLiveData(1L) } returns courseLiveData every { fileSyncProgressDao.findCourseFilesByCourseIdLiveData(1L) } returns MutableLiveData(emptyList()) - filesTabProgressItemViewModel = createItemViewModel(courseUUID) + filesTabProgressItemViewModel = createItemViewModel(1L) assertEquals(ProgressState.COMPLETED, filesTabProgressItemViewModel.data.state) } @Test fun `Create file items`() { - val courseUUID = UUID.randomUUID() val fileSyncProgresses = listOf( FileSyncProgressEntity( courseId = 1L, @@ -113,7 +113,6 @@ class FilesTabProgressItemViewModelTest { val courseProgress = CourseSyncProgressEntity( 1L, - courseUUID.toString(), "Course", emptyMap(), additionalFilesStarted = true, @@ -122,10 +121,10 @@ class FilesTabProgressItemViewModelTest { val courseLiveData = MutableLiveData(courseProgress) val fileLiveData = MutableLiveData(fileSyncProgresses) - every { courseSyncProgressDao.findByWorkerIdLiveData(courseUUID.toString()) } returns courseLiveData + every { courseSyncProgressDao.findByCourseIdLiveData(1L) } returns courseLiveData every { fileSyncProgressDao.findCourseFilesByCourseIdLiveData(1L) } returns fileLiveData - filesTabProgressItemViewModel = createItemViewModel(courseUUID) + filesTabProgressItemViewModel = createItemViewModel(1L) assertEquals(ProgressState.IN_PROGRESS, filesTabProgressItemViewModel.data.state) assertEquals(3, filesTabProgressItemViewModel.data.items.size) @@ -139,7 +138,6 @@ class FilesTabProgressItemViewModelTest { @Test fun `Progress updates`() { - val courseUUID = UUID.randomUUID() var fileSyncData = listOf( FileSyncProgressEntity( courseId = 1L, @@ -168,7 +166,6 @@ class FilesTabProgressItemViewModelTest { ) val courseProgress = CourseSyncProgressEntity( 1L, - courseUUID.toString(), "Course", emptyMap(), progressState = ProgressState.COMPLETED @@ -176,10 +173,10 @@ class FilesTabProgressItemViewModelTest { val courseLiveData = MutableLiveData(courseProgress) val fileLiveData = MutableLiveData(fileSyncData) - every { courseSyncProgressDao.findByWorkerIdLiveData(courseUUID.toString()) } returns courseLiveData + every { courseSyncProgressDao.findByCourseIdLiveData(1L) } returns courseLiveData every { fileSyncProgressDao.findCourseFilesByCourseIdLiveData(1L) } returns fileLiveData - filesTabProgressItemViewModel = createItemViewModel(courseUUID) + filesTabProgressItemViewModel = createItemViewModel(1L) assertEquals(ProgressState.IN_PROGRESS, filesTabProgressItemViewModel.data.state) assertEquals(50, filesTabProgressItemViewModel.data.progress) @@ -248,10 +245,10 @@ class FilesTabProgressItemViewModelTest { } - private fun createItemViewModel(uuid: UUID): FilesTabProgressItemViewModel { + private fun createItemViewModel(courseId: Long): FilesTabProgressItemViewModel { return FilesTabProgressItemViewModel( FileTabProgressViewData( - courseWorkerId = uuid.toString(), + courseId, emptyList(), ), context, diff --git a/libs/pandautils/src/test/java/com/instructure/pandautils/features/offline/sync/progress/itemviewmodels/TabProgressItemViewModelTest.kt b/libs/pandautils/src/test/java/com/instructure/pandautils/features/offline/sync/progress/itemviewmodels/TabProgressItemViewModelTest.kt index 5923b506ba..6241bba1ee 100644 --- a/libs/pandautils/src/test/java/com/instructure/pandautils/features/offline/sync/progress/itemviewmodels/TabProgressItemViewModelTest.kt +++ b/libs/pandautils/src/test/java/com/instructure/pandautils/features/offline/sync/progress/itemviewmodels/TabProgressItemViewModelTest.kt @@ -45,18 +45,16 @@ class TabProgressItemViewModelTest { @Test fun `Progress updates`() { - val uuid = UUID.randomUUID() var courseProgress = CourseSyncProgressEntity( 1L, - uuid.toString(), "Course", CourseSyncSettingsEntity.TABS.associateWith { TabSyncData(it, ProgressState.IN_PROGRESS) }, ) val courseLiveData = MutableLiveData(courseProgress) - every { courseSyncProgressDao.findByWorkerIdLiveData(uuid.toString()) } returns courseLiveData + every { courseSyncProgressDao.findByCourseIdLiveData(1L) } returns courseLiveData - tabProgressItemViewModel = createItemViewModel(uuid) + tabProgressItemViewModel = createItemViewModel(1L) assertEquals(ProgressState.IN_PROGRESS, tabProgressItemViewModel.data.state) @@ -69,12 +67,12 @@ class TabProgressItemViewModelTest { assertEquals(ProgressState.COMPLETED, tabProgressItemViewModel.data.state) } - private fun createItemViewModel(uuid: UUID): TabProgressItemViewModel { + private fun createItemViewModel(courseId: Long): TabProgressItemViewModel { return TabProgressItemViewModel( TabProgressViewData( Tab.ASSIGNMENTS_ID, "Assignments", - uuid.toString(), + courseId, ProgressState.IN_PROGRESS ), courseSyncProgressDao From 007b86a30a44c61c7fef8ac30b2649afc8a9cee5 Mon Sep 17 00:00:00 2001 From: Akos Hermann <72087159+hermannakos@users.noreply.github.com> Date: Wed, 15 Nov 2023 16:11:42 +0100 Subject: [PATCH 008/132] [MBL-17184][Student] Apply theme to settings fragment onConfigChange (#2251) refs: MBL-17184 affects: Student release note: Fixed a bug where the theme of the settings page would not update. test plan: See ticket. --- .../student/activity/SettingsActivity.kt | 13 +++++++++++++ .../student/fragment/ApplicationSettingsFragment.kt | 1 - 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/apps/student/src/main/java/com/instructure/student/activity/SettingsActivity.kt b/apps/student/src/main/java/com/instructure/student/activity/SettingsActivity.kt index 78a7509f68..b209e64757 100644 --- a/apps/student/src/main/java/com/instructure/student/activity/SettingsActivity.kt +++ b/apps/student/src/main/java/com/instructure/student/activity/SettingsActivity.kt @@ -18,9 +18,11 @@ package com.instructure.student.activity import android.content.Context import android.content.Intent +import android.content.res.Configuration import android.os.Bundle import androidx.appcompat.app.AppCompatActivity import androidx.fragment.app.Fragment +import com.instructure.interactions.FragmentInteractions import com.instructure.pandautils.analytics.SCREEN_VIEW_SETTINGS import com.instructure.pandautils.analytics.ScreenView import com.instructure.pandautils.binding.viewBinding @@ -51,6 +53,17 @@ class SettingsActivity : AppCompatActivity(){ private val currentFragment: Fragment? get() = supportFragmentManager.fragments.last() + override fun onConfigurationChanged(newConfig: Configuration) { + super.onConfigurationChanged(newConfig) + applyThemeForAllFragments() + } + + private fun applyThemeForAllFragments() { + supportFragmentManager.fragments.forEach { + (it as? FragmentInteractions)?.applyTheme() + } + } + fun addFragment(fragment: Fragment) { val ft = supportFragmentManager.beginTransaction() currentFragment?.let { ft.hide(it) } diff --git a/apps/student/src/main/java/com/instructure/student/fragment/ApplicationSettingsFragment.kt b/apps/student/src/main/java/com/instructure/student/fragment/ApplicationSettingsFragment.kt index 594680ba04..91ac257182 100644 --- a/apps/student/src/main/java/com/instructure/student/fragment/ApplicationSettingsFragment.kt +++ b/apps/student/src/main/java/com/instructure/student/fragment/ApplicationSettingsFragment.kt @@ -28,7 +28,6 @@ import androidx.fragment.app.Fragment import androidx.lifecycle.lifecycleScope import com.instructure.canvasapi2.utils.* import com.instructure.canvasapi2.utils.pageview.PageView -import com.instructure.loginapi.login.dialog.NoInternetConnectionDialog import com.instructure.pandautils.analytics.SCREEN_VIEW_APPLICATION_SETTINGS import com.instructure.pandautils.analytics.ScreenView import com.instructure.pandautils.binding.viewBinding From af4bc5b93c7a8ee895f0764252b6378232de98ac Mon Sep 17 00:00:00 2001 From: Akos Hermann <72087159+hermannakos@users.noreply.github.com> Date: Wed, 15 Nov 2023 16:12:06 +0100 Subject: [PATCH 009/132] [MBL-17189][Student] Launch LTI tools with id (#2250) refs: MBL-17189 affects: Teacher release note: Fixed a bug where some LTI tools would not open. test plan: See ticket. --- .../teacher/fragments/LtiLaunchFragment.kt | 30 ++++++++++++++----- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/apps/teacher/src/main/java/com/instructure/teacher/fragments/LtiLaunchFragment.kt b/apps/teacher/src/main/java/com/instructure/teacher/fragments/LtiLaunchFragment.kt index c8224a11c0..5ca2eba4e2 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/fragments/LtiLaunchFragment.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/fragments/LtiLaunchFragment.kt @@ -26,6 +26,8 @@ import androidx.browser.customtabs.CustomTabsIntent import androidx.fragment.app.FragmentActivity import com.instructure.canvasapi2.managers.SubmissionManager import com.instructure.canvasapi2.models.CanvasContext +import com.instructure.canvasapi2.models.Course +import com.instructure.canvasapi2.models.Group import com.instructure.canvasapi2.models.Tab import com.instructure.canvasapi2.utils.ApiPrefs import com.instructure.canvasapi2.utils.pageview.PageView @@ -108,16 +110,28 @@ class LtiLaunchFragment : BaseFragment() { var url = ltiUrl // Replace deep link scheme .replaceFirst("canvas-courses://", "${ApiPrefs.protocol}://") .replaceFirst("canvas-student://", "${ApiPrefs.protocol}://") - if (sessionLessLaunch) { - if (url.contains("api/v1/")) { - getSessionlessLtiUrl(url) - } else { - // This is specific for Studio and Gauge - url = "${ApiPrefs.fullDomain}/api/v1/accounts/self/external_tools/sessionless_launch?url=$url" + + when { + sessionLessLaunch -> { + val id = url.substringAfterLast("/external_tools/").substringBefore("?") + url = when { + (id.toIntOrNull() != null) -> when (canvasContext) { + is Course -> "${ApiPrefs.fullDomain}/api/v1/courses/${(canvasContext as Course).id}/external_tools/sessionless_launch?id=$id" + is Group -> "${ApiPrefs.fullDomain}/api/v1/groups/${(canvasContext as Group).id}/external_tools/sessionless_launch?id=$id" + else -> "${ApiPrefs.fullDomain}/api/v1/accounts/self/external_tools/sessionless_launch?id=$id" + } + + else -> { + when (canvasContext) { + is Course -> "${ApiPrefs.fullDomain}/api/v1/courses/${(canvasContext as Course).id}/external_tools/sessionless_launch?url=$url" + is Group -> "${ApiPrefs.fullDomain}/api/v1/groups/${(canvasContext as Group).id}/external_tools/sessionless_launch?url=$url" + else -> "${ApiPrefs.fullDomain}/api/v1/accounts/self/external_tools/sessionless_launch?url=$url" + } + } + } getSessionlessLtiUrl(url) } - } else { - launchCustomTab(url) + else -> launchCustomTab(url) } } else -> displayError() From 0068d0169aba3ae980b31c5c1d84e595bd5689d9 Mon Sep 17 00:00:00 2001 From: Tamas Kozmer <72397075+tamaskozmer@users.noreply.github.com> Date: Thu, 16 Nov 2023 09:29:12 +0100 Subject: [PATCH 010/132] [MBL-17182][Student] Offline content fixes (#2249) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit refs: MBL-17182 affects: Student release note: none * Fixed page sync issue. * Fixed HTML parsing issue. * Removed network check from edit dashboard button on the emtpy view. * Fixed dashboard cards. * Fixed not available content issue. * Fixed UI glitch with offline settings. * lock info fixes * Fixed new errors in additional files. * Fixed dashboard card updates. * Removed unused import. * Added offline check for the dashboard update. * Fixed tests. * Fixed Dao test. * Do not sync files without url. * Fixed external file size calculation and external file errors. --------- Co-authored-by: Ákos Hermann --- .../student/activity/NavigationActivity.kt | 2 +- .../student/activity/SettingsActivity.kt | 12 ++- .../adapter/DashboardRecyclerAdapter.kt | 2 + .../features/dashboard/DashboardRepository.kt | 7 +- .../features/modules/util/ModuleUtility.kt | 12 +-- .../fragment/ApplicationSettingsFragment.kt | 12 +-- .../student/fragment/DashboardFragment.kt | 42 +++++++-- .../dashboard/DashboardRepositoryTest.kt | 9 +- .../student/test/util/ModuleUtilityTest.kt | 86 ------------------- .../1.json | 28 +++--- .../room/offline/daos/LockInfoDaoTest.kt | 11 +++ .../room/offline/daos/LockedModuleDaoTest.kt | 13 +-- .../room/offline/daos/PageDaoTest.kt | 6 +- .../features/offline/sync/CourseSync.kt | 16 ++-- .../features/offline/sync/FileSync.kt | 31 +++++-- .../features/offline/sync/HtmlParser.kt | 5 +- .../offline/sync/OfflineSyncWorker.kt | 5 +- .../room/offline/daos/LockInfoDao.kt | 3 + .../pandautils/room/offline/daos/PageDao.kt | 4 +- .../room/offline/entities/LockInfoEntity.kt | 3 +- .../offline/entities/LockedModuleEntity.kt | 12 +-- .../room/offline/facade/LockInfoFacade.kt | 5 +- .../features/offline/sync/HtmlParserTest.kt | 5 +- .../room/offline/facade/LockInfoFacadeTest.kt | 24 ++++-- .../pandarecycler/PaginatedRecyclerAdapter.kt | 7 ++ 25 files changed, 186 insertions(+), 176 deletions(-) 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 9187905881..7c99312709 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 @@ -227,7 +227,7 @@ class NavigationActivity : BaseRouterActivity(), Navigation, MasqueradingDialog. R.id.navigationDrawerItem_stopMasquerading -> { MasqueradeHelper.stopMasquerading(startActivityClass) } - R.id.navigationDrawerSettings -> startActivity(Intent(applicationContext, SettingsActivity::class.java)) + R.id.navigationDrawerSettings -> startActivity(SettingsActivity.createIntent(applicationContext, featureFlagProvider.offlineEnabled())) } } } diff --git a/apps/student/src/main/java/com/instructure/student/activity/SettingsActivity.kt b/apps/student/src/main/java/com/instructure/student/activity/SettingsActivity.kt index 78a7509f68..746dac953a 100644 --- a/apps/student/src/main/java/com/instructure/student/activity/SettingsActivity.kt +++ b/apps/student/src/main/java/com/instructure/student/activity/SettingsActivity.kt @@ -31,6 +31,8 @@ import com.instructure.student.databinding.ActivitySettingsBinding import dagger.hilt.android.AndroidEntryPoint import javax.inject.Inject +private const val OFFLINE_ENABLED = "offlineEnabled" + @ScreenView(SCREEN_VIEW_SETTINGS) @AndroidEntryPoint class SettingsActivity : AppCompatActivity(){ @@ -40,10 +42,12 @@ class SettingsActivity : AppCompatActivity(){ private val binding by viewBinding(ActivitySettingsBinding::inflate) + var offlineEnabled: Boolean = false + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + offlineEnabled = intent.getBooleanExtra(OFFLINE_ENABLED, false) setContentView(binding.root) - networkStateProvider.isOnlineLiveData.observe(this) { isOnline -> binding.offlineIndicator.root.setVisible(!isOnline) } @@ -60,8 +64,10 @@ class SettingsActivity : AppCompatActivity(){ } companion object { - fun createIntent(context: Context): Intent { - return Intent(context, SettingsActivity::class.java) + fun createIntent(context: Context, offlineEnabled: Boolean): Intent { + return Intent(context, SettingsActivity::class.java).apply { + putExtra(OFFLINE_ENABLED, offlineEnabled) + } } } } diff --git a/apps/student/src/main/java/com/instructure/student/adapter/DashboardRecyclerAdapter.kt b/apps/student/src/main/java/com/instructure/student/adapter/DashboardRecyclerAdapter.kt index 725f9456d3..7b26e23d3f 100644 --- a/apps/student/src/main/java/com/instructure/student/adapter/DashboardRecyclerAdapter.kt +++ b/apps/student/src/main/java/com/instructure/student/adapter/DashboardRecyclerAdapter.kt @@ -135,6 +135,8 @@ class DashboardRecyclerAdapter( val dashboardCards = repository.getDashboardCourses(isRefresh) val syncedCourseIds = repository.getSyncedCourseIds() + resetData() + mCourseMap = courses.associateBy { it.id } // Map not null is needed because the dashboard api can return unpublished courses diff --git a/apps/student/src/main/java/com/instructure/student/features/dashboard/DashboardRepository.kt b/apps/student/src/main/java/com/instructure/student/features/dashboard/DashboardRepository.kt index 4d85ff6b22..760bd0f69c 100644 --- a/apps/student/src/main/java/com/instructure/student/features/dashboard/DashboardRepository.kt +++ b/apps/student/src/main/java/com/instructure/student/features/dashboard/DashboardRepository.kt @@ -45,11 +45,14 @@ class DashboardRepository( } suspend fun getDashboardCourses(forceNetwork: Boolean): List { - val dashboardCards = dataSource().getDashboardCards(forceNetwork).sortedBy { it.position } + var dashboardCards = dataSource().getDashboardCards(forceNetwork) + if (dashboardCards.all { it.position == Int.MAX_VALUE }) { + dashboardCards = dashboardCards.mapIndexed { index, dashboardCard -> dashboardCard.copy(position = index) } + } if (isOnline() && isOfflineEnabled()) { localDataSource.saveDashboardCards(dashboardCards) } - return dashboardCards + return dashboardCards.sortedBy { it.position } } suspend fun getSyncedCourseIds(): Set { diff --git a/apps/student/src/main/java/com/instructure/student/features/modules/util/ModuleUtility.kt b/apps/student/src/main/java/com/instructure/student/features/modules/util/ModuleUtility.kt index 4a3bfe25ac..fdea8d6c99 100644 --- a/apps/student/src/main/java/com/instructure/student/features/modules/util/ModuleUtility.kt +++ b/apps/student/src/main/java/com/instructure/student/features/modules/util/ModuleUtility.kt @@ -56,11 +56,7 @@ object ModuleUtility { syncedFileIds: List, context: Context ): Fragment? = when (item.type) { - "Page" -> { - createFragmentWithOfflineCheck(isOnline, course, item, syncedTabs, context, setOf(Tab.PAGES_ID)) { - PageDetailsFragment.newInstance(PageDetailsFragment.makeRoute(course, item.title, item.pageUrl, navigatedFromModules)) - } - } + "Page" -> PageDetailsFragment.newInstance(PageDetailsFragment.makeRoute(course, item.title, item.pageUrl, navigatedFromModules)) "Assignment" -> { createFragmentWithOfflineCheck(isOnline, course, item, syncedTabs, context, setOf(Tab.ASSIGNMENTS_ID, Tab.GRADES_ID, Tab.SYLLABUS_ID)) { AssignmentDetailsFragment.newInstance(makeRoute(course, getAssignmentId(item))) @@ -78,10 +74,8 @@ object ModuleUtility { "Locked" -> LockedModuleItemFragment.newInstance(LockedModuleItemFragment.makeRoute(course, item.title!!, item.moduleDetails?.lockExplanation ?: "")) "SubHeader" -> null // Don't do anything with headers, they're just dividers so we don't show them here. "Quiz" -> { - createFragmentWithOfflineCheck(isOnline, course, item, syncedTabs, context, setOf(Tab.QUIZZES_ID)) { - val apiURL = removeDomain(item.url) - ModuleQuizDecider.newInstance(ModuleQuizDecider.makeRoute(course, item.htmlUrl!!, apiURL!!, item.contentId)) - } + val apiURL = removeDomain(item.url) + ModuleQuizDecider.newInstance(ModuleQuizDecider.makeRoute(course, item.htmlUrl!!, apiURL!!, item.contentId)) } "ChooseAssignmentGroup" -> { createFragmentWithOfflineCheck(isOnline, course, item, syncedTabs, context) { diff --git a/apps/student/src/main/java/com/instructure/student/fragment/ApplicationSettingsFragment.kt b/apps/student/src/main/java/com/instructure/student/fragment/ApplicationSettingsFragment.kt index 594680ba04..3415885946 100644 --- a/apps/student/src/main/java/com/instructure/student/fragment/ApplicationSettingsFragment.kt +++ b/apps/student/src/main/java/com/instructure/student/fragment/ApplicationSettingsFragment.kt @@ -28,7 +28,6 @@ import androidx.fragment.app.Fragment import androidx.lifecycle.lifecycleScope import com.instructure.canvasapi2.utils.* import com.instructure.canvasapi2.utils.pageview.PageView -import com.instructure.loginapi.login.dialog.NoInternetConnectionDialog import com.instructure.pandautils.analytics.SCREEN_VIEW_APPLICATION_SETTINGS import com.instructure.pandautils.analytics.ScreenView import com.instructure.pandautils.binding.viewBinding @@ -182,11 +181,8 @@ class ApplicationSettingsFragment : ParentFragment() { private fun setUpSyncSettings() { lifecycleScope.launch { - if (!featureFlagProvider.offlineEnabled()) { - binding.offlineContentDivider.setGone() - binding.offlineContentTitle.setGone() - binding.offlineSyncSettingsContainer.setGone() - } else { + val offlineEnabled = (activity as? SettingsActivity)?.offlineEnabled ?: false + if (offlineEnabled) { syncSettingsFacade.getSyncSettingsListenable().observe(viewLifecycleOwner) { syncSettings -> if (syncSettings == null) { binding.offlineSyncSettingsContainer.setGone() @@ -202,6 +198,10 @@ class ApplicationSettingsFragment : ParentFragment() { binding.offlineSyncSettingsContainer.onClick { addFragment(SyncSettingsFragment.newInstance()) } + } else { + binding.offlineContentDivider.setGone() + binding.offlineContentTitle.setGone() + binding.offlineSyncSettingsContainer.setGone() } } } diff --git a/apps/student/src/main/java/com/instructure/student/fragment/DashboardFragment.kt b/apps/student/src/main/java/com/instructure/student/fragment/DashboardFragment.kt index 2bfd905eaf..9e55d7c9d3 100644 --- a/apps/student/src/main/java/com/instructure/student/fragment/DashboardFragment.kt +++ b/apps/student/src/main/java/com/instructure/student/fragment/DashboardFragment.kt @@ -32,10 +32,12 @@ import androidx.localbroadcastmanager.content.LocalBroadcastManager import androidx.recyclerview.widget.DefaultItemAnimator import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.RecyclerView +import androidx.work.WorkInfo.State +import androidx.work.WorkManager +import androidx.work.WorkQuery import com.instructure.canvasapi2.managers.CourseNicknameManager import com.instructure.canvasapi2.managers.UserManager import com.instructure.canvasapi2.models.* -import com.instructure.canvasapi2.utils.APIHelper import com.instructure.canvasapi2.utils.pageview.PageView import com.instructure.canvasapi2.utils.weave.awaitApi import com.instructure.canvasapi2.utils.weave.catch @@ -47,6 +49,8 @@ import com.instructure.pandautils.binding.viewBinding import com.instructure.pandautils.features.dashboard.edit.EditDashboardFragment import com.instructure.pandautils.features.dashboard.notifications.DashboardNotificationsFragment import com.instructure.pandautils.features.offline.offlinecontent.OfflineContentFragment +import com.instructure.pandautils.features.offline.sync.AggregateProgressObserver +import com.instructure.pandautils.features.offline.sync.OfflineSyncWorker import com.instructure.pandautils.utils.* import com.instructure.student.R import com.instructure.student.adapter.DashboardRecyclerAdapter @@ -86,6 +90,12 @@ class DashboardFragment : ParentFragment() { @Inject lateinit var networkStateProvider: NetworkStateProvider + @Inject + lateinit var aggregateProgressObserver: AggregateProgressObserver + + @Inject + lateinit var workManager: WorkManager + private val binding by viewBinding(FragmentCourseGridBinding::bind) private lateinit var recyclerBinding: CourseGridRecyclerRefreshLayoutBinding @@ -96,6 +106,8 @@ class DashboardFragment : ParentFragment() { private var courseColumns: Int = LIST_SPAN_COUNT private var groupColumns: Int = LIST_SPAN_COUNT + private val runningWorkers = mutableSetOf() + private val somethingChangedReceiver = object : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent?) { if (recyclerAdapter != null && intent?.extras?.getBoolean(Const.COURSE_FAVORITES) == true) { @@ -120,6 +132,28 @@ class DashboardFragment : ParentFragment() { recyclerAdapter?.refresh() if (online) recyclerBinding.swipeRefreshLayout.isRefreshing = true } + + lifecycleScope.launch { + if (featureFlagProvider.offlineEnabled()) { + subscribeToOfflineSyncUpdates() + } + } + } + + private fun subscribeToOfflineSyncUpdates() { + val workQuery = WorkQuery.Builder.fromTags(listOf(OfflineSyncWorker.PERIODIC_TAG, OfflineSyncWorker.ONE_TIME_TAG)).build() + workManager.getWorkInfosLiveData(workQuery).observe(this) { workInfos -> + workInfos.forEach { workInfo -> + if (workInfo.state == State.RUNNING) { + runningWorkers.add(workInfo.id.toString()) + } + } + + if (workInfos?.any { (it.state == State.SUCCEEDED || it.state == State.FAILED) && runningWorkers.contains(it.id.toString()) } == true) { + recyclerAdapter?.silentRefresh() + runningWorkers.clear() + } + } } @@ -307,11 +341,7 @@ class DashboardFragment : ParentFragment() { recyclerBinding.listView.clipToPadding = false emptyCoursesView.onClickAddCourses { - if (!APIHelper.hasNetworkConnection()) { - toast(R.string.notAvailableOffline) - } else { - RouteMatcher.route(requireActivity(), EditDashboardFragment.makeRoute()) - } + RouteMatcher.route(requireActivity(), EditDashboardFragment.makeRoute()) } } diff --git a/apps/student/src/test/java/com/instructure/student/features/dashboard/DashboardRepositoryTest.kt b/apps/student/src/test/java/com/instructure/student/features/dashboard/DashboardRepositoryTest.kt index dd8b0b4f5f..fcf03f3d66 100644 --- a/apps/student/src/test/java/com/instructure/student/features/dashboard/DashboardRepositoryTest.kt +++ b/apps/student/src/test/java/com/instructure/student/features/dashboard/DashboardRepositoryTest.kt @@ -24,7 +24,6 @@ import com.instructure.pandautils.room.offline.daos.CourseDao import com.instructure.pandautils.room.offline.daos.CourseSyncSettingsDao import com.instructure.pandautils.room.offline.entities.CourseEntity import com.instructure.pandautils.room.offline.entities.CourseSyncSettingsEntity -import com.instructure.pandautils.utils.FEATURE_FLAG_OFFLINE import com.instructure.pandautils.utils.FeatureFlagProvider import com.instructure.pandautils.utils.NetworkStateProvider import io.mockk.coEvery @@ -120,8 +119,9 @@ class DashboardRepositoryTest { coEvery { localDataSource.getDashboardCards(any()) } returns offlineCards val result = repository.getDashboardCourses(true) + val expected = listOf(DashboardCard(3, position = 0), DashboardCard(4, position = 1)) - Assert.assertEquals(offlineCards, result) + Assert.assertEquals(expected, result) } @Test @@ -134,13 +134,14 @@ class DashboardRepositoryTest { coEvery { localDataSource.getDashboardCards(any()) } returns offlineCards val result = repository.getDashboardCourses(true) + val expected = listOf(DashboardCard(1, position = 0), DashboardCard(2, position = 1)) - Assert.assertEquals(onlineCards, result) + Assert.assertEquals(expected, result) } @Test fun `Returned dashboard courses are saved to the local store`() = runTest { - val onlineCards = listOf(DashboardCard(1), DashboardCard(2)) + val onlineCards = listOf(DashboardCard(1, position = 0), DashboardCard(2, position = 1)) every { networkStateProvider.isOnline() } returns true coEvery { networkDataSource.getDashboardCards(any()) } returns onlineCards diff --git a/apps/student/src/test/java/com/instructure/student/test/util/ModuleUtilityTest.kt b/apps/student/src/test/java/com/instructure/student/test/util/ModuleUtilityTest.kt index cf5fd6ed3c..25e4cae7c8 100644 --- a/apps/student/src/test/java/com/instructure/student/test/util/ModuleUtilityTest.kt +++ b/apps/student/src/test/java/com/instructure/student/test/util/ModuleUtilityTest.kt @@ -128,45 +128,6 @@ class ModuleUtilityTest : TestCase() { TestCase.assertEquals(expectedBundle.toString(), parentFragment.arguments!!.toString()) } - @Test - fun testGetFragment_page_offlineSynced() { - val url = "https://mobile.canvas.net/api/v1/courses/222/pages/hello-world" - val moduleItem = ModuleItem( - id = 4567, - type = "Page", - url = url, - title = "hello-world" - ) - - val course = Course() - val expectedBundle = Bundle() - expectedBundle.putParcelable(Const.CANVAS_CONTEXT, course) - expectedBundle.putString(PageDetailsFragment.PAGE_NAME, "hello-world") - expectedBundle.putBoolean(PageDetailsFragment.NAVIGATED_FROM_MODULES, false) - - val parentFragment = callGetFragment(moduleItem, course, null, isOnline = false, tabs = setOf(Tab.PAGES_ID)) - TestCase.assertNotNull(parentFragment) - TestCase.assertEquals(PageDetailsFragment::class.java, parentFragment!!.javaClass) - TestCase.assertEquals(expectedBundle.toString(), parentFragment.arguments!!.toString()) - } - - @Test - fun testGetFragment_page_offlineNotSynced() { - val url = "https://mobile.canvas.net/api/v1/courses/222/pages/hello-world" - val moduleItem = ModuleItem( - id = 4567, - type = "Page", - url = url, - title = "hello-world" - ) - - val course = Course() - - val fragment = callGetFragment(moduleItem, course, null, isOnline = false) - TestCase.assertNotNull(fragment) - TestCase.assertEquals(NotAvailableOfflineFragment::class.java, fragment!!.javaClass) - } - @Test fun testGetFragment_assignment() { val url = "https://mobile.canvas.net/api/v1/courses/222/assignments/123456789" @@ -352,53 +313,6 @@ class ModuleUtilityTest : TestCase() { TestCase.assertEquals(expectedBundle.toString(), parentFragment.arguments!!.toString()) } - @Test - fun testGetFragment_quiz_offlineSynced() { - val url = "https://mobile.canvas.net/api/v1/courses/222/quizzes/123456789" - val htmlUrl = "https://mobile.canvas.net/courses/222/quizzes/123456789" - val apiUrl = "courses/222/quizzes/123456789" - - val moduleItem = ModuleItem( - id = 4567, - type = "Quiz", - url = url, - htmlUrl = htmlUrl, - contentId = 55 - ) - - val course = Course() - val expectedBundle = Bundle() - expectedBundle.putParcelable(Const.CANVAS_CONTEXT, course) - expectedBundle.putString(Const.URL, htmlUrl) - expectedBundle.putString(Const.API_URL, apiUrl) - expectedBundle.putLong(Const.ID, 55) - - val parentFragment = callGetFragment(moduleItem, course, null, isOnline = false, tabs = setOf(Tab.QUIZZES_ID)) - TestCase.assertNotNull(parentFragment) - TestCase.assertEquals(ModuleQuizDecider::class.java, parentFragment!!.javaClass) - TestCase.assertEquals(expectedBundle.toString(), parentFragment.arguments!!.toString()) - } - - @Test - fun testGetFragment_quiz_offlineNotSynced() { - val url = "https://mobile.canvas.net/api/v1/courses/222/quizzes/123456789" - val htmlUrl = "https://mobile.canvas.net/courses/222/quizzes/123456789" - val apiUrl = "courses/222/quizzes/123456789" - - val moduleItem = ModuleItem( - id = 4567, - type = "Quiz", - url = url, - htmlUrl = htmlUrl, - contentId = 55 - ) - - val course = Course() - val fragment = callGetFragment(moduleItem, course, null, isOnline = false) - TestCase.assertNotNull(fragment) - TestCase.assertEquals(NotAvailableOfflineFragment::class.java, fragment!!.javaClass) - } - @Test fun testGetFragment_discussion() { val url = "https://mobile.canvas.net/api/v1/courses/222/discussion_topics/123456789" diff --git a/libs/pandautils/schemas/com.instructure.pandautils.room.offline.OfflineDatabase/1.json b/libs/pandautils/schemas/com.instructure.pandautils.room.offline.OfflineDatabase/1.json index 7c8f1b4539..4a8f8781a2 100644 --- a/libs/pandautils/schemas/com.instructure.pandautils.room.offline.OfflineDatabase/1.json +++ b/libs/pandautils/schemas/com.instructure.pandautils.room.offline.OfflineDatabase/1.json @@ -2,7 +2,7 @@ "formatVersion": 1, "database": { "version": 1, - "identityHash": "a5fc40aae4d227313a98d9a89f41cfdd", + "identityHash": "a1cb353c4afbf14831de8752ace07ce9", "entities": [ { "tableName": "AssignmentDueDateEntity", @@ -4554,17 +4554,7 @@ "id" ] }, - "indices": [ - { - "name": "index_LockInfoEntity_lockedModuleId", - "unique": true, - "columnNames": [ - "lockedModuleId" - ], - "orders": [], - "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_LockInfoEntity_lockedModuleId` ON `${TABLE_NAME}` (`lockedModuleId`)" - } - ], + "indices": [], "foreignKeys": [ { "table": "ModuleContentDetailsEntity", @@ -4603,7 +4593,7 @@ }, { "tableName": "LockedModuleEntity", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `contextId` INTEGER NOT NULL, `contextType` TEXT, `name` TEXT, `unlockAt` TEXT, `isRequireSequentialProgress` INTEGER NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`id`) REFERENCES `LockInfoEntity`(`lockedModuleId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `contextId` INTEGER NOT NULL, `contextType` TEXT, `name` TEXT, `unlockAt` TEXT, `isRequireSequentialProgress` INTEGER NOT NULL, `lockInfoId` INTEGER, PRIMARY KEY(`id`), FOREIGN KEY(`lockInfoId`) REFERENCES `LockInfoEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", "fields": [ { "fieldPath": "id", @@ -4640,6 +4630,12 @@ "columnName": "isRequireSequentialProgress", "affinity": "INTEGER", "notNull": true + }, + { + "fieldPath": "lockInfoId", + "columnName": "lockInfoId", + "affinity": "INTEGER", + "notNull": false } ], "primaryKey": { @@ -4655,10 +4651,10 @@ "onDelete": "CASCADE", "onUpdate": "NO ACTION", "columns": [ - "id" + "lockInfoId" ], "referencedColumns": [ - "lockedModuleId" + "id" ] } ] @@ -5507,7 +5503,7 @@ "views": [], "setupQueries": [ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'a5fc40aae4d227313a98d9a89f41cfdd')" + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'a1cb353c4afbf14831de8752ace07ce9')" ] } } \ No newline at end of file diff --git a/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/LockInfoDaoTest.kt b/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/LockInfoDaoTest.kt index e9107c983f..265760824a 100644 --- a/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/LockInfoDaoTest.kt +++ b/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/LockInfoDaoTest.kt @@ -165,4 +165,15 @@ class LockInfoDaoTest { Assert.assertNull(result) } + + @Test + fun testFindByRowId() = runTest { + val expected = LockInfoEntity(LockInfo(unlockAt = "1"), assignmentId = 1) + + val rowId = lockInfoDao.insert(expected) + + val result = lockInfoDao.findByRowId(rowId) + + Assert.assertEquals(expected.assignmentId, result?.assignmentId) + } } \ No newline at end of file diff --git a/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/LockedModuleDaoTest.kt b/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/LockedModuleDaoTest.kt index aac575c8c5..53cec27be1 100644 --- a/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/LockedModuleDaoTest.kt +++ b/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/LockedModuleDaoTest.kt @@ -69,11 +69,11 @@ class LockedModuleDaoTest { @Test fun testFindById() = runTest { - val expected = LockedModuleEntity(LockedModule(id = 1)) + val expected = LockedModuleEntity(LockedModule(id = 1), 1L) lockedModuleDao.insert(expected) - lockedModuleDao.insert(LockedModuleEntity(LockedModule(id = 2))) - lockedModuleDao.insert(LockedModuleEntity(LockedModule(id = 3))) + lockedModuleDao.insert(LockedModuleEntity(LockedModule(id = 2), 2L)) + lockedModuleDao.insert(LockedModuleEntity(LockedModule(id = 3), 3L)) val result = lockedModuleDao.findById(1) @@ -82,14 +82,15 @@ class LockedModuleDaoTest { @Test(expected = SQLiteConstraintException::class) fun testLockInfoForeignKey() = runTest { - lockedModuleDao.insert(LockedModuleEntity(LockedModule(id = 4))) + lockedModuleDao.insert(LockedModuleEntity(LockedModule(id = 4), 4L)) } @Test fun testLockInfoCascade() = runTest { - val id = lockInfoDao.insert(LockInfoEntity(LockInfo(contextModule = LockedModule(1)))) + val rowId = lockInfoDao.insert(LockInfoEntity(LockInfo(contextModule = LockedModule(1)))) + val id = lockInfoDao.findByRowId(rowId)?.id ?: 0 - lockedModuleDao.insert(LockedModuleEntity(LockedModule(id = 1))) + lockedModuleDao.insert(LockedModuleEntity(LockedModule(id = 1), id)) lockInfoDao.delete(LockInfoEntity(id, null, null, null, null, null, null)) diff --git a/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/PageDaoTest.kt b/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/PageDaoTest.kt index 2749734a27..b29d25edcc 100644 --- a/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/PageDaoTest.kt +++ b/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/PageDaoTest.kt @@ -88,14 +88,18 @@ class PageDaoTest { @Test fun testFindByUrl() = runTest { val courseEntity = CourseEntity(Course(id = 1L)) + val courseEntity2 = CourseEntity(Course(id = 2L)) courseDao.insert(courseEntity) + courseDao.insert(courseEntity2) val pageEntity = PageEntity(Page(id = 1, title = "Page1", url = "page-1-url"), courseId = 1L) val pageEntity2 = PageEntity(Page(id = 2, title = "Page2", url = "page-2-url"), courseId = 1L) + val pageEntity3 = PageEntity(Page(id = 3, title = "Page3", url = "page-2-url"), courseId = 2L) pageDao.insert(pageEntity) pageDao.insert(pageEntity2) + pageDao.insert(pageEntity3) - val result = pageDao.findByUrl("page-2-url") + val result = pageDao.findByUrlAndCourse("page-2-url", 1L) assertEquals(pageEntity2, result) } diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/sync/CourseSync.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/sync/CourseSync.kt index 2cfe45356d..aa2b4fea03 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/sync/CourseSync.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/sync/CourseSync.kt @@ -113,6 +113,7 @@ class CourseSync( private val additionalFileIdsToSync = mutableMapOf>() private val externalFilesToSync = mutableMapOf>() + private val failedTabsPerCourse = mutableMapOf>() private var isStopped = false set(value) = synchronized(this) { @@ -128,14 +129,14 @@ class CourseSync( } private suspend fun syncCourse(courseId: Long) { + additionalFileIdsToSync[courseId] = emptySet() + externalFilesToSync[courseId] = emptySet() + val courseSettingsWithFiles = courseSyncSettingsDao.findWithFilesById(courseId) ?: return val courseSettings = courseSettingsWithFiles.courseSyncSettings val course = fetchCourseDetails(courseId) - additionalFileIdsToSync[courseId] = emptySet() - externalFilesToSync[courseId] = emptySet() - initProgress(courseSettings, course) if (courseSettings.fullFileSync || courseSettingsWithFiles.files.isNotEmpty()) { @@ -519,6 +520,7 @@ class CourseSync( e.printStackTrace() updateTabError(courseId, *tabIds) firebaseCrashlytics.recordException(e) + failedTabsPerCourse[courseId] = failedTabsPerCourse[courseId].orEmpty() + tabIds } } @@ -527,7 +529,9 @@ class CourseSync( it: ModuleItem, params: RestParams ) { - if (it.pageUrl != null && pageDao.findByUrl(it.pageUrl!!) == null) { + // If the pages failed we might already have the page, but we need to get it again to make sure it's up to date + // and download the files inside because those are always deleted + if (it.pageUrl != null && (pageDao.findByUrlAndCourse(it.pageUrl!!, courseId) == null || failedTabsPerCourse[courseId]?.contains(Tab.PAGES_ID) == true)) { val page = pageApi.getDetailedPage(courseId, it.pageUrl!!, params).dataOrNull page?.body = parseHtmlContent(page?.body, courseId) page?.let { pageFacade.insertPage(it, courseId) } @@ -556,7 +560,9 @@ class CourseSync( it: ModuleItem, params: RestParams ) { - if (quizDao.findById(it.contentId) == null) { + // If the pages failed we might already have the page, but we need to get it again to make sure it's up to date + // and download the files inside because those are always deleted + if (quizDao.findById(it.contentId) == null || failedTabsPerCourse[courseId]?.contains(Tab.QUIZZES_ID) == true) { val quiz = quizApi.getQuiz(courseId, it.contentId, params).dataOrNull quiz?.description = parseHtmlContent(quiz?.description, courseId) quiz?.let { quizDao.insert(QuizEntity(it, courseId)) } diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/sync/FileSync.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/sync/FileSync.kt index f67aa666f0..a851435233 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/sync/FileSync.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/sync/FileSync.kt @@ -20,7 +20,6 @@ package com.instructure.pandautils.features.offline.sync import android.content.Context import android.net.Uri -import android.util.Log import com.google.firebase.crashlytics.FirebaseCrashlytics import com.instructure.canvasapi2.apis.DownloadState import com.instructure.canvasapi2.apis.FileDownloadAPI @@ -29,6 +28,7 @@ import com.instructure.canvasapi2.apis.saveFile import com.instructure.canvasapi2.builders.RestParams import com.instructure.canvasapi2.models.FileFolder import com.instructure.canvasapi2.utils.ApiPrefs +import com.instructure.canvasapi2.utils.DataResult import com.instructure.pandautils.room.offline.daos.CourseSyncProgressDao import com.instructure.pandautils.room.offline.daos.FileFolderDao import com.instructure.pandautils.room.offline.daos.FileSyncProgressDao @@ -103,7 +103,7 @@ class FileSync( fileFolderDao.insertAll(nonPublicFiles.map { FileFolderEntity(it) }) - val additionalFiles = additionalPublicFilesToSync + nonPublicFiles + val additionalFiles = (additionalPublicFilesToSync + nonPublicFiles).filter { !it.url.isNullOrEmpty() } fileSyncProgressDao.insertAll(additionalFiles.map { createProgress(it, courseId, true) }) val syncData = mutableListOf() @@ -149,17 +149,26 @@ class FileSync( var downloadedFile = getDownloadFile(fileName, fileSyncData.externalFile, fileSyncData.courseId) try { - fileDownloadApi.downloadFile( + val downloadResult = fileDownloadApi.downloadFile( fileSyncData.fileUrl, RestParams(shouldIgnoreToken = fileSyncData.externalFile) ) + + // External images can fail for various reasons (for example the file is no longer available), + // so we just mark them as completed in this case. The user wouldn't see those in online mode anyway. + if (fileSyncData.externalFile && downloadResult is DataResult.Fail) { + updateProgress(fileSyncData.fileId, 100, ProgressState.COMPLETED, fileSyncData.externalFile) + return + } + + downloadResult .dataOrThrow .saveFile(downloadedFile) .collect { if (isStopped) throw IllegalStateException("Worker was stopped") when (it) { is DownloadState.InProgress -> { - updateProgress(fileSyncData.fileId, it.progress, ProgressState.IN_PROGRESS) + updateProgress(fileSyncData.fileId, it.progress, ProgressState.IN_PROGRESS, fileSyncData.externalFile, it.totalBytes) } is DownloadState.Success -> { @@ -181,7 +190,7 @@ class FileSync( ) ) } - updateProgress(fileSyncData.fileId, 100, ProgressState.COMPLETED) + updateProgress(fileSyncData.fileId, 100, ProgressState.COMPLETED, fileSyncData.externalFile, it.totalBytes) } is DownloadState.Failure -> { @@ -191,7 +200,7 @@ class FileSync( } } catch (e: Exception) { downloadedFile.delete() - updateProgress(fileSyncData.fileId, 0, ProgressState.ERROR) + updateProgress(fileSyncData.fileId, 0, ProgressState.ERROR, fileSyncData.externalFile) firebaseCrashlytics.recordException(e) } } @@ -275,9 +284,13 @@ class FileSync( return fileSyncProgressDao.findByRowId(rowId)?.fileId ?: -1L } - private suspend fun updateProgress(fileId: Long, progress: Int, progressState: ProgressState) { - fileSyncProgressDao.findByFileId(fileId)?.copy(progress = progress, progressState = progressState) - ?.let { fileSyncProgressDao.update(it) } + private suspend fun updateProgress(fileId: Long, progress: Int, progressState: ProgressState, externalFile: Boolean, size: Long? = null) { + var newProgress = fileSyncProgressDao.findByFileId(fileId)?.copy(progress = progress, progressState = progressState) + if (externalFile && size != null) { + newProgress = newProgress?.copy(fileSize = size) + } + + newProgress?.let { fileSyncProgressDao.update(it) } } } diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/sync/HtmlParser.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/sync/HtmlParser.kt index f8d288eee7..1079bc7c6f 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/sync/HtmlParser.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/sync/HtmlParser.kt @@ -63,8 +63,9 @@ class HtmlParser( resultHtml = newHtml if (shouldSyncFile) internalFileIds.add(fileId) } else { - val fileName = Uri.parse(imageUrl).lastPathSegment - if (fileName != null) { + val fileUri = Uri.parse(imageUrl) + val fileName = fileUri.lastPathSegment + if (fileName != null && fileUri.scheme == "https") { // We don't allow cleartext traffic in the app. resultHtml = resultHtml.replace(imageUrl, "file://${createLocalFilePathForExternalFile(fileName, courseId)}") externalFileUrls.add(imageUrl) } diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/sync/OfflineSyncWorker.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/sync/OfflineSyncWorker.kt index c659a6f86b..df828ff23b 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/sync/OfflineSyncWorker.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/sync/OfflineSyncWorker.kt @@ -78,8 +78,11 @@ class OfflineSyncWorker @AssistedInject constructor( if (!featureFlagProvider.offlineEnabled() || apiPrefs.user == null) return Result.success() - val dashboardCards = + var dashboardCards = courseApi.getDashboardCourses(RestParams(isForceReadFromNetwork = true)).dataOrNull.orEmpty() + if (dashboardCards.all { it.position == Int.MAX_VALUE }) { + dashboardCards = dashboardCards.mapIndexed { index, dashboardCard -> dashboardCard.copy(position = index) } + } dashboardCardDao.updateEntities(dashboardCards.map { DashboardCardEntity(it) }) val params = RestParams(isForceReadFromNetwork = true, usePerPageQueryParam = true) diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/LockInfoDao.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/LockInfoDao.kt index 7da1353da9..9cd96dc238 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/LockInfoDao.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/LockInfoDao.kt @@ -40,4 +40,7 @@ interface LockInfoDao { @Query("SELECT * FROM LockInfoEntity WHERE pageId = :pageId") suspend fun findByPageId(pageId: Long): LockInfoEntity? + + @Query("SELECT * FROM LockInfoEntity WHERE ROWID = :rowId") + suspend fun findByRowId(rowId: Long): LockInfoEntity? } \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/PageDao.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/PageDao.kt index 53ac401873..aa5a94173e 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/PageDao.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/PageDao.kt @@ -44,8 +44,8 @@ interface PageDao { @Query("SELECT * FROM PageEntity WHERE id=:id") suspend fun findById(id: Long): PageEntity? - @Query("SELECT * FROM PageEntity WHERE url=:url") - suspend fun findByUrl(url: String): PageEntity? + @Query("SELECT * FROM PageEntity WHERE url=:url AND courseId=:courseId") + suspend fun findByUrlAndCourse(url: String, courseId: Long): PageEntity? @Query("SELECT * FROM PageEntity WHERE frontPage=1 AND courseId=:courseId") suspend fun getFrontPage(courseId: Long): PageEntity? diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/LockInfoEntity.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/LockInfoEntity.kt index b6a6ffc1ea..e1261b66f8 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/LockInfoEntity.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/LockInfoEntity.kt @@ -47,8 +47,7 @@ import com.instructure.canvasapi2.models.LockedModule onDelete = ForeignKey.CASCADE, deferred = true ) - ], - indices = [Index("lockedModuleId", unique = true)] + ] ) data class LockInfoEntity( @PrimaryKey(autoGenerate = true) diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/LockedModuleEntity.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/LockedModuleEntity.kt index 4cb4ac8d0a..cc78f39ff6 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/LockedModuleEntity.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/LockedModuleEntity.kt @@ -28,8 +28,8 @@ import com.instructure.canvasapi2.models.ModuleName foreignKeys = [ ForeignKey( entity = LockInfoEntity::class, - parentColumns = ["lockedModuleId"], - childColumns = ["id"], + parentColumns = ["id"], + childColumns = ["lockInfoId"], onDelete = ForeignKey.CASCADE ) ] @@ -41,15 +41,17 @@ data class LockedModuleEntity( val contextType: String?, val name: String?, val unlockAt: String?, - val isRequireSequentialProgress: Boolean + val isRequireSequentialProgress: Boolean, + val lockInfoId: Long? ) { - constructor(lockedModule: LockedModule) : this( + constructor(lockedModule: LockedModule, lockInfoId: Long?) : this( id = lockedModule.id, contextId = lockedModule.contextId, contextType = lockedModule.contextType, name = lockedModule.name, unlockAt = lockedModule.unlockAt, - isRequireSequentialProgress = lockedModule.isRequireSequentialProgress + isRequireSequentialProgress = lockedModule.isRequireSequentialProgress, + lockInfoId = lockInfoId ) fun toApiModel( diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/facade/LockInfoFacade.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/facade/LockInfoFacade.kt index 9078cd9ff7..e81998b332 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/facade/LockInfoFacade.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/facade/LockInfoFacade.kt @@ -47,9 +47,10 @@ class LockInfoFacade( } private suspend fun insertLockInfo(lockInfo: LockInfo, assignmentId: Long? = null, moduleId: Long? = null, pageId: Long? = null) { - lockInfoDao.insert(LockInfoEntity(lockInfo, assignmentId, moduleId, pageId)) + val rowId = lockInfoDao.insert(LockInfoEntity(lockInfo, assignmentId, moduleId, pageId)) + val lockInfoEntity = lockInfoDao.findByRowId(rowId) lockInfo.contextModule?.let { lockedModule -> - lockedModuleDao.insert(LockedModuleEntity(lockedModule)) + lockedModuleDao.insert(LockedModuleEntity(lockedModule, lockInfoEntity?.id)) moduleNameDao.insertAll(lockedModule.prerequisites?.map { ModuleNameEntity(it, lockedModule.id) }.orEmpty()) lockedModule.completionRequirements.forEach { val oldEntity = completionRequirementDao.findById(it.id) diff --git a/libs/pandautils/src/test/java/com/instructure/pandautils/features/offline/sync/HtmlParserTest.kt b/libs/pandautils/src/test/java/com/instructure/pandautils/features/offline/sync/HtmlParserTest.kt index 1cb94dfd1a..d6fac411be 100644 --- a/libs/pandautils/src/test/java/com/instructure/pandautils/features/offline/sync/HtmlParserTest.kt +++ b/libs/pandautils/src/test/java/com/instructure/pandautils/features/offline/sync/HtmlParserTest.kt @@ -67,7 +67,10 @@ class HtmlParserTest { mockkStatic(Uri::class) every { Uri.parse(any()) } answers { val url = it.invocation.args.first() as String - mockk() { every { lastPathSegment } returns url.split("/").last() } + mockk() { + every { lastPathSegment } returns url.split("/").last() + every { scheme } returns "https" + } } } diff --git a/libs/pandautils/src/test/java/com/instructure/pandautils/room/offline/facade/LockInfoFacadeTest.kt b/libs/pandautils/src/test/java/com/instructure/pandautils/room/offline/facade/LockInfoFacadeTest.kt index 726ac3f71b..52a439b713 100644 --- a/libs/pandautils/src/test/java/com/instructure/pandautils/room/offline/facade/LockInfoFacadeTest.kt +++ b/libs/pandautils/src/test/java/com/instructure/pandautils/room/offline/facade/LockInfoFacadeTest.kt @@ -59,10 +59,13 @@ class LockInfoFacadeTest { coEvery { completionRequirementDao.findById(any()) } returns null + val lockInfoEntity = LockInfoEntity(lockInfo, assignmentId) + coEvery { lockInfoDao.findByRowId(any()) } returns lockInfoEntity + facade.insertLockInfoForAssignment(lockInfo, assignmentId) coVerify { lockInfoDao.insert(LockInfoEntity(lockInfo, assignmentId)) } - coVerify { lockedModuleDao.insert(LockedModuleEntity(lockedModule)) } + coVerify { lockedModuleDao.insert(LockedModuleEntity(lockedModule, lockInfoEntity.id)) } coVerify { moduleNameDao.insertAll(prerequisites.map { ModuleNameEntity(it, 1L) }) } coVerify { completionRequirementDao.insert(ModuleCompletionRequirementEntity(completionRequirements.first(), 1L, 1L)) } } @@ -77,10 +80,13 @@ class LockInfoFacadeTest { coEvery { completionRequirementDao.findById(any()) } returns null + val lockInfoEntity = LockInfoEntity(lockInfo, moduleId = moduleId) + coEvery { lockInfoDao.findByRowId(any()) } returns lockInfoEntity + facade.insertLockInfoForModule(lockInfo, moduleId) - coVerify { lockInfoDao.insert(LockInfoEntity(lockInfo, moduleId = moduleId)) } - coVerify { lockedModuleDao.insert(LockedModuleEntity(lockedModule)) } + coVerify { lockInfoDao.insert(lockInfoEntity) } + coVerify { lockedModuleDao.insert(LockedModuleEntity(lockedModule, lockInfoEntity.id)) } coVerify { moduleNameDao.insertAll(prerequisites.map { ModuleNameEntity(it, 1L) }) } coVerify { completionRequirementDao.insert(ModuleCompletionRequirementEntity(completionRequirements.first(), 1L, 1L)) } } @@ -93,8 +99,10 @@ class LockInfoFacadeTest { val lockedModule = LockedModule(id = 1L, prerequisites = prerequisites, completionRequirements = completionRequirements, contextId = 1) val lockInfo = LockInfo(modulePrerequisiteNames = arrayListOf("1", "2"), contextModule = lockedModule, unlockAt = Date().toApiString()) - coEvery { lockInfoDao.findByAssignmentId(assignmentId) } returns LockInfoEntity(lockInfo, assignmentId) - coEvery { lockedModuleDao.findById(any()) } returns LockedModuleEntity(lockedModule) + val lockInfoEntity = LockInfoEntity(lockInfo, assignmentId) + + coEvery { lockInfoDao.findByAssignmentId(assignmentId) } returns lockInfoEntity + coEvery { lockedModuleDao.findById(any()) } returns LockedModuleEntity(lockedModule, lockInfoEntity.id) coEvery { moduleNameDao.findByLockModuleId(any()) } returns prerequisites.map { ModuleNameEntity(it, 1L) } coEvery { completionRequirementDao.findByModuleId(any()) } returns completionRequirements.map { ModuleCompletionRequirementEntity(it, 1L, 1L) @@ -114,8 +122,10 @@ class LockInfoFacadeTest { val lockedModule = LockedModule(id = 1L, prerequisites = prerequisites, completionRequirements = completionRequirements, contextId = 1) val lockInfo = LockInfo(modulePrerequisiteNames = arrayListOf("1", "2"), contextModule = lockedModule, unlockAt = Date().toApiString()) - coEvery { lockInfoDao.findByModuleId(moduleId) } returns LockInfoEntity(lockInfo, moduleId = moduleId) - coEvery { lockedModuleDao.findById(any()) } returns LockedModuleEntity(lockedModule) + val lockInfoEntity = LockInfoEntity(lockInfo, moduleId = moduleId) + + coEvery { lockInfoDao.findByModuleId(moduleId) } returns lockInfoEntity + coEvery { lockedModuleDao.findById(any()) } returns LockedModuleEntity(lockedModule, lockInfoEntity.id) coEvery { moduleNameDao.findByLockModuleId(any()) } returns prerequisites.map { ModuleNameEntity(it, 1L) } coEvery { completionRequirementDao.findByModuleId(any()) } returns completionRequirements.map { ModuleCompletionRequirementEntity(it, 1L, 1L) diff --git a/libs/recyclerview/src/main/java/com/instructure/pandarecycler/PaginatedRecyclerAdapter.kt b/libs/recyclerview/src/main/java/com/instructure/pandarecycler/PaginatedRecyclerAdapter.kt index 49b8dbdc75..79cf3f66ed 100644 --- a/libs/recyclerview/src/main/java/com/instructure/pandarecycler/PaginatedRecyclerAdapter.kt +++ b/libs/recyclerview/src/main/java/com/instructure/pandarecycler/PaginatedRecyclerAdapter.kt @@ -146,6 +146,13 @@ abstract class PaginatedRecyclerAdapter(c loadData() } + fun silentRefresh() { + nextUrl = null + resetBooleans() + isRefresh = false // We don't care about fresh data here, just want to update the offline state + loadData() + } + override fun resetData() { clear() nextUrl = null From 5d77dc8b46c7c66f73cda291fbe1f7f5d4d74371 Mon Sep 17 00:00:00 2001 From: Tamas Kozmer <72397075+tamaskozmer@users.noreply.github.com> Date: Fri, 17 Nov 2023 16:02:33 +0100 Subject: [PATCH 011/132] Fixed progress id issues. (#2255) --- .../1.json | 14 ++- .../offline/daos/FileSyncProgressDaoTest.kt | 96 ++++++++++++++----- .../features/offline/sync/FileSync.kt | 59 ++++++------ .../room/offline/daos/FileSyncProgressDao.kt | 3 + .../entities/FileSyncProgressEntity.kt | 3 +- 5 files changed, 120 insertions(+), 55 deletions(-) diff --git a/libs/pandautils/schemas/com.instructure.pandautils.room.offline.OfflineDatabase/1.json b/libs/pandautils/schemas/com.instructure.pandautils.room.offline.OfflineDatabase/1.json index 4a8f8781a2..644075b091 100644 --- a/libs/pandautils/schemas/com.instructure.pandautils.room.offline.OfflineDatabase/1.json +++ b/libs/pandautils/schemas/com.instructure.pandautils.room.offline.OfflineDatabase/1.json @@ -2,7 +2,7 @@ "formatVersion": 1, "database": { "version": 1, - "identityHash": "a1cb353c4afbf14831de8752ace07ce9", + "identityHash": "1182046fc85ec1b16d7693e1a593081b", "entities": [ { "tableName": "AssignmentDueDateEntity", @@ -5433,7 +5433,7 @@ }, { "tableName": "FileSyncProgressEntity", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`fileId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `courseId` INTEGER NOT NULL, `fileName` TEXT NOT NULL, `progress` INTEGER NOT NULL, `fileSize` INTEGER NOT NULL, `additionalFile` INTEGER NOT NULL, `progressState` TEXT NOT NULL, FOREIGN KEY(`courseId`) REFERENCES `CourseSyncProgressEntity`(`courseId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`fileId` INTEGER NOT NULL, `courseId` INTEGER NOT NULL, `fileName` TEXT NOT NULL, `progress` INTEGER NOT NULL, `fileSize` INTEGER NOT NULL, `additionalFile` INTEGER NOT NULL, `progressState` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, FOREIGN KEY(`courseId`) REFERENCES `CourseSyncProgressEntity`(`courseId`) ON UPDATE NO ACTION ON DELETE CASCADE )", "fields": [ { "fieldPath": "fileId", @@ -5476,12 +5476,18 @@ "columnName": "progressState", "affinity": "TEXT", "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true } ], "primaryKey": { "autoGenerate": true, "columnNames": [ - "fileId" + "id" ] }, "indices": [], @@ -5503,7 +5509,7 @@ "views": [], "setupQueries": [ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'a1cb353c4afbf14831de8752ace07ce9')" + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '1182046fc85ec1b16d7693e1a593081b')" ] } } \ No newline at end of file diff --git a/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/FileSyncProgressDaoTest.kt b/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/FileSyncProgressDaoTest.kt index 6802ae1119..afa0edd429 100644 --- a/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/FileSyncProgressDaoTest.kt +++ b/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/FileSyncProgressDaoTest.kt @@ -76,7 +76,8 @@ class FileSyncProgressDaoTest { progress = 0, fileSize = 1000L, progressState = ProgressState.IN_PROGRESS, - fileId = 1L + fileId = 1L, + id = 1L ) fileSyncProgressDao.insert(entity) @@ -92,7 +93,8 @@ class FileSyncProgressDaoTest { progress = 0, fileSize = 1000L, progressState = ProgressState.IN_PROGRESS, - fileId = 1L + fileId = 1L, + id = 1L ) fileSyncProgressDao.insertAll(listOf(entity)) @@ -143,7 +145,8 @@ class FileSyncProgressDaoTest { progress = 0, fileSize = 1000L, progressState = ProgressState.IN_PROGRESS, - fileId = 1L + fileId = 1L, + id = 1L ), FileSyncProgressEntity( courseId = 1L, @@ -151,7 +154,8 @@ class FileSyncProgressDaoTest { progress = 0, fileSize = 1000L, progressState = ProgressState.IN_PROGRESS, - fileId = 2L + fileId = 2L, + id = 2L ) ) fileSyncProgressDao.insertAll(entities) @@ -161,6 +165,33 @@ class FileSyncProgressDaoTest { assertEquals(entities[0], result) } + @Test + fun testFindById() = runTest { + val entities = listOf( + FileSyncProgressEntity( + courseId = 1L, + fileName = "File 1", + progress = 0, + fileSize = 1000L, + progressState = ProgressState.IN_PROGRESS, + fileId = 100L + ), + FileSyncProgressEntity( + courseId = 1L, + fileName = "File 2", + progress = 0, + fileSize = 1000L, + progressState = ProgressState.IN_PROGRESS, + fileId = 200L + ) + ) + fileSyncProgressDao.insertAll(entities) + + val result = fileSyncProgressDao.findById(1L) + + assertEquals(entities[0].copy(id = 1L), result) + } + @Test fun testFindByFileIdLiveData() = runTest { val entities = listOf( @@ -170,7 +201,8 @@ class FileSyncProgressDaoTest { progress = 0, fileSize = 1000L, progressState = ProgressState.IN_PROGRESS, - fileId = 1L + fileId = 1L, + id = 1L ), FileSyncProgressEntity( courseId = 1L, @@ -178,7 +210,8 @@ class FileSyncProgressDaoTest { progress = 0, fileSize = 1000L, progressState = ProgressState.IN_PROGRESS, - fileId = 2L + fileId = 2L, + id = 2L ) ) fileSyncProgressDao.insertAll(entities) @@ -199,7 +232,8 @@ class FileSyncProgressDaoTest { progress = 0, fileSize = 1000L, progressState = ProgressState.IN_PROGRESS, - fileId = 1L + fileId = 1L, + id = 1L ), FileSyncProgressEntity( courseId = 1L, @@ -207,7 +241,8 @@ class FileSyncProgressDaoTest { progress = 0, fileSize = 1000L, progressState = ProgressState.IN_PROGRESS, - fileId = 2L + fileId = 2L, + id = 2L ), FileSyncProgressEntity( courseId = 2L, @@ -215,7 +250,8 @@ class FileSyncProgressDaoTest { progress = 0, fileSize = 1000L, progressState = ProgressState.IN_PROGRESS, - fileId = 3L + fileId = 3L, + id = 3L ) ) fileSyncProgressDao.insertAll(entities) @@ -236,7 +272,8 @@ class FileSyncProgressDaoTest { progress = 0, fileSize = 1000L, progressState = ProgressState.IN_PROGRESS, - fileId = 1L + fileId = 1L, + id = 1L ), FileSyncProgressEntity( courseId = 1L, @@ -244,7 +281,8 @@ class FileSyncProgressDaoTest { progress = 0, fileSize = 1000L, progressState = ProgressState.IN_PROGRESS, - fileId = 2L + fileId = 2L, + id = 2L ), FileSyncProgressEntity( courseId = 2L, @@ -252,7 +290,8 @@ class FileSyncProgressDaoTest { progress = 0, fileSize = 1000L, progressState = ProgressState.IN_PROGRESS, - fileId = 3L + fileId = 3L, + id = 3L ) ) fileSyncProgressDao.insertAll(entities) @@ -303,7 +342,8 @@ class FileSyncProgressDaoTest { progress = 0, fileSize = 1000L, progressState = ProgressState.IN_PROGRESS, - fileId = 1L + fileId = 1L, + id = 1L ), FileSyncProgressEntity( courseId = 1L, @@ -311,7 +351,8 @@ class FileSyncProgressDaoTest { progress = 0, fileSize = 1000L, progressState = ProgressState.IN_PROGRESS, - fileId = 2L + fileId = 2L, + id = 2L ), FileSyncProgressEntity( courseId = 1L, @@ -320,7 +361,8 @@ class FileSyncProgressDaoTest { fileSize = 1000L, additionalFile = true, progressState = ProgressState.IN_PROGRESS, - fileId = 3L + fileId = 3L, + id = 3L ), FileSyncProgressEntity( courseId = 1L, @@ -329,7 +371,8 @@ class FileSyncProgressDaoTest { fileSize = 0, additionalFile = true, progressState = ProgressState.IN_PROGRESS, - fileId = 4L + fileId = 4L, + id = 4L ) ) @@ -350,7 +393,8 @@ class FileSyncProgressDaoTest { progress = 0, fileSize = 1000L, progressState = ProgressState.IN_PROGRESS, - fileId = 1L + fileId = 1L, + id = 1L ), FileSyncProgressEntity( courseId = 1L, @@ -358,7 +402,8 @@ class FileSyncProgressDaoTest { progress = 0, fileSize = 1000L, progressState = ProgressState.IN_PROGRESS, - fileId = 2L + fileId = 2L, + id = 2L ), FileSyncProgressEntity( courseId = 1L, @@ -367,7 +412,8 @@ class FileSyncProgressDaoTest { fileSize = 1000L, additionalFile = true, progressState = ProgressState.IN_PROGRESS, - fileId = 3L + fileId = 3L, + id = 3L ), FileSyncProgressEntity( courseId = 1L, @@ -376,7 +422,8 @@ class FileSyncProgressDaoTest { fileSize = 0, additionalFile = true, progressState = ProgressState.IN_PROGRESS, - fileId = 4L + fileId = 4L, + id = 4L ) ) @@ -397,7 +444,8 @@ class FileSyncProgressDaoTest { progress = 0, fileSize = 1000L, progressState = ProgressState.IN_PROGRESS, - fileId = 1L + fileId = 1L, + id = 1L ), FileSyncProgressEntity( courseId = 1L, @@ -405,7 +453,8 @@ class FileSyncProgressDaoTest { progress = 0, fileSize = 1000L, progressState = ProgressState.IN_PROGRESS, - fileId = 2L + fileId = 2L, + id = 2L ) ) @@ -417,7 +466,8 @@ class FileSyncProgressDaoTest { progress = 0, fileSize = 1000L, progressState = ProgressState.IN_PROGRESS, - fileId = 3L + fileId = 3L, + id = 3L ) val rowId = fileSyncProgressDao.insert(entity) diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/sync/FileSync.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/sync/FileSync.kt index a851435233..d24625a705 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/sync/FileSync.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/sync/FileSync.kt @@ -68,14 +68,18 @@ class FileSync( val fileFolders = fileFolderDao.findFilesToSync(courseId, syncSettings.fullFileSync) .map { it.toApiModel() } - fileSyncProgressDao.insertAll(fileFolders.map { createProgress(it, courseId, false) }) + val syncData = mutableListOf() + + fileFolders.forEach { + val progressId = createAndInsertProgress(it, courseId, false) + syncData.add(FileSyncData(progressId, it.id, it.displayName.orEmpty(), it.url.orEmpty(), courseId, false)) + } courseSyncProgressDao.findByCourseId(courseId)?.copy(progressState = ProgressState.IN_PROGRESS)?.let { courseSyncProgressDao.update(it) } - val chunks = - fileFolders.map { FileSyncData(it.id, it.displayName.orEmpty(), it.url.orEmpty(), courseId) }.chunked(6) + val chunks = syncData.chunked(6) coroutineScope { chunks.forEach { @@ -94,9 +98,12 @@ class FileSync( ) { val courseId = syncSettings.courseId - val additionalPublicFilesToSync = fileFolderDao.findByIds(additionalFileIdsToSync).map { it.toApiModel() } + val localIds = localFileDao.findByCourseId(courseId).map { it.id }.toSet() + val internalFileIdsToSync = additionalFileIdsToSync.filterNot { localIds.contains(it) }.toSet() + + val additionalPublicFilesToSync = fileFolderDao.findByIds(internalFileIdsToSync).map { it.toApiModel() } - val nonPublicFileIds = additionalFileIdsToSync.minus(additionalPublicFilesToSync.map { it.id }.toSet()) + val nonPublicFileIds = internalFileIdsToSync.minus(additionalPublicFilesToSync.map { it.id }.toSet()) val nonPublicFiles = nonPublicFileIds.map { fileFolderApi.getCourseFile(courseId, it, RestParams(isForceReadFromNetwork = false)).dataOrNull }.filterNotNull() @@ -104,23 +111,17 @@ class FileSync( fileFolderDao.insertAll(nonPublicFiles.map { FileFolderEntity(it) }) val additionalFiles = (additionalPublicFilesToSync + nonPublicFiles).filter { !it.url.isNullOrEmpty() } - fileSyncProgressDao.insertAll(additionalFiles.map { createProgress(it, courseId, true) }) val syncData = mutableListOf() - additionalFiles.map { - FileSyncData( - it.id, - it.displayName.orEmpty(), - it.url.orEmpty(), - courseId, - false - ) - }.let { syncData.addAll(it) } + additionalFiles.forEach { + val progressId = createAndInsertProgress(it, courseId, true) + syncData.add(FileSyncData(progressId, it.id, it.displayName.orEmpty(), it.url.orEmpty(), courseId, false)) + } externalFilesToSync.forEach { - val id = createExternalProgress(it, courseId) - syncData.add(FileSyncData(id, Uri.parse(it).lastPathSegment.orEmpty(), it, courseId, true)) + val progressId = createAndInsertExternalProgress(it, courseId) + syncData.add(FileSyncData(progressId, -1, Uri.parse(it).lastPathSegment.orEmpty(), it, courseId, true)) } courseSyncProgressDao.findByCourseId(courseId)?.copy(additionalFilesStarted = true) @@ -157,7 +158,7 @@ class FileSync( // External images can fail for various reasons (for example the file is no longer available), // so we just mark them as completed in this case. The user wouldn't see those in online mode anyway. if (fileSyncData.externalFile && downloadResult is DataResult.Fail) { - updateProgress(fileSyncData.fileId, 100, ProgressState.COMPLETED, fileSyncData.externalFile) + updateProgress(fileSyncData.progressId, 100, ProgressState.COMPLETED, fileSyncData.externalFile) return } @@ -168,7 +169,7 @@ class FileSync( if (isStopped) throw IllegalStateException("Worker was stopped") when (it) { is DownloadState.InProgress -> { - updateProgress(fileSyncData.fileId, it.progress, ProgressState.IN_PROGRESS, fileSyncData.externalFile, it.totalBytes) + updateProgress(fileSyncData.progressId, it.progress, ProgressState.IN_PROGRESS, fileSyncData.externalFile, it.totalBytes) } is DownloadState.Success -> { @@ -190,7 +191,7 @@ class FileSync( ) ) } - updateProgress(fileSyncData.fileId, 100, ProgressState.COMPLETED, fileSyncData.externalFile, it.totalBytes) + updateProgress(fileSyncData.progressId, 100, ProgressState.COMPLETED, fileSyncData.externalFile, it.totalBytes) } is DownloadState.Failure -> { @@ -200,7 +201,7 @@ class FileSync( } } catch (e: Exception) { downloadedFile.delete() - updateProgress(fileSyncData.fileId, 0, ProgressState.ERROR, fileSyncData.externalFile) + updateProgress(fileSyncData.progressId, 0, ProgressState.ERROR, fileSyncData.externalFile) firebaseCrashlytics.recordException(e) } } @@ -258,8 +259,8 @@ class FileSync( return fileFolderDao.findAllFilesByCourseId(courseId) } - private fun createProgress(fileFolder: FileFolder, courseId: Long, additionalFile: Boolean): FileSyncProgressEntity { - return FileSyncProgressEntity( + private suspend fun createAndInsertProgress(fileFolder: FileFolder, courseId: Long, additionalFile: Boolean): Long { + val progress = FileSyncProgressEntity( fileFolder.id, courseId, fileFolder.displayName.orEmpty(), @@ -268,9 +269,12 @@ class FileSync( additionalFile, ProgressState.IN_PROGRESS ) + + val rowId = fileSyncProgressDao.insert(progress) + return fileSyncProgressDao.findByRowId(rowId)?.id ?: -1L } - private suspend fun createExternalProgress(url: String, courseId: Long): Long { + private suspend fun createAndInsertExternalProgress(url: String, courseId: Long): Long { val progress = FileSyncProgressEntity( 0, courseId, @@ -281,11 +285,11 @@ class FileSync( ProgressState.IN_PROGRESS ) val rowId = fileSyncProgressDao.insert(progress) - return fileSyncProgressDao.findByRowId(rowId)?.fileId ?: -1L + return fileSyncProgressDao.findByRowId(rowId)?.id ?: -1L } - private suspend fun updateProgress(fileId: Long, progress: Int, progressState: ProgressState, externalFile: Boolean, size: Long? = null) { - var newProgress = fileSyncProgressDao.findByFileId(fileId)?.copy(progress = progress, progressState = progressState) + private suspend fun updateProgress(progressId: Long, progress: Int, progressState: ProgressState, externalFile: Boolean, size: Long? = null) { + var newProgress = fileSyncProgressDao.findById(progressId)?.copy(progress = progress, progressState = progressState) if (externalFile && size != null) { newProgress = newProgress?.copy(fileSize = size) } @@ -295,6 +299,7 @@ class FileSync( } data class FileSyncData( + val progressId: Long, val fileId: Long, val inputFileName: String, val fileUrl: String, diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/FileSyncProgressDao.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/FileSyncProgressDao.kt index d2854419e5..09a8b008e7 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/FileSyncProgressDao.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/FileSyncProgressDao.kt @@ -37,6 +37,9 @@ interface FileSyncProgressDao { @Update suspend fun update(fileSyncProgressEntity: FileSyncProgressEntity) + @Query("SELECT * FROM FileSyncProgressEntity WHERE id = :id") + suspend fun findById(id: Long): FileSyncProgressEntity? + @Query("SELECT * FROM FileSyncProgressEntity WHERE courseId = :courseId") fun findByCourseIdLiveData(courseId: Long): LiveData> diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/FileSyncProgressEntity.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/FileSyncProgressEntity.kt index 261d0dd40f..6c768215cd 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/FileSyncProgressEntity.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/FileSyncProgressEntity.kt @@ -33,7 +33,6 @@ import com.instructure.pandautils.features.offline.sync.ProgressState ] ) data class FileSyncProgressEntity( - @PrimaryKey(autoGenerate = true) val fileId: Long, val courseId: Long, val fileName: String, @@ -41,4 +40,6 @@ data class FileSyncProgressEntity( val fileSize: Long, val additionalFile: Boolean = false, val progressState: ProgressState, + @PrimaryKey(autoGenerate = true) + val id: Long = 0 ) \ No newline at end of file From 211128d6b9648c28c007e014416d54af042f1699 Mon Sep 17 00:00:00 2001 From: Akos Hermann <72087159+hermannakos@users.noreply.github.com> Date: Fri, 17 Nov 2023 16:02:58 +0100 Subject: [PATCH 012/132] [MBL-17201][Student] Fetch feature flags for the correct user (#2253) refs: MBL-17201 affects: Student release note: none test plan: Check the offline features. --- .../main/java/com/instructure/pandautils/di/FeatureFlagModule.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/di/FeatureFlagModule.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/di/FeatureFlagModule.kt index 5a38909b2a..5913eb5395 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/di/FeatureFlagModule.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/di/FeatureFlagModule.kt @@ -32,7 +32,6 @@ import javax.inject.Singleton class FeatureFlagModule { @Provides - @Singleton fun provideFeatureFlagProvider(userManager: UserManager, apiPrefs: ApiPrefs, featuresApi: FeaturesAPI.FeaturesInterface, environmentFeatureFlagsDao: EnvironmentFeatureFlagsDao): FeatureFlagProvider { return FeatureFlagProvider(userManager, apiPrefs, featuresApi, environmentFeatureFlagsDao) } From fe3d39001c37042e44d755356656b29e61b58bd5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81kos=20Hermann?= Date: Tue, 21 Nov 2023 11:37:09 +0100 Subject: [PATCH 013/132] version bump --- apps/teacher/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/teacher/build.gradle b/apps/teacher/build.gradle index 3ffda0139f..e0699ea43f 100644 --- a/apps/teacher/build.gradle +++ b/apps/teacher/build.gradle @@ -39,8 +39,8 @@ android { defaultConfig { minSdkVersion Versions.MIN_SDK targetSdkVersion Versions.TARGET_SDK - versionCode = 60 - versionName = '1.26.0' + versionCode = 61 + versionName = '1.27.0' vectorDrawables.useSupportLibrary = true multiDexEnabled true testInstrumentationRunner 'com.instructure.teacher.ui.espresso.TeacherHiltTestRunner' From 1255563972aa97890f6531e56af25ce836f2d986 Mon Sep 17 00:00:00 2001 From: inst-danger Date: Tue, 21 Nov 2023 12:35:23 +0100 Subject: [PATCH 014/132] Update translations (#2256) --- apps/flutter_parent/lib/l10n/res/intl_id.arb | 85 +++++++++- .../lib/l10n/res/intl_id.arb | 15 +- .../src/main/res/values-id/strings.xml | 3 +- .../src/main/res/values-ar/strings.xml | 17 +- .../res/values-b+da+DK+instk12/strings.xml | 17 +- .../res/values-b+en+AU+unimelb/strings.xml | 17 +- .../res/values-b+en+GB+instukhe/strings.xml | 17 +- .../res/values-b+nb+NO+instk12/strings.xml | 17 +- .../res/values-b+sv+SE+instk12/strings.xml | 17 +- .../src/main/res/values-b+zh+HK/strings.xml | 17 +- .../src/main/res/values-b+zh+Hans/strings.xml | 17 +- .../src/main/res/values-b+zh+Hant/strings.xml | 17 +- .../src/main/res/values-ca/strings.xml | 17 +- .../src/main/res/values-cy/strings.xml | 17 +- .../src/main/res/values-da/strings.xml | 17 +- .../src/main/res/values-de/strings.xml | 17 +- .../src/main/res/values-en-rAU/strings.xml | 17 +- .../src/main/res/values-en-rCY/strings.xml | 17 +- .../src/main/res/values-en-rGB/strings.xml | 17 +- .../src/main/res/values-es-rES/strings.xml | 17 +- .../src/main/res/values-es/strings.xml | 17 +- .../src/main/res/values-fi/strings.xml | 17 +- .../src/main/res/values-fr-rCA/strings.xml | 17 +- .../src/main/res/values-fr/strings.xml | 17 +- .../src/main/res/values-ht/strings.xml | 17 +- .../src/main/res/values-id/strings.xml | 159 +++++++++++++++++- .../src/main/res/values-is/strings.xml | 17 +- .../src/main/res/values-it/strings.xml | 17 +- .../src/main/res/values-ja/strings.xml | 17 +- .../src/main/res/values-mi/strings.xml | 17 +- .../src/main/res/values-ms/strings.xml | 17 +- .../src/main/res/values-nb/strings.xml | 17 +- .../src/main/res/values-nl/strings.xml | 17 +- .../src/main/res/values-pl/strings.xml | 17 +- .../src/main/res/values-pt-rBR/strings.xml | 17 +- .../src/main/res/values-pt-rPT/strings.xml | 17 +- .../src/main/res/values-ru/strings.xml | 17 +- .../src/main/res/values-sl/strings.xml | 17 +- .../src/main/res/values-sv/strings.xml | 17 +- .../src/main/res/values-th/strings.xml | 17 +- .../src/main/res/values-vi/strings.xml | 17 +- .../src/main/res/values-zh/strings.xml | 17 +- 42 files changed, 865 insertions(+), 43 deletions(-) diff --git a/apps/flutter_parent/lib/l10n/res/intl_id.arb b/apps/flutter_parent/lib/l10n/res/intl_id.arb index 8d63ed0338..5975fdcc49 100644 --- a/apps/flutter_parent/lib/l10n/res/intl_id.arb +++ b/apps/flutter_parent/lib/l10n/res/intl_id.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2022-10-28T11:03:07.232972", + "@@last_modified": "2023-08-25T11:04:20.901151", "alertsLabel": "Peringatan", "@alertsLabel": { "description": "The label for the Alerts tab", @@ -182,6 +182,19 @@ "points": {} } }, + "calendarDaySemanticsLabel": "{eventCount,plural, =1{{date}, {eventCount} acara}other{{date}, {eventCount} acara}}", + "@calendarDaySemanticsLabel": { + "description": "Screen reader label used for calendar day, reads the date and count of events", + "type": "text", + "placeholders_order": [ + "date", + "eventCount" + ], + "placeholders": { + "date": {}, + "eventCount": {} + } + }, "No Events Today!": "Tidak Ada Acara Hari Ini!", "@No Events Today!": { "description": "Title displayed when there are no calendar events for the current day", @@ -2666,5 +2679,75 @@ "type": "text", "placeholders_order": [], "placeholders": {} + }, + "Acceptable Use Policy": "Kebijakan Penggunaan yang Dapat Diterima", + "@Acceptable Use Policy": { + "description": "title for the acceptable use policy screen", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Submit": "Serahkan", + "@Submit": { + "description": "submit button title for acceptable use policy screen", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Either you're a new user or the Acceptable Use Policy has changed since you last agreed to it. Please agree to the Acceptable Use Policy before you continue.": "Anda pengguna baru atau Kebijakan Penggunaan yang Dapat Diterima telah berubah sejak Anda terakhir kali menyetujuinya. Silakan setujui Kebijakan Penggunaan yang Dapat Diterima sebelum Anda melanjutkan.", + "@Either you're a new user or the Acceptable Use Policy has changed since you last agreed to it. Please agree to the Acceptable Use Policy before you continue.": { + "description": "acceptable use policy screen description", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "I agree to the Acceptable Use Policy.": "Saya menyetujui Kebijakan Penggunaan yang Dapat Diterima.", + "@I agree to the Acceptable Use Policy.": { + "description": "acceptable use policy switch title", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "About": "Tentang", + "@About": { + "description": "Title for about menu item in settings", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "App": "App", + "@App": { + "description": "Title for App field on about page", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Login ID": "ID Login", + "@Login ID": { + "description": "Title for Login ID field on about page", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Email": "Email", + "@Email": { + "description": "Title for Email field on about page", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Version": "Versi", + "@Version": { + "description": "Title for Version field on about page", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Instructure logo": "Logo Instructure", + "@Instructure logo": { + "description": "Semantics label for the Instructure logo on the about page", + "type": "text", + "placeholders_order": [], + "placeholders": {} } } \ No newline at end of file diff --git a/libs/flutter_student_embed/lib/l10n/res/intl_id.arb b/libs/flutter_student_embed/lib/l10n/res/intl_id.arb index d8ec08b159..50bfd8386a 100644 --- a/libs/flutter_student_embed/lib/l10n/res/intl_id.arb +++ b/libs/flutter_student_embed/lib/l10n/res/intl_id.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2022-10-28T11:03:17.232435", + "@@last_modified": "2023-08-25T11:04:30.842905", "coursesLabel": "Kursus", "@coursesLabel": { "description": "The label for the Courses tab", @@ -101,6 +101,19 @@ "points": {} } }, + "calendarDaySemanticsLabel": "{eventCount,plural, =1{{date}, {eventCount} acara}other{{date}, {eventCount} acara}}", + "@calendarDaySemanticsLabel": { + "description": "Screen reader label used for calendar day, reads the date and count of events", + "type": "text", + "placeholders_order": [ + "date", + "eventCount" + ], + "placeholders": { + "date": {}, + "eventCount": {} + } + }, "No Events Today!": "Tidak Ada Acara Hari Ini!", "@No Events Today!": { "description": "Title displayed when there are no calendar events for the current day", diff --git a/libs/login-api-2/src/main/res/values-id/strings.xml b/libs/login-api-2/src/main/res/values-id/strings.xml index 7433318e75..11403151ae 100644 --- a/libs/login-api-2/src/main/res/values-id/strings.xml +++ b/libs/login-api-2/src/main/res/values-id/strings.xml @@ -110,8 +110,7 @@ school.instructure.com Hapus Pengguna Sebelumnya Tidak dapat menemukan sekolah Anda? Coba ketikkan URL sekolah lengkap. - Ketuk di sini untuk bantuan. - + Ketuk di sini untuk bantuan login. Tidak Ada Sambungan Internet Tindakan ini membutuhkan sambungan internet. Subjek dan deskripsi harus ada untuk menyerahkan umpan balik. diff --git a/libs/pandares/src/main/res/values-ar/strings.xml b/libs/pandares/src/main/res/values-ar/strings.xml index d99ab6d62a..d2f6ca711d 100644 --- a/libs/pandares/src/main/res/values-ar/strings.xml +++ b/libs/pandares/src/main/res/values-ar/strings.xml @@ -1481,6 +1481,21 @@ تحديد الكل إلغاء تحديد الكل حدث خطأ أثناء تحميل المحتوى. + لا يوجد أي مساق + سيتم إدراج مساقاتك هنا، ثم يمكنك إتاحته للاستخدام دون اتصال. + لا يوجد محتوى مساق + سيتم إدراج محتوى المساق هنا، ثم يمكنك إتاحته للاستخدام دون اتصال. + هل تريد تجاهل التغييرات؟ + إذا اخترت التجاهل، لن يتم حفظ التغييرات. + تجاهل + هل تريد مزامنة المحتوى دون اتصال؟ + سيؤدي هذا إلى مزامنة ~%s من المحتوى. وقد يؤدي هذا إلى رسوم إضافية من موفر الخدمة إذا لم تكن متصلاً بشبكة Wi-Fi. + سيؤدي هذا إلى مزامنة ~%s من المحتوى فقط أثناء اتصالك بشبكة Wi-Fi. + مزامنة + جارٍ التحميل... + مهلاً، نحن نجهز لك الأشياء. + توسيع المحتوى + طي المحتوى سيؤدي تمكين مزامنة المحتوى التلقائية إلى تنزيل المحتوى المحدد بناءً على الإعدادات أدناه. ستحدث مزامنة المحتوى حتى لو لم يكن يعمل التطبيق. إذا تم إيقاف تشغيل الإعداد، فلن تحدث المزامنة. لن يتم حذف المحتوى الذي تم تنزيله بالفعل. تكرار المزامنة مزامنة المحتوى التلقائية @@ -1527,7 +1542,6 @@ %d من المساقات قيد المزامنة. %d من المساقات قيد المزامنة. - صور محتوى المساق هذه المهمة لم تعد متوفرة. أنت غير متصل بالإنترنت ليس لديك حاليًا أي مساقات غير متوفرة عبر الإنترنت. @@ -1543,4 +1557,5 @@ تمت مزامنة %d من المساقات. تمت مزامنة %d من المساقات. + محتوى مساق إضافي diff --git a/libs/pandares/src/main/res/values-b+da+DK+instk12/strings.xml b/libs/pandares/src/main/res/values-b+da+DK+instk12/strings.xml index 692494e0bb..9dfdafe588 100644 --- a/libs/pandares/src/main/res/values-b+da+DK+instk12/strings.xml +++ b/libs/pandares/src/main/res/values-b+da+DK+instk12/strings.xml @@ -1410,6 +1410,21 @@ Vælg alle Fravælg alle Der opstod en fejl under indlæsning af indholdet. + Ingen fag + Dine fag vil blive vist her, og derefter kan du gøre dem tilgængelige til offline brug. + Intet fagindhold + Fagindholdet vil blive vist her, og derefter kan du gøre det tilgængeligt til offline brug. + Annuller ændringer? + Hvis du vælger at kassere, vil ændringerne ikke blive gemt. + Kasser + Vil du synkronisere offline indhold? + Dette vil synkronisere ~%s indhold. Det kan medføre yderligere gebyrer fra din dataudbyder, hvis du ikke er forbundet til et wi-fi-netværk. + Dette vil kun synkronisere ~%s indhold, mens du er tilsluttet et wi-fi-netværk. + Synkroniser + Indlæser ... + Vent venligst, mens vi gør tingene klar til dig. + Udvid indhold + Skjul indholdet Aktivering af automatisk indholdssynkronisering sørger for at downloade det valgte indhold baseret på nedenstående indstillinger. Indholdssynkroniseringen gennemføres, selvom applikationen ikke kører. Hvis indstillingen er deaktiveret, sker der ingen synkronisering. Det allerede downloadede indhold vil ikke blive slettet. Synkroniseringsfrekvens Automatisk indholdssynkronisering @@ -1452,7 +1467,6 @@ %d faget synkroniseres. %d fag synkroniseres. - Fagindhold billeder Denne opgave er ikke længere tilgængelig. Du er offline Du har i øjeblikket ingen fag, der er tilgængelige offline. @@ -1464,4 +1478,5 @@ %d faget er blevet synkroniseret. %d fagene er blevet synkroniseret. + Yderligere fagindhold diff --git a/libs/pandares/src/main/res/values-b+en+AU+unimelb/strings.xml b/libs/pandares/src/main/res/values-b+en+AU+unimelb/strings.xml index c2f7ac0a04..fe5fecc1f2 100644 --- a/libs/pandares/src/main/res/values-b+en+AU+unimelb/strings.xml +++ b/libs/pandares/src/main/res/values-b+en+AU+unimelb/strings.xml @@ -1410,6 +1410,21 @@ Select All Deselect All An error occurred while loading the content. + No Subjects + Your subjects will be listed here, and then you can make them available for offline usage. + No Subject Content + The subject content will be listed here, and then you can make them available for offline usage. + Discard Changes? + If you choose to discard, the changes will not be saved. + Discard + Sync Offline Content? + This will sync ~%s content. It may result in additional charges from your data provider if you are not connected to a Wi-Fi network. + This will sync ~%s content only while you are connected to a Wi-Fi network. + Sync + Loading... + Hang tight, we\'re getting things ready for you. + Expand content + Collapse content Enabling the Auto Content Sync will take care of downloading the selected content based on the below settings. The content synchronisation will happen even if the application is not running. If the setting is switched off, then no synchronisation will happen. The already downloaded content will not be deleted. Sync Frequency Auto Content Sync @@ -1452,7 +1467,6 @@ %d subject is syncing. %d subjects are syncing. - Subject content images This assignment is no longer available. You are offline You currently don\'t have any subjects that are available offline. @@ -1464,4 +1478,5 @@ %d subject has been synced. %d subjects have been synced. + Additional subject content diff --git a/libs/pandares/src/main/res/values-b+en+GB+instukhe/strings.xml b/libs/pandares/src/main/res/values-b+en+GB+instukhe/strings.xml index 9a7a75afe0..a86cf9a071 100644 --- a/libs/pandares/src/main/res/values-b+en+GB+instukhe/strings.xml +++ b/libs/pandares/src/main/res/values-b+en+GB+instukhe/strings.xml @@ -1410,6 +1410,21 @@ Select all Un-select All An error occurred while loading the content. + No Modules + Your modules will be listed here, and then you can make them available for offline usage. + No Module Content + The module content will be listed here, and then you can make them available for offline usage. + Discard Changes? + If you choose to discard, the changes will not be saved. + Discard + Sync Offline Content? + This will sync ~%s content. It may result in additional charges from your data provider, if you are not connected to a Wi-Fi network. + This will sync ~%s content only while you are connected to a Wi-Fi network. + Sync + Loading... + Hang tight, we\'re getting things ready for you. + Expand content + Collapse content Enabling the Auto Content Sync will take care of downloading the selected content based on the below settings. The content synchronization will happen even if the application is not running. If the setting is switched off, then no synchronization will happen. The already downloaded content will not be deleted. Sync Frequency Auto Content Sync @@ -1452,7 +1467,6 @@ %d module is syncing. %d modules are syncing. - Module content images This assignment is no longer available. You are offline You currently don\'t have any modules that are available offline. @@ -1464,4 +1478,5 @@ %d module has been synced. %d modules have been synced. + Additional module content diff --git a/libs/pandares/src/main/res/values-b+nb+NO+instk12/strings.xml b/libs/pandares/src/main/res/values-b+nb+NO+instk12/strings.xml index 90a7760b69..b48a88b2e6 100644 --- a/libs/pandares/src/main/res/values-b+nb+NO+instk12/strings.xml +++ b/libs/pandares/src/main/res/values-b+nb+NO+instk12/strings.xml @@ -1411,6 +1411,21 @@ Velg alle Fjern all merking Det oppsto en feil ved lasting av innholdet. + Ingen fag + Fagene dine vil vises her, og du kan deretter gjøre det tilgjengelig for frakoblet bruk. + Intet faginnhold + Faginnholdet vises her, og du kan deretter gjøre det tilgjengelig for frakoblet bruk. + Forkaste endringer? + Hvis du velger å forkaste, blir ikke endringene lagret. + Forkast + Synkronisere frakoblet innhold? + Dette vil synkronisere ~%s-innhold. Dette kan føre til ekstra kostnader fra din datatilbyder hvis du ikke er koblet til et Wi-Fi-nettverk. + Dette vil synkronisere ~%s-innhold bare når du er tilkoblet et Wi-Fi-nettverk. + Synkroniser + Laster… + Vent litt, vi gjør alt klart for deg. + Utvid innhold + Skjul innhold Aktivering av Automatisk synkronisering av innhold vil ta seg av nedlasting av det valgte innholdet basert på følgende innstillinger. Synkronisering av innhold vil skje selv om applikasjonen ikke kjører. Hvis innstillingen er slått av, vil det ikke skje noen synkronisering. Det allerede nedlastede innholdet vil ikke bli slettet. Synkroniseringsfrekvens Automatisk synkronisering av innhold @@ -1453,7 +1468,6 @@ %d fag synkroniseres. %d fag synkroniseres. - Faginnhold-bilder Denne oppgaven er ikke lenger tilgjengelig. Du er frakoblet Du har ingen fag som er tilgjengelig i frakoblet modus. @@ -1465,4 +1479,5 @@ %d fag er synkronisert. %d fag er synkronisert. + Ytterligere faginnhold diff --git a/libs/pandares/src/main/res/values-b+sv+SE+instk12/strings.xml b/libs/pandares/src/main/res/values-b+sv+SE+instk12/strings.xml index 63545c2531..5d2ad46623 100644 --- a/libs/pandares/src/main/res/values-b+sv+SE+instk12/strings.xml +++ b/libs/pandares/src/main/res/values-b+sv+SE+instk12/strings.xml @@ -1410,6 +1410,21 @@ Välj alla Avmarkera alla Ett fel uppstod vid inläsning av innehållet. + Inga kurser + Dina kurser kommer att listas här, och sedan kan du göra dem tillgängliga för offlineanvändning. + Inget kursinnehåll + Kursinnehållet kommer att listas här, och sedan kan du göra dem tillgängliga för offlineanvändning. + Ta bort ändringar? + Om du väljer att avvisa kommer ändringarna inte att sparas. + Avbryt + Synkronisera offlineinnehåll? + Detta synkroniserar ~%s-innehåll. Detta kan medföra ytterligare kostnader från din dataleverantör om du inte är ansluten till ett wifi-nätverk. + Detta synkroniserar ~%s-innehåll endast när du är ansluten till ett wifi-nätverk. + Synkronisera + Läser in ... + Vänta, vi förbereder åt dig. + Visa innehållet + Dölj innehållet Om automatisk innehållssynkronisering aktiveras laddas det valda innehållet ned baserat på nedanstående inställningar. Innehållssynkroniseringen sker även om programmet inte körs. Om inställningen är avstängd sker ingen synkronisering. Innehåll som redan laddats ned raderas inte. Synkroniseringsfrekvens Automatisk innehållssynkronisering @@ -1452,7 +1467,6 @@ %d-kurs synkroniserar. %d-kurser synkroniserar. - Bilder i kursinnehållet Denna uppgift är inte längre tillgänglig. Du är offline Du har för närvarande inte några kurser som är tillgängliga offline. @@ -1464,4 +1478,5 @@ %d-kurs har synkroniserats. %d-kurser har synkroniserats. + Extra kursinnehåll diff --git a/libs/pandares/src/main/res/values-b+zh+HK/strings.xml b/libs/pandares/src/main/res/values-b+zh+HK/strings.xml index 1c0a7022e7..c38571beeb 100644 --- a/libs/pandares/src/main/res/values-b+zh+HK/strings.xml +++ b/libs/pandares/src/main/res/values-b+zh+HK/strings.xml @@ -1392,6 +1392,21 @@ 全選 取消全選 載入內容時發生錯誤。 + 無課程 + 您的課程將將在此處列出,然後您可以讓它們可以離線使用。 + 沒有課程內容 + 課程內容將在此處列出,然後您可以讓它們可以離線使用。 + 捨棄變更? + 如果您選擇捨棄,系統將不會儲存變更。 + 放棄 + 同步離線內容? + 這將同步 ~%s 內容。如果您未連線到 Wi-Fi 網路,則可能會導致您的資料提供者收取額外費用。 + 這將僅在您連線到 Wi-Fi 網路時同步 ~%s 內容。 + 同步 + 正在載入…… + 請稍等,我們已經為您準備好了。 + 展開內容 + 收起內容 啟用「自動內容同步」將根據以下設定注意下載所選取的內容。內容同步將會進行,即使應用程式並未正在執行中。如果關閉設定,則將不會進行同步。已經下載的內容也將不會刪除。 同步頻率 自動內容同步 @@ -1433,7 +1448,6 @@ %d 課程正在同步中。 - 課程內容影像 此作業不再可用。 您已離線 您目前沒有任何可離線使用的課程。 @@ -1444,4 +1458,5 @@ %d 課程已同步。 + 額外課程內容 diff --git a/libs/pandares/src/main/res/values-b+zh+Hans/strings.xml b/libs/pandares/src/main/res/values-b+zh+Hans/strings.xml index f9e4a98069..d2e4fdcb21 100644 --- a/libs/pandares/src/main/res/values-b+zh+Hans/strings.xml +++ b/libs/pandares/src/main/res/values-b+zh+Hans/strings.xml @@ -1392,6 +1392,21 @@ 全选 取消全选 加载内容时出错。 + 没有课程 + 此处将列出您的课程,然后您可以使内容离线可用。 + 无课程内容 + 此处将列出课程内容,然后您可以使内容离线可用。 + 放弃更改? + 如果您选择放弃,更改将不会保存。 + 放弃 + 同步离线内容? + 大约会有 %s 项内容同步。如果未连接到无线网络,您的数据流量提供商可能会额外收费。 + 仅当连接到无线网络时,大约会有 %s 项内容同步。 + 同步 + 加载中... + 正在准备中,请稍等。 + 扩展内容 + 折叠内容 如果启用“自动同步内容”,将根据以下设置下载所选内容。即使应用程序没有运行,内容也会同步。如果关闭该设置,将不会同步。已下载的内容不会被删除。 同步周期 自动同步内容 @@ -1433,7 +1448,6 @@ %d 门课程正在同步。 - 课程内容图像 此作业不再可用。 您已离线 您目前没有任何可离线使用的课程。 @@ -1444,4 +1458,5 @@ %d 门课程已同步。 + 更多课程内容 diff --git a/libs/pandares/src/main/res/values-b+zh+Hant/strings.xml b/libs/pandares/src/main/res/values-b+zh+Hant/strings.xml index 1c0a7022e7..c38571beeb 100644 --- a/libs/pandares/src/main/res/values-b+zh+Hant/strings.xml +++ b/libs/pandares/src/main/res/values-b+zh+Hant/strings.xml @@ -1392,6 +1392,21 @@ 全選 取消全選 載入內容時發生錯誤。 + 無課程 + 您的課程將將在此處列出,然後您可以讓它們可以離線使用。 + 沒有課程內容 + 課程內容將在此處列出,然後您可以讓它們可以離線使用。 + 捨棄變更? + 如果您選擇捨棄,系統將不會儲存變更。 + 放棄 + 同步離線內容? + 這將同步 ~%s 內容。如果您未連線到 Wi-Fi 網路,則可能會導致您的資料提供者收取額外費用。 + 這將僅在您連線到 Wi-Fi 網路時同步 ~%s 內容。 + 同步 + 正在載入…… + 請稍等,我們已經為您準備好了。 + 展開內容 + 收起內容 啟用「自動內容同步」將根據以下設定注意下載所選取的內容。內容同步將會進行,即使應用程式並未正在執行中。如果關閉設定,則將不會進行同步。已經下載的內容也將不會刪除。 同步頻率 自動內容同步 @@ -1433,7 +1448,6 @@ %d 課程正在同步中。 - 課程內容影像 此作業不再可用。 您已離線 您目前沒有任何可離線使用的課程。 @@ -1444,4 +1458,5 @@ %d 課程已同步。 + 額外課程內容 diff --git a/libs/pandares/src/main/res/values-ca/strings.xml b/libs/pandares/src/main/res/values-ca/strings.xml index bcb976cfa4..9cec769a5d 100644 --- a/libs/pandares/src/main/res/values-ca/strings.xml +++ b/libs/pandares/src/main/res/values-ca/strings.xml @@ -1411,6 +1411,21 @@ Selecciona-ho tot Anul·la la selecció de tot S\'ha produït un error en carregar el contingut. + No hi ha cap assignatura + Les vostres assignatures s’enumeraran aquí; posteriorment, podeu fer que estiguin disponibles per utilitzar-se sense connexió. + No hi ha cap contingut de l’assignatura + El contingut de l’assignatura s’enumerarà aquí; posteriorment, podeu fer que estigui disponible per utilitzar-se sense connexió. + Voleu rebutjar els canvis? + Si trieu rebutjar-los, no es desaran els canvis. + Rebutja + Voleu sincronitzar el contingut sense connexió? + Amb aquesta operació se sincronitzarà el contingut de ~%s. Si no us heu connectat a cap xarxa Wi-Fi, és possible que el proveïdor de dades us cobri algun import addicional. + Amb aquesta operació se sincronitzarà el contingut de ~%s només mentre tingueu connexió a una xarxa Wi-Fi. + Sincronitza + S\'està carregant… + Un moment, us ho estem preparant. + Desplega el contingut + Contrau el contingut En activar la sincronització automàtica del contingut es baixarà el contingut seleccionat segons les opcions de configuració indicades a continuació. Se sincronitzarà el contingut encara que l’aplicació no s’estigui executant. Si el paràmetre està desactivat no es durà a terme la sincronització. No se suprimirà el contingut que ja s’hagi baixat. Freqüència de sincronització Sincronització automàtica del contingut @@ -1453,7 +1468,6 @@ S’està sincronitzant %d assignatura. S’estan sincronitzant %d assignatures. - Imatges del contingut de l\'assignatura Aquesta activitat ja no està disponible. Esteu sense connexió Actualment, no teniu cap assignatura que estigui disponible sense connexió. @@ -1465,4 +1479,5 @@ S’ha sincronitzat %d assignatura. S’han sincronitzat %d assignatures. + Contingut de l’assignatura addicional diff --git a/libs/pandares/src/main/res/values-cy/strings.xml b/libs/pandares/src/main/res/values-cy/strings.xml index 84e4e1f225..64444749c2 100644 --- a/libs/pandares/src/main/res/values-cy/strings.xml +++ b/libs/pandares/src/main/res/values-cy/strings.xml @@ -1410,6 +1410,21 @@ Dewis y cyfan Dad-ddewis y Cyfan Gwall wrth lwytho’r cynnwys. + Dim Cyrsiau + Bydd eich cyrsiau’n cael eu rhestru yma, ac yna gallwch chi ei gwneud ar gael i\'w defnyddio all-lein. + Dim Cynnwys Cwrs + Bydd cynnwys y cwrs yn cael ei restru yma, ac yna gallwch chi ei gwneud ar gael i\'w defnyddio all-lein. + Hepgor y Newidiadau? + Os byddwch chi’n dewis hepgor, ni fydd y newidiadau’n cael eu cadw. + Hepgor + Cysoni Cynnwys All-lein? + Bydd hyn yn cysoni ~%s cynnwys. Gall hyn arwain at gostau ychwanegol gan eich darparwr data, os nad ydych chi wedi cysylltu â rhwydwaith Wi-Fi. + Bydd hyn yn cysoni ~%s cynnwys dim ond tra rydych chi wedi eich cysylltu i rwydwaith Wi-Fi. + Cysoni + Wrthi’n llwytho... + Arhoswch, rydym ni’n cael pethau’n barod i chi. + Ehangu cynnwys + Crebachu cynnwys Bydd galluogi Cysoni Cynnwys Awtomatig yn gofalu am lwytho’r cynnwys sydd wedi’i ddewis i lawr yn seiliedig ar y gosodiadau isod. Bydd cysoni cynnwys yn digwydd hyd yn oed os nad yw’r rhaglen yn rhedeg. Os yw’r gosodiadau wedi’i ddiffodd ni fydd cysoni’n digwydd. Ni fydd cynnwys sydd eisoes wedi’i lwytho i lawr yn cael ei ddileu. Amlder Cysoni Cysoni Cynnwys Awtomatig @@ -1452,7 +1467,6 @@ %d cwrs yn cysoni. %d cwrs yn cysoni. - Delweddau cynnwys cwrs Dydy’r aseiniad hwn ddim ar gael mwyach. Rydych chi all-lein Ar hyn o bryd, does gennych chi ddim cyrsiau sydd ar gael all-lein. @@ -1464,4 +1478,5 @@ %d cwrs wedi cael ei gysoni. %d cwrs wedi cael eu cysoni. + Cynnwys cwrs ychwanegol diff --git a/libs/pandares/src/main/res/values-da/strings.xml b/libs/pandares/src/main/res/values-da/strings.xml index 9823813992..c51f15be4f 100644 --- a/libs/pandares/src/main/res/values-da/strings.xml +++ b/libs/pandares/src/main/res/values-da/strings.xml @@ -1410,6 +1410,21 @@ Vælg alle Fravælg alle Der opstod en fejl under indlæsning af indholdet. + Ingen kurser + Dine fag vil blive vist her, og derefter kan du gøre dem tilgængelige til offline brug. + Intet fagindhold + Fagindholdet vil blive vist her, og derefter kan du gøre det tilgængeligt til offline brug. + Annuller ændringer? + Hvis du vælger at kassere, vil ændringerne ikke blive gemt. + Kasser + Vil du synkronisere offline indhold? + Dette vil synkronisere ~%s indhold. Det kan medføre yderligere gebyrer fra din dataudbyder, hvis du ikke er forbundet til et wi-fi-netværk. + Dette vil kun synkronisere ~%s indhold, mens du er tilsluttet et wi-fi-netværk. + Synkroniser + Indlæser ... + Vent venligst, mens vi gør tingene klar til dig. + Udvid indhold + Skjul indholdet Aktivering af automatisk indholdssynkronisering sørger for at downloade det valgte indhold baseret på nedenstående indstillinger. Indholdssynkroniseringen gennemføres, selvom applikationen ikke kører. Hvis indstillingen er deaktiveret, sker der ingen synkronisering. Det allerede downloadede indhold vil ikke blive slettet. Synkroniseringsfrekvens Automatisk indholdssynkronisering @@ -1452,7 +1467,6 @@ %d faget synkroniseres. %d fag synkroniseres. - Fagindhold billeder Denne opgave er ikke længere tilgængelig. Du er offline Du har i øjeblikket ingen fag, der er tilgængelige offline. @@ -1464,4 +1478,5 @@ %d faget er blevet synkroniseret. %d fagene er blevet synkroniseret. + Yderligere fagindhold diff --git a/libs/pandares/src/main/res/values-de/strings.xml b/libs/pandares/src/main/res/values-de/strings.xml index 820eb976ff..da151b2dc7 100644 --- a/libs/pandares/src/main/res/values-de/strings.xml +++ b/libs/pandares/src/main/res/values-de/strings.xml @@ -1410,6 +1410,21 @@ Alle auswählen Alle abwählen Beim Laden des Inhalts ist ein Fehler aufgetreten. + Keine Kurse + Ihre Kurse werden hier aufgelistet und können dann für die Offline-Nutzung verfügbar gemacht werden. + Keine Kursinhalte + Die Kursinhalte werden hier aufgelistet und können dann für die Offline-Nutzung verfügbar gemacht werden. + Änderungen verwerfen? + Wenn Sie die Änderungen verwerfen, werden sie nicht gespeichert. + Verwerfen + Offline-Inhalte synchronisieren + Damit werden ~%s Inhalte synchronisiert. Es können zusätzliche Gebühren von Ihrem Datenanbieter anfallen, wenn Sie nicht mit einem WLAN-Netzwerk verbunden sind. + Damit werden ~%s Inhalte synchronisiert, wenn Sie mit einem WLAN-Netzwerk verbunden sind. + Synchronisieren + Wird geladen ... + Bleiben Sie dran, wir bereiten alles für Sie vor. + Content erweitern + Content ausblenden Wenn Sie die automatische Inhaltssynchronisierung aktivieren, wird der Download der ausgewählten Inhalte auf der Grundlage der unten aufgeführten Einstellungen durchgeführt. Die Synchronisierung der Inhalte erfolgt auch dann, wenn die Anwendung nicht ausgeführt wird. Wenn die Einstellung ausgeschaltet ist, findet keine Synchronisierung statt. Die bereits heruntergeladenen Inhalte werden nicht gelöscht. Synchronisierungsfrequenz Automatische Synchronisation von Inhalten @@ -1452,7 +1467,6 @@ %d Kurs wird synchronisiert. %d Kurse werden synchronisiert. - Bilder zum Kursinhalt Diese Aufgabe ist nicht mehr verfügbar. Sie sind offline Sie haben derzeit keine offline verfügbaren Kurse. @@ -1464,4 +1478,5 @@ %d Kurs wurde synchronisiert. %d Kurse wurden synchronisiert. + Zusätzliche Kursinhalte diff --git a/libs/pandares/src/main/res/values-en-rAU/strings.xml b/libs/pandares/src/main/res/values-en-rAU/strings.xml index a94e856389..ce9cf16f72 100644 --- a/libs/pandares/src/main/res/values-en-rAU/strings.xml +++ b/libs/pandares/src/main/res/values-en-rAU/strings.xml @@ -1410,6 +1410,21 @@ Select All Deselect All An error occurred while loading the content. + No Courses + Your courses will be listed here, and then you can make them available for offline usage. + No Course Content + The course content will be listed here, and then you can make them available for offline usage. + Discard Changes? + If you choose to discard, the changes will not be saved. + Discard + Sync Offline Content? + This will sync ~%s content. It may result in additional charges from your data provider if you are not connected to a Wi-Fi network. + This will sync ~%s content only while you are connected to a Wi-Fi network. + Sync + Loading... + Hang tight, we\'re getting things ready for you. + Expand content + Collapse content Enabling the Auto Content Sync will take care of downloading the selected content based on the below settings. The content synchronisation will happen even if the application is not running. If the setting is switched off, then no synchronisation will happen. The already downloaded content will not be deleted. Sync Frequency Auto Content Sync @@ -1452,7 +1467,6 @@ %d course is syncing. %d courses are syncing. - Course content images This assignment is no longer available. You are offline You currently don\'t have any courses that are available offline. @@ -1464,4 +1478,5 @@ %d course has been synced. %d courses have been synced. + Additional course content diff --git a/libs/pandares/src/main/res/values-en-rCY/strings.xml b/libs/pandares/src/main/res/values-en-rCY/strings.xml index 9a7a75afe0..a86cf9a071 100644 --- a/libs/pandares/src/main/res/values-en-rCY/strings.xml +++ b/libs/pandares/src/main/res/values-en-rCY/strings.xml @@ -1410,6 +1410,21 @@ Select all Un-select All An error occurred while loading the content. + No Modules + Your modules will be listed here, and then you can make them available for offline usage. + No Module Content + The module content will be listed here, and then you can make them available for offline usage. + Discard Changes? + If you choose to discard, the changes will not be saved. + Discard + Sync Offline Content? + This will sync ~%s content. It may result in additional charges from your data provider, if you are not connected to a Wi-Fi network. + This will sync ~%s content only while you are connected to a Wi-Fi network. + Sync + Loading... + Hang tight, we\'re getting things ready for you. + Expand content + Collapse content Enabling the Auto Content Sync will take care of downloading the selected content based on the below settings. The content synchronization will happen even if the application is not running. If the setting is switched off, then no synchronization will happen. The already downloaded content will not be deleted. Sync Frequency Auto Content Sync @@ -1452,7 +1467,6 @@ %d module is syncing. %d modules are syncing. - Module content images This assignment is no longer available. You are offline You currently don\'t have any modules that are available offline. @@ -1464,4 +1478,5 @@ %d module has been synced. %d modules have been synced. + Additional module content diff --git a/libs/pandares/src/main/res/values-en-rGB/strings.xml b/libs/pandares/src/main/res/values-en-rGB/strings.xml index a838cfd2cd..f714fcd87a 100644 --- a/libs/pandares/src/main/res/values-en-rGB/strings.xml +++ b/libs/pandares/src/main/res/values-en-rGB/strings.xml @@ -1410,6 +1410,21 @@ Select all Un-select All An error occurred while loading the content. + No Courses + Your courses will be listed here, and then you can make them available for offline usage. + No Course Content + The course content will be listed here, and then you can make them available for offline usage. + Discard Changes? + If you choose to discard, the changes will not be saved. + Discard + Sync Offline Content? + This will sync ~%s content. It may result in additional charges from your data provider, if you are not connected to a Wi-Fi network. + This will sync ~%s content only while you are connected to a Wi-Fi network. + Sync + Loading... + Hang tight, we\'re getting things ready for you. + Expand content + Collapse content Enabling the Auto Content Sync will take care of downloading the selected content based on the below settings. The content synchronization will happen even if the application is not running. If the setting is switched off, then no synchronization will happen. The already downloaded content will not be deleted. Sync Frequency Auto Content Sync @@ -1452,7 +1467,6 @@ %d course is syncing. %d courses are syncing. - Course content images This assignment is no longer available. You are offline You currently don\'t have any courses that are available offline. @@ -1464,4 +1478,5 @@ %d course has been synced. %d courses have been synced. + Additional course content diff --git a/libs/pandares/src/main/res/values-es-rES/strings.xml b/libs/pandares/src/main/res/values-es-rES/strings.xml index 37d94a6525..a7ceba25e1 100644 --- a/libs/pandares/src/main/res/values-es-rES/strings.xml +++ b/libs/pandares/src/main/res/values-es-rES/strings.xml @@ -1412,6 +1412,21 @@ Seleccionar todo Deseleccionar todo Ha habido un error al cargar el contenido. + No hay cursos + Tus cursos aparecerán aquí y luego, podrás habilitarlos para utilizarlos sin conexión. + No hay contenido del curso + El contenido del curso aparecerá aquí y luego, podrás habilitarlo para utilizarlo sin conexión. + ¿Descartar los cambios? + Si optas por descartar, los cambios no se guardarán. + Descartar + ¿Sincronizar contenido sin conexión? + Esto sincronizará ~%s contenido. Si no estás conectado a una red wifi, es posible que tu proveedor de datos cobre cargos adicionales. + Esto sincronizará ~%s contenido, pero solo mientras estés conectado a una red wifi. + Sincronización + Cargando... + Ponte cómodo, estamos preparando todo para ti. + Expandir contenido + Colapsar contenido Con la acción de habilitar la sincronización automática de contenidos, se descargará el contenido seleccionado según los siguientes ajustes. La sincronización del contenido se realizará incluso aunque la aplicación no esté en funcionamiento. Si los ajustes están apagados, no se realizará la sincronización. El contenido ya descargado no podrá eliminarse. Frecuencia de sincronización Sincronización automática del contenido @@ -1454,7 +1469,6 @@ %d curso se está sincronizando. %d cursos se están sincronizando. - Contenido de las imágenes del curso Esta actividad ya no está disponible. No tienes conexión Actualmente no tienes ningún curso disponible sin conexión. @@ -1466,4 +1480,5 @@ %d curso se ha sincronizado. %d cursos se han sincronizado. + Contenido del curso adicional diff --git a/libs/pandares/src/main/res/values-es/strings.xml b/libs/pandares/src/main/res/values-es/strings.xml index 71d1cdce1e..380810ec03 100644 --- a/libs/pandares/src/main/res/values-es/strings.xml +++ b/libs/pandares/src/main/res/values-es/strings.xml @@ -1410,6 +1410,21 @@ Seleccionar todo Desmarcar todo Hubo un error al cargar el contenido. + Sin cursos + Sus cursos se mostrarán aquí y luego podrá ponerlos a disposición para su uso sin conexión. + Sin contenido del curso + El contenido del curso se mostrará aquí y luego podrá ponerlo a disposición para su uso sin conexión. + ¿Descartar los cambios? + Si elige descartarlos, no se guardarán los cambios. + Descartar + ¿Quiere sincronizar contenido sin conexión? + Esto sincronizará ~%s de contenido. Si no se conecta a una red Wi-Fi, es posible que su proveedor de datos le aplique cargos adicionales. + Esto sincronizará ~%s de contenido solo mientras tenga conexión a una red Wi-Fi. + Sincronización + Cargando... + Aguarde un momento. Estamos preparando todo para usted. + Expandir contenido + Contraer contenido Habilitar la sincronización automática de contenido se ocupará de descargar el contenido seleccionado en función de las configuraciones siguientes. La sincronización de contenido se realizará incluso si la aplicación no se está ejecutando. Si la configuración está desactivada, no se realizará ninguna sincronización. El contenido ya descargado no se eliminará. Frecuencia de sincronización Sincronización automática de contenido @@ -1452,7 +1467,6 @@ Se está sincronizando el curso %d. Se están sincronizando los cursos %d. - Imágenes del contenido del curso Esta tarea ya no está disponible. Está sin conexión Actualmente, no tiene ningún curso disponible sin conexión. @@ -1464,4 +1478,5 @@ Se ha sincronizado el curso %d. Se han sincronizado los cursos %d. + Contenido adicional del curso diff --git a/libs/pandares/src/main/res/values-fi/strings.xml b/libs/pandares/src/main/res/values-fi/strings.xml index 2e783d8563..b5cbe900ab 100644 --- a/libs/pandares/src/main/res/values-fi/strings.xml +++ b/libs/pandares/src/main/res/values-fi/strings.xml @@ -1410,6 +1410,21 @@ Valitse kaikki Poista kaikkien valinta Kurssin sisältöä ladattaessa ilmeni virhe. + Ei kursseja + Kurssisi näkyvät täällä, ja voit sitten asettaa sen saataville verkon ulkopuoliseen käyttöön. + Ei kurssisisältöä + Kurssisältö näkyy täällä, ja voit sitten asettaa sen saataville verkon ulkopuoliseen käyttöön. + Ohitetaanko muutokset? + Jos valitset ohittavasi, muutoksia ei tallenneta. + Ohita + Synkronoidaanko verkon ulkopuolinen sisältö + Tämä synkronisoi ~%s sisällön. Tästä saattaa aiheutua ylimääräisiä veloituksia datan toimittajilta, jos et ole yhteydessä Wi-Fi-verkkoon. + Tämä synkronisoi ~%s sisällön ainoastaan silloin, kn yhteys Wi-Fi -verkkoon on saatavilla. + Synkronointi + Ladataan... + Pysyttele täällä, valmistamme täällä kaikkea sinulle. + Laajenna sisältöä. + Kutista sisältöä. Automaattisen sisällön synkronisoinnin ottamisellal käyttöön huolehditaan valitun sisällön latauksesta alla olevien asetusten perusteella. Sisällön synkronisointi tapahtuu myös silloin, kun sovellus ei ole käynnissä jos asetukset on kytketty pois päältä, synkronisointia ei tapahdu. Jo ladattua sisältöä ei poisteta. Synkronoinnin tiheys Automaattinen sisällön synkronisointi @@ -1452,7 +1467,6 @@ %d kurssia synkronoidaan. %d kurssia synkronoidaan. - Kurssin sisällön kuvakkeet. Tämä tehtävä ei enää ole käytettävissä. Olet verkon ulkopuolella Sinulla ei parhaillaan ole kursseja, jotka ovat saatavilla verkon ulkopuolella. @@ -1464,4 +1478,5 @@ %d kurssi on synkronoitu. %d kurssia on synkronoitu. + LIsäkurssin sisältö. diff --git a/libs/pandares/src/main/res/values-fr-rCA/strings.xml b/libs/pandares/src/main/res/values-fr-rCA/strings.xml index 72c52b2f56..0de87d4836 100644 --- a/libs/pandares/src/main/res/values-fr-rCA/strings.xml +++ b/libs/pandares/src/main/res/values-fr-rCA/strings.xml @@ -1410,6 +1410,21 @@ Sélectionner tout Désélectionner tout Une erreur s’est produite lors du chargement du contenu. + Aucun cours + Vos cours seront répertoriés ici, puis vous pourrez les rendre disponibles pour une utilisation hors ligne. + Pas de contenu de cours + Le contenu du cours sera répertorié ici, puis vous pourrez le rendre disponible pour une utilisation hors ligne. + Abandonner les modifications? + Si vous choisissez d’annuler, les modifications ne seront pas enregistrées. + Abandonner + Synchronisation du contenu hors connexion? + Cela synchronisera le contenu de ~%s. Cela peut entraîner des frais supplémentaires de la part de votre fournisseur de données, si vous n’êtes pas connecté à un réseau Wi-Fi. + Cela synchronisera le contenu de ~%s uniquement lorsque vous êtes connecté à un réseau Wi-Fi. + Synchronisation + En cours de chargement... + Tenez bon, on prépare les choses pour vous. + Développer le contenu + Réduire le contenu L’activation de la synchronisation automatique du contenu se chargera de télécharger le contenu sélectionné en fonction des paramètres ci-dessous. La synchronisation du contenu se produit même si l’application n’est pas en cours d’exécution. Si le paramètre est désactivé, aucune synchronisation ne se produira. Le contenu déjà téléchargé ne sera pas supprimé. Fréquence de synchronisation Synchronisation automatique du contenu @@ -1452,7 +1467,6 @@ Synchronisation de %d cours. Synchronisation de %d cours. - Images du contenu du cours Cette tâche n’est plus disponible. Vous êtes hors ligne Vous n’avez actuellement aucun cours disponible hors ligne. @@ -1464,4 +1478,5 @@ %d cours a été synchronisé. %d cours ont été synchronisés. + Contenu supplémentaire de cours diff --git a/libs/pandares/src/main/res/values-fr/strings.xml b/libs/pandares/src/main/res/values-fr/strings.xml index b4f1328b77..21add8a7e4 100644 --- a/libs/pandares/src/main/res/values-fr/strings.xml +++ b/libs/pandares/src/main/res/values-fr/strings.xml @@ -1410,6 +1410,21 @@ Tout sélectionner Tout désélectionner Une erreur est survenue lors du chargement du contenu. + Aucun cours + Vos cours seront répertoriés ici et vous pourrez les rendre disponibles pour une utilisation hors ligne. + Aucun contenu de cours + Le contenu du cours sera répertorié ici et vous pourrez le rendre disponible pour une utilisation hors ligne. + Abandonner les modifications ? + Si vous choisissez Abandonner, les modifications ne seront pas enregistrées. + Abandonner + Synchroniser le contenu hors ligne ? + Le contenu ~%s sera synchronisé. Votre fournisseur de données peut vous facturer des frais supplémentaires si vous n’êtes pas connecté à un réseau Wi-Fi. + Le contenu ~%s ne sera synchronisé que lorsque vous serez connecté à un réseau Wi-Fi. + Synchroniser + Chargement en cours... + Un petit instant, nous sommes en train de tout préparer. + Développer le contenu + Réduire le contenu L\'activation de la synchronisation auto du contenu gérera automatiquement le téléchargement du contenu sélectionné en fonction des paramètres ci-dessous. La synchronisation du contenu aura lieu même si l’application n’est pas en cours d’exécution. Si le paramètre est désactivé, aucune synchronisation n’aura lieu. Le contenu déjà téléchargé ne sera pas supprimé. Fréquence de la synchronisation Synchronisation auto du contenu @@ -1452,7 +1467,6 @@ %d cours est en cours de synchronisation. %d cours sont en cours de synchronisation. - Images du contenu du cours Ce travail n’est plus disponible. Vous êtes hors ligne. Vous n’avez actuellement aucun cours disponible hors ligne. @@ -1464,4 +1478,5 @@ %d cours a été synchronisé. %d cours ont été synchronisés. + Contenu de cours supplémentaire diff --git a/libs/pandares/src/main/res/values-ht/strings.xml b/libs/pandares/src/main/res/values-ht/strings.xml index 5eda272465..02d547258d 100644 --- a/libs/pandares/src/main/res/values-ht/strings.xml +++ b/libs/pandares/src/main/res/values-ht/strings.xml @@ -1410,6 +1410,21 @@ Seleksyone Tout Deseleksyone Tout Yon erè fèt pandan chajman kontni an. + Pa gen Kou + Kou ou yo ap afiche isit la, apre sa ou ka mete yo disponib pou itilize san koneksyon. + Pa gen Kontni Kou + Kontni kou a ap afiche isit la, apre sa ou ka mete yo disponib pou itilize san koneksyon. + Rejte Chanjman? + si w chwazi rejte yo, chanjman yo pa p sovgade. + Abandone + Senkwonize Kontni San Koneksyon? + Sa ap senkwonize ~%s kontni. Li ka lakoz founisè sèvis entènèt ou a touche anplis si w pa konekte sou yon rezo wi-fi. + Sa ap senkwonize ~%s kontni sèlman lè w konekte sou yon rezo wi-fi. + Sync + Chajman... + Talè, n\ ap prepare bagay yo pou ou. + Elaji kontni + Ratresi kontni Aktivasyon senkwonizasyon Kontni Otomatik la ap gen pou telechaje kontni ki seleksyone a an fonksyon de paramèt ki anba yo. Senkwonizasyon kontni an ap fèt menm si aplikasyon an pa ekzekite. Si reglaj la dezaktive, pa gen senkwonizasyon k ap fèt. Kontni ki deja telechaje a pa p efase. Frekans Senkwonizasyon Senkwonizasyon Kontni Otomatik @@ -1452,7 +1467,6 @@ %d kou an senkwonizasyon %d kou yo ap senkwonize. - Imaj kontni kou Travay sa pa diponib ankò. Ou pa konekte Kounye a ou pa gen okenn kou ki disponib san koneksyon. @@ -1464,4 +1478,5 @@ %d kou a enkwonize. %d kou yo senkwonize. + Plis kontni kou diff --git a/libs/pandares/src/main/res/values-id/strings.xml b/libs/pandares/src/main/res/values-id/strings.xml index bbaf905458..bbe108efcb 100644 --- a/libs/pandares/src/main/res/values-id/strings.xml +++ b/libs/pandares/src/main/res/values-id/strings.xml @@ -432,6 +432,7 @@ Dihapus + Nilai diperbarui Memuat Konten Canvas… UnknownDevice @@ -1116,6 +1117,8 @@ %s %s %s %s, %s + Anda dapat membuka detail Penyerahan dari sini + Nilai: %s %s Menit %s Menit @@ -1299,10 +1302,18 @@ Tutup dialog kemajuan %1$s dari %2$s Mengunggah ke File + Satu atau lebih file gagal diunggah. Periksa sambungan internet Anda dan coba serahkan lagi. Mengunggah penyerahan ke \"%s\" Mengunggah Penyerahan - Mengunggah File + Penyerahan Berhasil + Penyerahan Gagal + + Mengunggah File + Mengunggah File + + Unggahan File Berhasil + Unggahan File Gagal Batalkan Penyerahan Ini akan membatalkan dan menghapus penyerahan Anda. Unggahan File Gagal @@ -1310,7 +1321,9 @@ Unggah ke File Saya Serahkan tugas Pilih kursus + Pilih kursus, kursus yang dipilih adalah: %s Pilih tugas + Pilih tugas, tugas yang dipilih adalah: %s %1$s, %2$s Batalkan Unggahan? Ini akan membatalkan unggahan Anda. @@ -1325,4 +1338,148 @@ Dibuat oleh Student View Kesalahan terjadi. Topik mungkin tidak lagi tersedia. Izin kamera ditolak secara permanen. Buka pengaturan app untuk mengizinkannya. + Tidak dapat menerbitkan tugas jika ada penyerahan siswa. + Berlangganan ke Umpan Kalender + Anda dapat menyinkronkan kalender Canvas ke akun Google Calendar Anda dengan mengeklik tombol Subscribe di dialog ini. Lalu, Anda harus pergi ke aplikasi Google Calendar di perangkat Anda dan mengaktifkan sinkronisasi di pengaturan untuk kalender baru. + Berlangganan + Alihkan ke Mode Terang + Alihkan ke Mode Gelap + Anda pengguna baru atau Kebijakan Penggunaan yang Dapat Diterima telah berubah sejak Anda terakhir kali menyetujuinya. Silakan setujui Kebijakan Penggunaan yang Dapat Diterima sebelum Anda melanjutkan. + Kebijakan Penggunaan yang Dapat Diterima + Kebijakan Penggunaan yang Dapat Diterima + Saya menyetujui Kebijakan Penggunaan yang Dapat Diterima. + Serahkan + Gagal menyerahkan penerimaan terhadap ketentuan. + Upaya %d + Pertanyaan: %d + Batas waktu: %s + Upaya yang diizinkan: %s + Upaya digunakan: %d + Terjadi kesalahan yang tidak terduga. + Hapus + Arsipkan + Keluarkan dari arsip + Tandai sudah dibaca + Tandai belum dibaca + Bintang + Hapus Bintang + Gagal melakukan operasi + %s dihapus + %s diarsipkan + %s dikeluarkan dari arsip + %s ditandai sudah dibaca + %s ditandai belum dibaca + %s dibintangi + %s bintang dihapus + + Percakapan ini akan dihapus dari Kotak Masuk di semua perangkat Anda. Tindakan ini tidak bisa diurungkan. + Percakapan ini akan dihapus dari Kotak Masuk di semua perangkat Anda. Tindakan ini tidak bisa diurungkan. + + Gagal memuat halaman selanjutnya. Periksa sambungan internet Anda. + Gagal memuat ulang percakapan. Periksa sambungan internet Anda. + Kotak Masuk + Pilih Kursus atau Grup + Kosongkan + Filter Kursus: %s + pilih + Percakapan dipilih. Mode seleksi diaktifkan. Navigasikan untuk tindakan. + Percakapan dipilih + Percakapan batal dipilih + Keluar mode seleksi + Mode seleksi dinonaktifkan. + Avatar dari %s + Urungkan + App + Domain + ID Login + Email + Versi + Ada masalah saat memuat ulang tugas ini. Silakan periksa sambungan internet Anda dan coba lagi. + Logo Instructure + Preferensi + Konten Offline + Sinkronisasi + + Konten Offline + Kelola Konten Offline + Penyimpanan + %s dari %s Digunakan + Aplikasi Lain + Canvas Student + Tersisa + Semua Kursus + Sinkronkan + %d Dipilih + Pilih Semua + Hapus pilihan semua + Kesalahan terjadi saat memuat konten. + Tidak Ada Kursus + Kursus Anda akan tertera di sini, lalu Anda dapat menyediakannya untuk penggunaan offline. + Tidak Ada Konten Kursus + Konten kursus akan tertera di sini, lalu Anda dapat menyediakannya untuk penggunaan offline. + Buang Perubahan? + Jika Anda memutuskan untuk membuang, perubahan tidak akan disimpan. + Buang + Sinkronisasikan Konten Offline? + Ini akan menyinkronkan konten ~%s. Ini dapat menghasilkan biaya tambahan dari operator seluler Anda jika Anda tidak tersambung ke jaringan Wi-Fi. + Ini hanya akan menyinkronkan konten ~%s ketika Anda tersambung ke jaringan Wi-Fi. + Sinkronkan + Memuat... + Jangan ke mana-mana, kami sedang menyiapkannya untuk Anda. + Buka konten + Tutup konten + Mengaktifkan Sinkronisasi Konten Otomatis akan menangani pengunduhan dari konten yang dipilih berdasarkan pengaturan di bawah. Sinkronisasi konten akan terjadi walaupun aplikasi tidak berjalan. Jika pengaturan dimatikan, sinkronisasi tidak akan terjadi. Konten yang sudah diunduh tidak akan dihapus. + Frekuensi Sinkronisasi + Sinkronisasi Konten Otomatis + Spesifikasikan kemunculan ulang sinkronisasi konten. Sistem akan mengunduh konten yang dipilih berdasarkan frekuensi yang ditetapkan di sini. + Hanya Sinkronisasikan Konten Melalui Wi-Fi + Jika pengaturan ini diaktifkan, sinkronisasi konten hanya akan terjadi jika perangkat tersambung ke jaringan Wi-Fi; jika tidak, akan ditunda hingga jaringan Wi-Fi tersedia. + Sinkronisasi + Setiap Hari + Mingguan + Frekuensi Sinkronisasi + Matikan Sinkronisasikan Konten Hanya Melalui Wi-Fi? + Jika pengaturan ini diaktifkan, sinkronisasi konten hanya akan terjadi jika perangkat tersambung ke jaringan Wi-Fi; jika tidak, akan ditunda hingga jaringan Wi-Fi tersedia. + Matikan + Manual + Mode Offline + Tidak Tersedia Offline + Konten ini tidak tersedia dalam mode offline. + Konten ini tidak tersedia dalam mode offline. Jika Anda ingin mengubah pengaturan, buka layar Konten Offline dari dashboard ketika jaringan tersedia. + Offline + Sinkronisasi Gagal + Mengunduh %1$s dari %2$s + Diantrekan + Sinkronisasi Konten Offline Selesai + Sinkronisasi Konten Offline Gagal + Batalkan Sinkronisasi? + Akan menghentikan sinkronisasi konten offline. Anda dapat melakukannya lagi nanti. + Satu atau lebih file gagal disinkronkan. Periksa sambungan internet Anda dan coba serahkan lagi. + Unduhan dimulai + Kursus tidak dapat ditambahkan ke offline favorit. + Semua Kursus + Kursus + Grup + Semua Kursus + Memilih kursus untuk Dashboard hanya dapat dilakukan offline. Anda dapat menavigasikan ke detail kursus offline. + Catatan + Sukses! Mengunduh %1$s dari %2$s + Menyinkronisasikan Konten Offline? + Abaikan notifikasi + + Kursus %d disinkronkan. + Kursus %d disinkronkan. + + Tugas tidak lagi tersedia. + Anda offline + Anda saat ini tidak memiliki kursus apa pun yang tersedia offline. + Sinkronisasi Konten Offline Berhasil + Sinkronisasi Konten Offline Gagal + Pembaruan Sinkronisasi Offline + Notifikasi Canvas untuk pembaruan sinkronisasi offline. + + Kursus %d telah disinkronkan. + Kursus %d telah disinkronkan. + + Konten kursus tambahan diff --git a/libs/pandares/src/main/res/values-is/strings.xml b/libs/pandares/src/main/res/values-is/strings.xml index 05e63a199b..19cda25d85 100644 --- a/libs/pandares/src/main/res/values-is/strings.xml +++ b/libs/pandares/src/main/res/values-is/strings.xml @@ -1410,6 +1410,21 @@ Velja allt Afvelja allt Villa kom fram við að hlaða innihaldið. + Engin námskeið + Námskeiðin þín verða skráð hér og síðan geturðu gert þau aðgengileg til notkunar án nettengingar. + Ekkert námskeiðsefni + Efni námskeiðsins verður skráð hér og síðan geturðu gert það aðgengilegt til notkunar án nettengingar. + Henda breytingum? + Breytingarnar verða ekki vistaðar ef þú velur að henda. + Henda + Samhæfa efni án nettengingar? + Þetta mun samhæfa ~%s efni. Það getur leitt til viðbótargjalda frá gagnaveitunni þinni ef þú ert ekki tengd(ur) við Wi-Fi net. + Þetta mun eingöngu samhæfa ~%s efni á meðan þú ert tengd(ur) við Wi-Fi net. + Samhæfa + Hleður... + Bíddu við, við erum að gera hlutina tilbúna fyrir þig. + Víkka efni + Fella saman efni Með því að virkja sjálfvirka samstillingu efnis mun hún sjá um að hlaða niður völdu efni byggt á stillingunum hér að neðan. Samstilling efnis mun gerast jafnvel þótt forritið sé ekki í gangi. Ef slökkt er á stillingunni mun engin samstilling eiga sér stað. Efni sem þegar hefur verið hlaðið niður verður ekki eytt. Samstillingartíðni Sjálfvirk samstilling efnis @@ -1452,7 +1467,6 @@ %d námskeið er að samstillast. %d námskeið eru að samstillast. - Myndir af innihaldi námskeiðs Þetta verkefni er ekki lengur tiltækt. Þú ert án nettengingar Þú ert ekki með nein námskeið sem eru í boði án nettengingar. @@ -1464,4 +1478,5 @@ %d námskeið hefur verið samstillt. %d námskeið hafa verið samstillt. + Viðbótar námskeiðsefni diff --git a/libs/pandares/src/main/res/values-it/strings.xml b/libs/pandares/src/main/res/values-it/strings.xml index 1ddabbeb86..b01288d3eb 100644 --- a/libs/pandares/src/main/res/values-it/strings.xml +++ b/libs/pandares/src/main/res/values-it/strings.xml @@ -1410,6 +1410,21 @@ Seleziona tutto Deseleziona tutto Si è verificato un errore durante il caricamento dei contenuti. + Nessun corso + I tuoi corsi saranno elencati qui, quindi è possibile renderli disponibili per l’uso offline. + Nessun contenuto del corso + Il contenuto del corso sarà elencato qui, quindi è possibile renderlo disponibile per l’uso offline. + Annullare le modifiche? + Se scegli di eliminarle, le modifiche non saranno salvate. + Rimuovi + Sincronizzare contenuto offline? + Sarà sincronizzato il contenuto ~%s. Ciò può comportare l’addebito di costi aggiuntivi da parte del tuo fornitore di dati, nel caso in cui tu non sia collegato a una rete Wi-Fi. + Sarà sincronizzato il contenuto ~%s solo quando sei collegato ad una rete Wi-Fi. + Sincronizza + Caricamento in corso... + Un attimo di pazienza, stiamo preparando dei contenuti per te. + Espandi contenuti + Comprimi contenuti L’attivazione della Sincronizzazione contenuto automatica si occuperà del download del contenuto selezionato in base alle impostazioni riportate di seguito. La sincronizzazione del contenuto verrà effettuata anche se l’applicazione non è in funzione. Se l’impostazione è disattiva, non sarà effettuata alcuna sincronizzazione. Il contenuto già scaricato non sarà eliminato. Frequenza di sincronizzazione Sincronizzazione contenuto automatica @@ -1452,7 +1467,6 @@ %d corso in sincronizzazione. %d corsi in sincronizzazione. - Immagini contenuto corso Questo compito non è più disponibile. Sei offline Al momento non hai nessun corso disponibile offline. @@ -1464,4 +1478,5 @@ %d corso è stato sincronizzato. %d corsi sono stati sincronizzati. + Altro contenuto del corso diff --git a/libs/pandares/src/main/res/values-ja/strings.xml b/libs/pandares/src/main/res/values-ja/strings.xml index a64094ca34..2bd6db565b 100644 --- a/libs/pandares/src/main/res/values-ja/strings.xml +++ b/libs/pandares/src/main/res/values-ja/strings.xml @@ -1392,6 +1392,21 @@ すべて選択 すべての選択を取り消し コンテンツ読み込み中にエラーが起こりました。 + コースはありません + コースがここに表示され、オフラインで利用できるようにすることができます。 + コースコンテンツなし + コースコンテンツはここに表示され、オフラインで利用できるようになります。 + 変更を破棄しますか? + 破棄を選択した場合、変更は保存されません。 + 破棄する + オフラインのコンテンツを同期化する + これで、~%sのコンテンツが同期されます。Wi-Fiネットワークに接続していない、場合、データプロバイダから追加料金が請求される可能性があります。 + これで、Wi-Fiネットワークに接続している間のみ、~%sのコンテンツが同期されます + 同期化 + 読み込み中・・・ + 少々お待ちください。今、準備をしています。 + コンテンツを拡大 + コンテンツを折りたたむ コンテンツの自動同期を有効にすると、選択したコンテンツのダウンロードが以下の設定に基づいて行われます。コンテンツの同期は、アプリケーションが起動していなくても行われます。この設定をオフにすると、同期は行われません。すでにダウンロードされているコンテンツは削除されません。 同期の頻度 コンテンツ自動同期 @@ -1433,7 +1448,6 @@ %dコースは同期中です。 - コースコンテンツ画像 この機能はもう利用できません。 現在オフラインです 現在、オフラインで利用可能なコースはありません。 @@ -1444,4 +1458,5 @@ %dコースが同期されました。 + 追加のコースコンテンツ diff --git a/libs/pandares/src/main/res/values-mi/strings.xml b/libs/pandares/src/main/res/values-mi/strings.xml index 0dbbc8f33e..0121ebff4d 100644 --- a/libs/pandares/src/main/res/values-mi/strings.xml +++ b/libs/pandares/src/main/res/values-mi/strings.xml @@ -1410,6 +1410,21 @@ Tīpako katoa Whakakorehia te katoa I puta he hapa i te wa e uta ana te ihirangi. + Kāore he Akoranga + Ka whakarārangihia ō akoranga ki reira ka taea e koe te whakawātea mō te whakamahi tuimotu. + Karekau he ihirangi akoranga + Kua whakarārangitia te ihirangi akoranga ki konei katahi ka taea e koe te waatea mo te whakamahi tuimotu. + Tūraki ngā Huringa? + Mena ka whiriwhiri koe ki te whakakore, ka kore nga huringa e tiakina. + Tūraki + Tukutahi ihirangi tuimotu? + Tenei ka tukutahi ~%s ihirangi. Ka hua pea he utu taapiri mai i to kaiwhakarato raraunga, mena kaore koe e hono ki te whatunga Wi-Fi. + Ma tenei ka tukutahi ~%s ihirangi anake i te wa e hono ana koe ki te whatunga Wi-Fi. + Tukutahi + E uta ana .... + Kia mau, tatou kei te whakarite mea mo koe. + Roha ihirangi + Tiango ihirangi Ina whakahohea te waahanga Tukutahi Ihirangi Aunoa, ko nga ihirangi kua tikiakehia ka whakawhirinaki ki nga tautuhinga kua whakarārangihia i raro nei. Ahakoa kaore i tuwhera te tono, ka mau tonu te tukutahitanga ihirangi. Karekau he tukutahitanga mena kua weto te tautuhinga. Ko nga mea kua oti te tango ake ka kore e tangohia. Auautanga Tukutahi Tukutahi Ihirangi Aunoa @@ -1452,7 +1467,6 @@ %d Kei te tukutahi te akoranga. %d kei te tukutahi nga akoranga. - Whakaahua ihirangi akoranga Kaore tēnei whakataunga i te wātea. Kei te tuimotu koe I tenei wa karekau he akoranga kei te waatea tuimotu. @@ -1464,4 +1478,5 @@ %d kua tukutahia te akoranga. %d kua tukutahia nga akoranga. + Ihirangi akoranga taapiri diff --git a/libs/pandares/src/main/res/values-ms/strings.xml b/libs/pandares/src/main/res/values-ms/strings.xml index 7848518aa7..85e960c5fd 100644 --- a/libs/pandares/src/main/res/values-ms/strings.xml +++ b/libs/pandares/src/main/res/values-ms/strings.xml @@ -1416,6 +1416,21 @@ Pilih Semua Nyahpilih Semua Ralat berlaku semasa memuatkan kandungan. + Tiada Kursus + kursus anda akan disenaraikan di sini, kemudian anda boleh menyediakannya untuk kegunaan luar talian. + Tiada Kandungan Kursus + Kandungan kursus akan disenaraikan di sini, kemudian anda boleh menyediakannya untuk kegunaan luar talian. + Buang Perubahan? + Jika anda memilih untuk membuangnya, perubahan tidak akan disimpan. + Buang + Segerakkan Kandungan Luar Talian? + Tindakan ini akan menyegerakkan ~%s kandungan. Hal ini mungkin akan menyebabkan anda dikenakan caj tambahan oleh penyedia data anda sekiranya anda tidak bersambung ke rangkaian Wi-Fi. + Tindakan ini akan menyegerakkan ~%s kandungan hanya ketika anda bersambung ke rangkaian Wi-Fi. + Segerakkan + Memuatkan... + Sila tunggu, kami sedang menyediakan beberapa perkara untuk anda. + Kembangkan kandungan + Kuncupkan kandungan Mendayakan Segerakan Kandungan Automatik akan mengurus muat turun kandungan yang dipilih berdasarkan tetapan di bawah. Penyegerakan kandungan akan etap berlaku walaupun aplikasi tidak berjalan. Jika tetapan dimatikan maka tiada penyegerakan akan berlaku. Kandungan yang sudah dimuat turun tidak akan dipadamkan. Kekerapan Penyegerakan Segerakan Kandungan Automatik @@ -1458,7 +1473,6 @@ %d Kursus sedang disegerakkan. %d Kursus sedang disegerakkan. - Imej kandungan kursus Tugasan ini tidak lagi tersedia. Anda berada di luar talian Anda tidak mempunyai apa-apa kursus yang tersedia di luar talian buat masa ini. @@ -1470,4 +1484,5 @@ %d Kursus telah disegerakkan. %d Kursus telah disegerakkan. + Kandungan kursus tambahan diff --git a/libs/pandares/src/main/res/values-nb/strings.xml b/libs/pandares/src/main/res/values-nb/strings.xml index 81e7b55592..436fbd2300 100644 --- a/libs/pandares/src/main/res/values-nb/strings.xml +++ b/libs/pandares/src/main/res/values-nb/strings.xml @@ -1411,6 +1411,21 @@ Velg alle Fjern all merking Det oppsto en feil ved lasting av innholdet. + Ingen emner + Emnene dine vil vises her, og du kan deretter gjøre det tilgjengelig for frakoblet bruk. + Intet emneinnhold + Emneinnholdet vises her, og du kan deretter gjøre det tilgjengelig for frakoblet bruk. + Forkaste endringer? + Hvis du velger å forkaste, blir ikke endringene lagret. + Forkast + Synkronisere frakoblet innhold? + Dette vil synkronisere ~%s-innhold. Dette kan føre til ekstra kostnader fra din datatilbyder hvis du ikke er koblet til et Wi-Fi-nettverk. + Dette vil synkronisere ~%s-innhold bare når du er tilkoblet et Wi-Fi-nettverk. + Synkroniser + Laster… + Vent litt, vi gjør alt klart for deg. + Utvid innhold + Skjul innhold Aktivering av Automatisk synkronisering av innhold vil ta seg av nedlasting av det valgte innholdet basert på følgende innstillinger. Synkronisering av innhold vil skje selv om applikasjonen ikke kjører. Hvis innstillingen er slått av, vil det ikke skje noen synkronisering. Det allerede nedlastede innholdet vil ikke bli slettet. Synkroniseringsfrekvens Automatisk synkronisering av innhold @@ -1453,7 +1468,6 @@ %d emne synkroniseres. %d emner synkroniseres. - Emneinnhold-bilder Denne oppgaven er ikke lenger tilgjengelig. Du er frakoblet Du har ingen emner som er tilgjengelig i frakoblet modus. @@ -1465,4 +1479,5 @@ %d emne er synkronisert. %d emner er synkronisert. + Ytterligere emneinnhold diff --git a/libs/pandares/src/main/res/values-nl/strings.xml b/libs/pandares/src/main/res/values-nl/strings.xml index 182e1a3bac..ac1db111a8 100644 --- a/libs/pandares/src/main/res/values-nl/strings.xml +++ b/libs/pandares/src/main/res/values-nl/strings.xml @@ -1410,6 +1410,21 @@ Alles selecteren Selectie van alle items opheffen Er is een fout opgetreden tijdens het laden van de content. + Geen cursussen + Je cursussen worden hier weergegeven, waarvan je de inhoud beschikbaar kunt maken voor offline gebruik. + Geen cursusinhoud + De cursusinhoud wordt hier weergegeven, waar je deze beschikbaar kunt maken voor offline gebruik. + Wijzigingen negeren? + Als je kiest voor verwijderen, worden de wijzigingen niet opgeslagen. + Verwijderen + Offline inhoud synchroniseren? + Hiermee wordt ~%s inhoud gesynchroniseerd. Dat kan leiden tot extra kosten van je dataprovider als je niet verbonden bent met een wifi-netwerk. + Hiermee wordt ~%s inhoud alleen gesynchroniseerd als je verbonden bent met een wifi-netwerk. + Synchroniseren + Bezig met laden... + Een ogenblikje, we zetten het voor je klaar. + Inhoud uitvouwen + Inhoud samenvouwen Door Automatisch synchroniseren van content in te schakelen wordt de geselecteerde content gedownload op basis van de onderstaande instellingen. De contentsynchronisatie vindt ook plaats als de applicatie niet wordt uitgevoerd. Als de instelling wordt uitgeschakeld, wordt er geen synchronisatie uitgevoerd. De reeds gedownloade content wordt niet verwijderd. Synchronisatiefrequentie Automatisch synchroniseren van content @@ -1452,7 +1467,6 @@ %d-cursus wordt gesynchroniseerd. %d-cursussen worden gesynchroniseerd. - Afbeeldingen van cursusinhoud Deze opdracht is niet meer beschikbaar. Je bent offline Je hebt momenteel geen cursussen die offline beschikbaar zijn. @@ -1464,4 +1478,5 @@ %d-cursus is gesynchroniseerd. %d-cursussen zijn gesynchroniseerd. + Aanvullende cursusinhoud diff --git a/libs/pandares/src/main/res/values-pl/strings.xml b/libs/pandares/src/main/res/values-pl/strings.xml index 2a0f55ce2f..78c4a5ce2a 100644 --- a/libs/pandares/src/main/res/values-pl/strings.xml +++ b/libs/pandares/src/main/res/values-pl/strings.xml @@ -1446,6 +1446,21 @@ Zaznacz wszystko Usuń zaznaczenie wszystkich Podczas wczytywania zawartości wystąpił błąd. + Brak kursów + Kursy zostaną wyszczególnione tutaj, następnie można je będzie udostępnić do użytku offline. + Brak zawartości kursu + Zawartość kursu zostanie podana tutaj, następnie można ją będzie udostępnić do użytku offline. + Odrzucić zmiany? + W przypadku odrzucenia, zmiany nie zostaną zapisane. + Odrzuć + Zsynchronizować zawartość offline? + Spowoduje to synchronizację zawartości ~%s. Może to skutkować pobraniem dodatkowych opłat przez usługodawcę, jeśli nie nawiązano połączenia z siecią Wi-Fi. + Spowoduje to synchronizację zawartości ~%s, jeśli nawiązano połączenie z siecią Wi-Fi. + Synchronizacja + Wczytywanie... + Chwileczkę, wszystko będzie niedługo gotowe. + Rozwiń zawartość + Zwiń zawartość Włączenie automatycznej synchronizacji zawartości pozwoli pobierać wybraną zawartość w oparciu o poniższe ustawienia. Synchronizacja zawartości będzie się odbywać, nawet jeśli aplikacja nie zostanie włączona. Jeśli funkcja jest wyłączona, synchronizacja nie będzie działać. Pobrana już zawartość nie zostanie usunięta. Częstotliwość synchronizacji Automatyczna synchronizacja zawartości @@ -1490,7 +1505,6 @@ Trwa synchronizacja kursów %d. Trwa synchronizacja kursów %d. - Obrazy zawartości kursu To zadanie nie jest już dostępne. Jesteś offline Obecnie nie masz żadnych kursów dostępnych offline. @@ -1504,4 +1518,5 @@ Kursy %d zostały zsynchronizowane. Kursy %d zostały zsynchronizowane. + Dodatkowa zawartość kursu diff --git a/libs/pandares/src/main/res/values-pt-rBR/strings.xml b/libs/pandares/src/main/res/values-pt-rBR/strings.xml index 743fe896ee..31760bd583 100644 --- a/libs/pandares/src/main/res/values-pt-rBR/strings.xml +++ b/libs/pandares/src/main/res/values-pt-rBR/strings.xml @@ -1410,6 +1410,21 @@ Selecionar tudo Cancelar seleção de todos Ocorreu um erro ao carregar o conteúdo. + Sem Cursos + Seus cursos serão listados aqui e você poderá disponibilizá-los para uso offline. + Nenhum conteúdo do curso + O conteúdo do curso será listado aqui e você poderá disponibilizá-lo para uso offline. + Descartar alterações? + Se você optar por descartar, as alterações não serão salvas. + Descartar + Sincronizar conteúdo off-line? + Isso sincronizará o conteúdo ~%s. Isso pode resultar em cobranças adicionais do seu provedor de dados, se você não estiver conectado a uma rede Wi-Fi. + Isso sincronizará o conteúdo ~%s apenas enquanto você estiver conectado a uma rede Wi-Fi. + Sincronizar + Carregando... + Aguarde, estamos preparando tudo para você. + Expandir conteúdo + Recolher conteúdo A ativação da sincronização automática de conteúdo cuidará do download do conteúdo selecionado com base nas configurações abaixo. A sincronização de conteúdo acontecerá mesmo se o aplicativo não estiver em execução. Se a configuração estiver desativada, nenhuma sincronização ocorrerá. O conteúdo já baixado não será excluído. Frequência de sincronização Sincronização automática de conteúdo @@ -1452,7 +1467,6 @@ %d curso está sendo sincronizado. %d cursos estão sendo sincronizados. - Imagens do conteúdo do curso Essa tarefa não está mais disponível. Você está off-line No momento, você não tem nenhum curso disponível off-line. @@ -1464,4 +1478,5 @@ %d curso foi sincronizado. %d cursos foram sincronizados. + Conteúdo adicional do curso diff --git a/libs/pandares/src/main/res/values-pt-rPT/strings.xml b/libs/pandares/src/main/res/values-pt-rPT/strings.xml index 0e9fe3c275..4a06a3eafc 100644 --- a/libs/pandares/src/main/res/values-pt-rPT/strings.xml +++ b/libs/pandares/src/main/res/values-pt-rPT/strings.xml @@ -1410,6 +1410,21 @@ Selecionar tudo Desmarcar todos Ocorreu um erro ao carregar o conteúdo. + Sem Disciplinas + As suas disciplinas serão listadas aqui e, em seguida, pode disponibilizá-las para utilização offline. + Sem conteúdo da disciplina + Os conteúdos da disciplina serão listados aqui e, em seguida, pode disponibilizá-los para utilização offline. + Ignorar mudanças? + Se optar por rejeitar, as alterações não serão guardadas. + Ignorar + Sincronizar conteúdo offline? + Isto irá sincronizar o conteúdo ~%s. Pode resultar em custos adicionais do seu fornecedor de dados, se não estiver ligado a uma rede Wi-Fi. + Esta opção sincronizará o conteúdo ~%s apenas quando estiver ligado a uma rede Wi-Fi. + Sincronizar + A carregar... + Aguente firme, estamos a preparar tudo para si. + Expandir conteúdo + Recolher conteúdo A ativação da Sincronização automática de conteúdos encarregar-se-á de descarregar o conteúdo selecionado com base nas definições abaixo. A sincronização de conteúdos ocorrerá mesmo que a aplicação não esteja a ser executada. Se a definição estiver desativada, não será efetuada qualquer sincronização. O conteúdo já transferido não será eliminado. Frequência de sincronização Sincronização automática de conteúdos @@ -1452,7 +1467,6 @@ %d a disciplina está a sincronizar-se. %d as disciplinas estão a sincronizar-se. - Imagens do conteúdo da disciplina Esta tarefa não está mais disponível. Está offline Atualmente, não tem quaisquer disciplinas disponíveis offline. @@ -1464,4 +1478,5 @@ %d a disciplina foi sincronizada. %d as disciplinas foram sincronizadas. + Conteúdo adicional da disciplina diff --git a/libs/pandares/src/main/res/values-ru/strings.xml b/libs/pandares/src/main/res/values-ru/strings.xml index 5130909e65..b4d4c66796 100644 --- a/libs/pandares/src/main/res/values-ru/strings.xml +++ b/libs/pandares/src/main/res/values-ru/strings.xml @@ -1446,6 +1446,21 @@ Выбрать все Отменить выбор для всех Произошла ошибка при загрузке контента. + Курсы отсутствуют + Ваши курсы будут перечислены здесь, а затем вы сможете сделать их доступными для использования в режиме офлайн. + Нет контента курса + Содержание курсов будет перечислено здесь, а затем вы сможете сделать их доступными для использования в режиме офлайн. + Отменить изменения? + Если выбрать отмену, изменения не будут сохранены. + Удалить + Синхронизировать офлайн-контент? + Будет выполнена синхронизация содержимого ~%s. Это может привести к дополнительным расходам со стороны поставщика данных, если вы не подключены к сети Wi-Fi. + При этом синхронизация контента ~%s будет осуществляться только при подключении к сети Wi-Fi. + Синхронизировать + Выполняется загрузка... + Спокойно, мы готовим все для вас. + Развернуть содержание + Свернуть содержание При включении функции автоматической синхронизации содержимого будет выполнена загрузка выбранного содержимого на основе приведенных ниже настроек. Синхронизация содержимого будет происходить, даже если приложение не запущено. Если этот параметр выключен, синхронизация не будет выполняться. Уже загруженное содержимое не будет удаляться. Синхронизация частоты Автоматическая синхронизация контента @@ -1490,7 +1505,6 @@ %d курса(-ов) синхронизируются. %d курса(-ов) синхронизируются. - Изображения содержимого курса Это задание более недоступно. Вы находитесь в автономном режиме В настоящее время у вас нет никаких курсов, доступных в автономном режиме. @@ -1504,4 +1518,5 @@ %d курса(-ов) были синхронизированы. %d курса(-ов) были синхронизированы. + Дополнительный контент курса diff --git a/libs/pandares/src/main/res/values-sl/strings.xml b/libs/pandares/src/main/res/values-sl/strings.xml index c5e6bbde77..8bacdc881f 100644 --- a/libs/pandares/src/main/res/values-sl/strings.xml +++ b/libs/pandares/src/main/res/values-sl/strings.xml @@ -1410,6 +1410,21 @@ Izberi vse Razveljavi izbor vseh Pri nalaganju vsebine je prišlo do napake. + Ni predmetov + Vaši predmeti bodo navedeni tukaj, nato pa lahko zanje omogočite uporabo brez povezave. + Ni vsebine predmeta + Vsebina predmeta bo navedena tukaj, nato pa lahko zanjo omogočite uporabo brez povezave. + Želite zavreči spremembe? + Če jih boste zavrgli, spremembe ne bodo shranjene. + Zavrzi + Želite sinhronizirati vsebino brez povezave? + S tem boste sinhronizirali vsebino ~%s Ponudnik podatkov vam lahko zaračuna dodatne stroške, če niste povezani v omrežje Wi-Fi. + S tem boste sinhronizirali vsebino ~%s le, ko ste povezani v omrežje Wi-Fi. + Sinhronizacija + Nalaganje ... + Počakajte, za vas pripravljamo stvari. + Razširi vsebino + Strni vsebino Če omogočite samodejno sinhronizacijo vsebine, boste omogočili prenos izbrane vsebine na podlagi spodnjih nastavitev. Sinhronizacija vsebine bo izvedena tudi, če aplikacija ni zagnana. Če je nastavitev izklopljena, sinhronizacija ne bo izvedena. Že prenesena vsebina ne bo odstranjena. Pogostost sinhronizacije Samodejna sinhronizacija vsebine @@ -1452,7 +1467,6 @@ %d predmet se sinhronizira. %d predmetov se sinhronizira. - Slike vsebine predmeta Ta naloga ni več na voljo. Nimate povezave Trenutno nimate nobenega predmeta, ki bi bil na voljo brez povezave. @@ -1464,4 +1478,5 @@ %d predmet je bil sinhroniziran. %d predmetov je bilo sinhroniziranih. + Dodatna vsebina predmeta diff --git a/libs/pandares/src/main/res/values-sv/strings.xml b/libs/pandares/src/main/res/values-sv/strings.xml index ae0a2f612e..cff370c4bb 100644 --- a/libs/pandares/src/main/res/values-sv/strings.xml +++ b/libs/pandares/src/main/res/values-sv/strings.xml @@ -1410,6 +1410,21 @@ Välj alla Avmarkera alla Ett fel uppstod vid inläsning av innehållet. + Inga kurser + Dina kurser kommer att listas här, och sedan kan du göra dem tillgängliga för offlineanvändning. + Inget kursinnehåll + Kursinnehållet kommer att listas här, och sedan kan du göra dem tillgängliga för offlineanvändning. + Ignorera ändringar? + Om du väljer att avvisa kommer ändringarna inte att sparas. + Avbryt + Synkronisera offlineinnehåll? + Detta synkroniserar ~%s-innehåll. Detta kan medföra ytterligare kostnader från din dataleverantör om du inte är ansluten till ett wifi-nätverk. + Detta synkroniserar ~%s-innehåll endast när du är ansluten till ett wifi-nätverk. + Synkronisera + Läser in ... + Vänta, vi förbereder åt dig. + Visa innehållet + Dölj innehållet Om automatisk innehållssynkronisering aktiveras laddas det valda innehållet ned baserat på nedanstående inställningar. Innehållssynkroniseringen sker även om programmet inte körs. Om inställningen är avstängd sker ingen synkronisering. Innehåll som redan laddats ned raderas inte. Synkroniseringsfrekvens Automatisk innehållssynkronisering @@ -1452,7 +1467,6 @@ %d-kurs synkroniserar. %d-kurser synkroniserar. - Bilder i kursinnehållet Denna uppgift är inte längre tillgänglig. Du är offline Du har för närvarande inte några kurser som är tillgängliga offline. @@ -1464,4 +1478,5 @@ %d-kurs har synkroniserats. %d-kurser har synkroniserats. + Extra kursinnehåll diff --git a/libs/pandares/src/main/res/values-th/strings.xml b/libs/pandares/src/main/res/values-th/strings.xml index 6023ee0943..3696e6d1af 100644 --- a/libs/pandares/src/main/res/values-th/strings.xml +++ b/libs/pandares/src/main/res/values-th/strings.xml @@ -1410,6 +1410,21 @@ เลือกทั้งหมด ยกเลิกการเลือกทั้งหมด เกิดข้อผิดพลาดขณะโหลดเนื้อหา + ไม่มีบทเรียน + บทเรียนของคุณจะปรากฏขึ้นที่นี่ จากนั้นคุณสามารถเผยแพร่สำหรับใช้งานแบบออฟไลน์ + ไม่มีเนื้อหาบทเรียน + เนื้อหาบทเรียนจะถูกแสดงไว้ที่นี่ จากนั้นคุณสามารถเผยแพร่สำหรับการใช้งานออฟไลน์ + ยกเลิกการเปลี่ยนแปลงหรือไม่ + หากคุณเลือกล้มเลิก การเปลี่ยนแปลงจะไม่ถูกบันทึกไว้ + ล้มเลิก + ซิงค์ข้อมูลออฟไลน์หรือไม่ + นี่จะเป็นการซิงค์ข้อมูล ~%s อาจมีค่าบริการเพิ่มเติมจากผู้ให้บริการเครือข่ายหากคุณไม่ได้เชื่อมต่อกับเครือข่าย Wi-Fi + นี่จะเป็นการซิงค์ข้อมูล ~%s เฉพาะในขณะที่คุณเชื่อมต่อกับเครือข่าย Wi-Fi + ซิงค์ + กำลังโหลด... + อดทนไว้ เรากำลังเตรียมการให้กับคุณ + ขยายเนื้อหา + ย่อเนื้อหา การเปิดใช้การซิงค์เนื้อหาอัตโนมัติจะเป็นการจัดการการดาวน์โหลดเนื้อหาที่เลือกตามค่าปรับตั้งต่อไปนี้ การซิงค์เนื้อหาจะเกิดขึ้นแม้ว่าแอพพลิเคชั่นจะไม่เปิดทำงานอยู่ หากมีการปิดค่านี้ จะไม่มีการซิงค์ข้อมูลเกิดขึ้น เนื้อหาที่ดาวน์โหลดแล้วจะไม่ถูกลบทิ้ง ความถี่ในการซิงค์ ซิงค์เนื้อหาอัตโนมัติ @@ -1452,7 +1467,6 @@ %d บทเรียนกำลังซิงค์อยู่ %d บทเรียนกำลังซิงค์อยู่ - ภาพเนื้อหาบทเรียน ภารกิจนี้ไม่มีอยู่อีกต่อไป คุณออฟไลน์อยู่ ปัจจุบันคุณไม่มีบทเรียนแบบออฟไลน์ @@ -1464,4 +1478,5 @@ %d บทเรียนได้รับการซิงค์แล้ว %d บทเรียนได้รับการซิงค์แล้ว + เนื้อหาบทเรียนเพิ่มเติม diff --git a/libs/pandares/src/main/res/values-vi/strings.xml b/libs/pandares/src/main/res/values-vi/strings.xml index 886940b349..444e67677b 100644 --- a/libs/pandares/src/main/res/values-vi/strings.xml +++ b/libs/pandares/src/main/res/values-vi/strings.xml @@ -1411,6 +1411,21 @@ Chọn Tất Cả Bỏ Chọn Tất Cả Đã xảy ra lỗi khi tải nội dung. + Không Có Khóa Học + Khóa học của bạn sẽ được liệt kê ở đây, và sau đó bạn có thể xử lý để sử dụng ngoại tuyến. + Không Có Nội Dung Khóa Học + Nội dung khóa học sẽ được liệt kê ở đây, và sau đó bạn có thể xử lý để sử dụng ngoại tuyến. + Hủy Bỏ Thay Đổi? + Nếu bạn chọn hủy, các thay đổi sẽ không được lưu. + Hủy Bỏ + Đồng Bộ Hóa Nội Dung Ngoại Tuyến? + Thao tác này sẽ đồng bộ ~%s nội dung. Điều này có thể khiến bạn bị tính thêm phí từ nhà cung cấp dữ liệu của bạn, nếu bạn không kết nối với mạng Wi-Fi. + Thao tác này sẽ đồng bộ ~%s nội dung chỉ khi bạn kết nối với mạng Wi-Fi. + Đồng bộ + Đang tải... + Hãy chờ nhé, chúng tôi đang chuẩn bị mọi thứ cho bạn. + Mở rộng nội dung + Thu gọn nội dung Bật chức năng Tự Động Đồng Bộ Nội Dung sẽ xử lý việc tải xuống các nội dung được chọn dựa theo cài đặt dưới đây. Thao tác đồng bộ nội dung sẽ diễn ra ngay cả khi ứng dụng không chạy. Nếu tắt cài đặt thì quá trình đồng bộ sẽ không diễn ra. Nội dung đã được tải xuống sẽ không bị xóa. Tần Suất Đồng Bộ Tự Động Đồng Bộ Nội Dung @@ -1453,7 +1468,6 @@ %d khóa học đang đồng bộ. %d khóa học đang đồng bộ. - Hình ảnh nội dung khóa học Bài tập này không còn khả dụng. Bạn đang ngoại tuyến Hiện tại bạn không có bất kỳ khóa học nào khả dụng ngoại tuyến. @@ -1465,4 +1479,5 @@ %d khóa học đã được đồng bộ. %d khóa học đã được đồng bộ. + Nội dung khóa học bổ sung diff --git a/libs/pandares/src/main/res/values-zh/strings.xml b/libs/pandares/src/main/res/values-zh/strings.xml index f9e4a98069..d2e4fdcb21 100644 --- a/libs/pandares/src/main/res/values-zh/strings.xml +++ b/libs/pandares/src/main/res/values-zh/strings.xml @@ -1392,6 +1392,21 @@ 全选 取消全选 加载内容时出错。 + 没有课程 + 此处将列出您的课程,然后您可以使内容离线可用。 + 无课程内容 + 此处将列出课程内容,然后您可以使内容离线可用。 + 放弃更改? + 如果您选择放弃,更改将不会保存。 + 放弃 + 同步离线内容? + 大约会有 %s 项内容同步。如果未连接到无线网络,您的数据流量提供商可能会额外收费。 + 仅当连接到无线网络时,大约会有 %s 项内容同步。 + 同步 + 加载中... + 正在准备中,请稍等。 + 扩展内容 + 折叠内容 如果启用“自动同步内容”,将根据以下设置下载所选内容。即使应用程序没有运行,内容也会同步。如果关闭该设置,将不会同步。已下载的内容不会被删除。 同步周期 自动同步内容 @@ -1433,7 +1448,6 @@ %d 门课程正在同步。 - 课程内容图像 此作业不再可用。 您已离线 您目前没有任何可离线使用的课程。 @@ -1444,4 +1458,5 @@ %d 门课程已同步。 + 更多课程内容 From 9de1d609f769a02f11f4cb7735cd7551672235e1 Mon Sep 17 00:00:00 2001 From: Kristof Deak <92309696+kdeakinstructure@users.noreply.github.com> Date: Wed, 22 Nov 2023 12:41:40 +0100 Subject: [PATCH 015/132] [MBL-17114][Student] - Implement Dashboard Offline Unavailable features E2E test case (#2248) --- .../student/ui/e2e/LoginE2ETest.kt | 2 + .../offline/ManageOfflineContentE2ETest.kt | 5 +- .../ui/e2e/offline/OfflineDashboardE2ETest.kt | 70 ++++++++++++++++++- .../e2e/offline/OfflineSyncProgressE2ETest.kt | 2 - .../ui/e2e/offline/utils/OfflineTestUtils.kt | 30 ++++++-- .../student/ui/pages/DashboardPage.kt | 27 +++++-- .../espresso/matchers/WaitForViewMatcher.kt | 5 ++ .../panda_annotations/TestMetaData.kt | 2 +- 8 files changed, 122 insertions(+), 21 deletions(-) diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/LoginE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/LoginE2ETest.kt index 03c3a51ddc..374e00d019 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/LoginE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/LoginE2ETest.kt @@ -300,6 +300,8 @@ class LoginE2ETest : StudentTest() { private fun loginWithUser(user: CanvasUserApiModel, lastSchoolSaved: Boolean = false) { + Thread.sleep(5100) //Need to wait > 5 seconds before each login attempt because of new 'too many attempts' login policy on web. + if(lastSchoolSaved) { Log.d(STEP_TAG,"Click 'Find Another School' button.") loginLandingPage.clickFindAnotherSchoolButton() diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/ManageOfflineContentE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/ManageOfflineContentE2ETest.kt index 2d0af62c9e..aa8a2108a5 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/ManageOfflineContentE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/ManageOfflineContentE2ETest.kt @@ -48,7 +48,6 @@ class ManageOfflineContentE2ETest : StudentTest() { val student = data.studentsList[0] val course1 = data.coursesList[0] val course2 = data.coursesList[1] - val testAnnouncement = data.announcementsList[0] Log.d(STEP_TAG,"Login with user: ${student.name}, login id: ${student.loginId}.") tokenLogin(student) @@ -185,8 +184,8 @@ class ManageOfflineContentE2ETest : StudentTest() { Log.d(STEP_TAG, "Assert that both of the seeded courses are displayed as a selectable item in the Manage Offline Content page.") manageOfflineContentPage.assertCourseCountWithMatcher(2) - Log.d(STEP_TAG, "Click on the 'Sync' button.") - manageOfflineContentPage.clickOnSyncButton() + Log.d(STEP_TAG, "Click on the 'Sync' button and confirm sync.") + manageOfflineContentPage.clickOnSyncButtonAndConfirm() Log.d(STEP_TAG, "Wait for the 'Download Started' dashboard notification to be displayed, and the to disappear.") dashboardPage.waitForRender() diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineDashboardE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineDashboardE2ETest.kt index b874bb2430..309f23f989 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineDashboardE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineDashboardE2ETest.kt @@ -17,9 +17,12 @@ package com.instructure.student.ui.e2e.offline import android.util.Log +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.uiautomator.UiDevice import com.instructure.canvas.espresso.OfflineE2E import com.instructure.panda_annotations.FeatureCategory import com.instructure.panda_annotations.Priority +import com.instructure.panda_annotations.SecondaryFeatureCategory import com.instructure.panda_annotations.TestCategory import com.instructure.panda_annotations.TestMetaData import com.instructure.student.ui.e2e.offline.utils.OfflineTestUtils @@ -40,6 +43,7 @@ class OfflineDashboardE2ETest : StudentTest() { @Test @TestMetaData(Priority.MANDATORY, FeatureCategory.DASHBOARD, TestCategory.E2E) fun testOfflineDashboardE2E() { + Log.d(PREPARATION_TAG,"Seeding data.") val data = seedData(students = 1, teachers = 1, courses = 2, announcements = 1) val student = data.studentsList[0] @@ -47,6 +51,9 @@ class OfflineDashboardE2ETest : StudentTest() { val course2 = data.coursesList[1] val testAnnouncement = data.announcementsList[0] + Log.d(PREPARATION_TAG, "Get the device to be able to perform app-independent actions on it.") + val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) + Log.d(STEP_TAG,"Login with user: ${student.name}, login id: ${student.loginId}.") tokenLogin(student) dashboardPage.waitForRender() @@ -57,8 +64,6 @@ class OfflineDashboardE2ETest : StudentTest() { Log.d(STEP_TAG, "Select the entire '${course1.name}' course for sync. Click on the 'Sync' button.") manageOfflineContentPage.changeItemSelectionState(course1.name) manageOfflineContentPage.clickOnSyncButtonAndConfirm() - manageOfflineContentPage.changeItemSelectionState(course1.name) - manageOfflineContentPage.clickOnSyncButton() Log.d(STEP_TAG, "Wait for the 'Download Started' dashboard notification to be displayed, and the to disappear.") dashboardPage.waitForRender() @@ -71,6 +76,7 @@ class OfflineDashboardE2ETest : StudentTest() { Log.d(PREPARATION_TAG, "Turn off the Wi-Fi and Mobile Data on the device, so it will go offline.") OfflineTestUtils.turnOffConnectionViaADB() + device.waitForIdle() Log.d(STEP_TAG, "Wait for the Dashboard Page to be rendered. Refresh the page.") dashboardPage.waitForRender() @@ -78,7 +84,7 @@ class OfflineDashboardE2ETest : StudentTest() { Log.d(STEP_TAG, "Assert that the Offline Indicator (bottom banner) is displayed on the Dashboard Page.") OfflineTestUtils.assertOfflineIndicator() - Log.d(STEP_TAG, "Assert that the offline sync icon only displayed on the synced course's cours card.") + Log.d(STEP_TAG, "Assert that the offline sync icon only displayed on the synced course's course card.") dashboardPage.assertCourseOfflineSyncIconVisible(course1.name) dashboardPage.assertCourseOfflineSyncIconGone(course2.name) @@ -90,6 +96,64 @@ class OfflineDashboardE2ETest : StudentTest() { announcementListPage.assertTopicDisplayed(testAnnouncement.title) } + @OfflineE2E + @Test + @TestMetaData(Priority.MANDATORY, FeatureCategory.DASHBOARD, TestCategory.E2E, false, SecondaryFeatureCategory.OFFLINE_MODE) + fun testOfflineDashboardUnavailableFeaturesE2E() { + + Log.d(PREPARATION_TAG,"Seeding data.") + val data = seedData(students = 1, teachers = 1, courses = 1) + val student = data.studentsList[0] + val course = data.coursesList[0] + + Log.d(PREPARATION_TAG, "Get the device to be able to perform app-independent actions on it.") + val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) + + Log.d(STEP_TAG,"Login with user: ${student.name}, login id: ${student.loginId}.") + tokenLogin(student) + dashboardPage.waitForRender() + + Log.d(STEP_TAG, "Open the '${course.name}' course's 'Manage Offline Content' page via the more menu of the Dashboard Page.") + dashboardPage.clickCourseOverflowMenu(course.name, "Manage Offline Content") + + Log.d(STEP_TAG, "Select the entire '${course.name}' course for sync. Click on the 'Sync' button.") + manageOfflineContentPage.changeItemSelectionState(course.name) + manageOfflineContentPage.clickOnSyncButtonAndConfirm() + + Log.d(STEP_TAG, "Wait for the 'Download Started' dashboard notification to be displayed, and the to disappear.") + dashboardPage.waitForRender() + dashboardPage.waitForSyncProgressDownloadStartedNotification() + dashboardPage.waitForSyncProgressDownloadStartedNotificationToDisappear() + + Log.d(STEP_TAG, "Wait for the 'Syncing Offline Content' dashboard notification to be displayed, and the to disappear. (It should be displayed after the 'Download Started' notification immediately.)") + dashboardPage.waitForSyncProgressStartingNotification() + dashboardPage.waitForSyncProgressStartingNotificationToDisappear() + + Log.d(PREPARATION_TAG, "Turn off the Wi-Fi and Mobile Data on the device, so it will go offline.") + OfflineTestUtils.turnOffConnectionViaADB() + device.waitForIdle() + + Log.d(STEP_TAG, "Wait for the Dashboard Page to be rendered. Refresh the page.") + dashboardPage.waitForRender() + + Log.d(STEP_TAG, "Assert that the Offline Indicator (bottom banner) is displayed on the Dashboard Page.") + OfflineTestUtils.assertOfflineIndicator() + + Log.d(STEP_TAG, "Assert that the bottom menus (except Dashboard) are disabled and unavailable in offline mode.") + dashboardPage.assertBottomMenusAreDisabled() + + Log.d(STEP_TAG, "Try to open the '${course.name}' course's more menu of the Dashboard Page. Assert that the 'No Internet Connection' dialog is displayed. Dismiss it after the assertion.") + dashboardPage.clickOnCourseOverflowButton(course.name) + OfflineTestUtils.assertNoInternetConnectionDialog() + OfflineTestUtils.dismissNoInternetConnectionDialog() + + Log.d(STEP_TAG, "Try to open the global 'Manage Offline Content' page via the more menu of the Dashboard Page. Assert that the 'No Internet Connection' dialog is displayed. Dismiss it after the assertion.") + Thread.sleep(5000) //Wait for the system notification to disappear, because it overlaps the More menu button on the toolbar. + dashboardPage.openGlobalManageOfflineContentPage() + OfflineTestUtils.assertNoInternetConnectionDialog() + OfflineTestUtils.dismissNoInternetConnectionDialog() + } + @After fun tearDown() { Log.d(PREPARATION_TAG, "Turn back on the Wi-Fi and Mobile Data on the device, so it will come back online.") diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineSyncProgressE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineSyncProgressE2ETest.kt index dc0b91da01..77cea19bc2 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineSyncProgressE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineSyncProgressE2ETest.kt @@ -59,8 +59,6 @@ class OfflineSyncProgressE2ETest : StudentTest() { Log.d(STEP_TAG, "Select the entire '${course1.name}' course for sync. Click on the 'Sync' button.") manageOfflineContentPage.changeItemSelectionState(course1.name) manageOfflineContentPage.clickOnSyncButtonAndConfirm() - manageOfflineContentPage.changeItemSelectionState(course1.name) - manageOfflineContentPage.clickOnSyncButton() Log.d(STEP_TAG, "Wait for the 'Download Started' dashboard notification to be displayed, and the to disappear.") dashboardPage.waitForRender() diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/utils/OfflineTestUtils.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/utils/OfflineTestUtils.kt index 717d37af98..7cd51b7a66 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/utils/OfflineTestUtils.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/utils/OfflineTestUtils.kt @@ -16,14 +16,14 @@ */ package com.instructure.student.ui.e2e.offline.utils -import androidx.test.espresso.matcher.ViewMatchers.withChild -import androidx.test.espresso.matcher.ViewMatchers.withId -import androidx.test.espresso.matcher.ViewMatchers.withText +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.matcher.ViewMatchers.* import androidx.test.platform.app.InstrumentationRegistry import androidx.test.uiautomator.UiDevice import androidx.test.uiautomator.UiScrollable import androidx.test.uiautomator.UiSelector import com.instructure.espresso.assertDisplayed +import com.instructure.espresso.click import com.instructure.espresso.matchers.WaitForViewMatcher.waitForView import com.instructure.espresso.page.plus import com.instructure.student.R @@ -31,16 +31,22 @@ import org.hamcrest.CoreMatchers.allOf object OfflineTestUtils { + private const val ENABLE_WIFI_COMMAND: String = "svc wifi enable" + private const val DISABLE_WIFI_COMMAND: String = "svc wifi disable" + + private const val ENABLE_MOBILE_DATA_COMMAND: String = "svc data enable" + private const val DISABLE_MOBILE_DATA_COMMAND: String = "svc data disable" + fun turnOffConnectionViaADB() { val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) - device.executeShellCommand("svc wifi disable") - device.executeShellCommand("svc data disable") + device.executeShellCommand(DISABLE_WIFI_COMMAND) + device.executeShellCommand(DISABLE_MOBILE_DATA_COMMAND) } fun turnOnConnectionViaADB() { val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) - device.executeShellCommand("svc wifi enable") - device.executeShellCommand("svc data enable") + device.executeShellCommand(ENABLE_WIFI_COMMAND) + device.executeShellCommand(ENABLE_MOBILE_DATA_COMMAND) } fun turnOffConnectionOnUI() { @@ -80,4 +86,14 @@ object OfflineTestUtils { ) ).assertDisplayed() } + + fun assertNoInternetConnectionDialog() { + waitForView(withId(R.id.alertTitle) + withText(R.string.noInternetConnectionTitle)).assertDisplayed() + } + + fun dismissNoInternetConnectionDialog() { + onView(withText(android.R.string.ok) + isDescendantOfA(withId(R.id.buttonPanel) + + hasSibling(withId(R.id.topPanel) + + hasDescendant(withText(R.string.noInternetConnectionTitle))))).click() + } } \ No newline at end of file diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/DashboardPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/DashboardPage.kt index 78121a4c7e..a2a1c189f4 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/DashboardPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/DashboardPage.kt @@ -38,6 +38,7 @@ import com.instructure.canvasapi2.models.Group import com.instructure.dataseeding.model.CourseApiModel import com.instructure.dataseeding.model.GroupApiModel import com.instructure.espresso.* +import com.instructure.espresso.matchers.WaitForViewMatcher.waitForViewToBeCompletelyDisplayed import com.instructure.espresso.page.* import com.instructure.student.R import com.instructure.student.ui.utils.ViewUtils @@ -255,18 +256,22 @@ class DashboardPage : BasePage(R.id.dashboardPage) { } fun switchCourseView() { - Espresso.openActionBarOverflowOrOptionsMenu(InstrumentationRegistry.getInstrumentation().targetContext) + clickDashboardGlobalOverflowButton() onView(withText(containsString("Switch to"))) .perform(click()); } - //OfflineMethod fun openGlobalManageOfflineContentPage() { - Espresso.openActionBarOverflowOrOptionsMenu(InstrumentationRegistry.getInstrumentation().targetContext) - onView(withText(containsString("Manage Offline Content"))) + clickDashboardGlobalOverflowButton() + onView(withText(containsString("Manage Offline Content"))) .perform(click()); } + private fun clickDashboardGlobalOverflowButton() { + waitForViewToBeCompletelyDisplayed(withContentDescription("More options") + withAncestor(R.id.toolbar)) + Espresso.openActionBarOverflowOrOptionsMenu(InstrumentationRegistry.getInstrumentation().targetContext) + } + fun clickEditDashboard() { onView(withId(R.id.editDashboardTextView)).scrollTo().click() } @@ -304,12 +309,16 @@ class DashboardPage : BasePage(R.id.dashboardPage) { } fun clickCourseOverflowMenu(courseTitle: String, menuTitle: String) { + clickOnCourseOverflowButton(courseTitle) + waitForView(withId(R.id.title) + withText(menuTitle)).click() + } + + fun clickOnCourseOverflowButton(courseTitle: String) { val courseOverflowMatcher = withId(R.id.overflow) + withAncestor( withId(R.id.cardView) + withDescendant(withId(R.id.titleTextView) + withText(courseTitle)) ) waitForView(courseOverflowMatcher).scrollTo().click() - waitForView(withId(R.id.title) + withText(menuTitle)).click() } fun assertCourseGrade(courseName: String, courseGrade: String) { @@ -379,6 +388,14 @@ class DashboardPage : BasePage(R.id.dashboardPage) { fun waitForSyncProgressStartingNotificationToDisappear() { ViewUtils.waitForViewToDisappear(withText(com.instructure.pandautils.R.string.syncProgress_syncingOfflineContent), 30) } + + //OfflineMethod + fun assertBottomMenusAreDisabled() { + onView(withId(R.id.bottomNavigationCalendar)).check(matches(isNotEnabled())) + onView(withId(R.id.bottomNavigationToDo)).check(matches(isNotEnabled())) + onView(withId(R.id.bottomNavigationNotifications)).check(matches(isNotEnabled())) + onView(withId(R.id.bottomNavigationInbox)).check(matches(isNotEnabled())) + } } /** diff --git a/automation/espresso/src/main/kotlin/com/instructure/espresso/matchers/WaitForViewMatcher.kt b/automation/espresso/src/main/kotlin/com/instructure/espresso/matchers/WaitForViewMatcher.kt index 0d543a6b25..9c56c3e5a6 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/espresso/matchers/WaitForViewMatcher.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/espresso/matchers/WaitForViewMatcher.kt @@ -51,6 +51,11 @@ object WaitForViewMatcher { return waitForViewWithCustomMatcher(viewMatcher, duration, ViewMatchers.isClickable()) } + fun waitForViewToBeCompletelyDisplayed(viewMatcher: Matcher, duration: Long = 10): ViewInteraction { + log.i("Wait for View to be completely displayed.") + return waitForViewWithCustomMatcher(viewMatcher, duration, ViewMatchers.isCompletelyDisplayed()) + } + private fun waitForViewWithCustomMatcher(viewMatcher: Matcher, duration: Long = 10, customMatcher: Matcher): ViewInteraction { waiting.set(true) val waitTime = TimeUnit.SECONDS.toMillis(duration) diff --git a/libs/panda_annotations/src/main/java/com/instructure/panda_annotations/TestMetaData.kt b/libs/panda_annotations/src/main/java/com/instructure/panda_annotations/TestMetaData.kt index 7b7909c6e6..e9f5f0ddb6 100644 --- a/libs/panda_annotations/src/main/java/com/instructure/panda_annotations/TestMetaData.kt +++ b/libs/panda_annotations/src/main/java/com/instructure/panda_annotations/TestMetaData.kt @@ -43,7 +43,7 @@ enum class SecondaryFeatureCategory { ASSIGNMENT_COMMENTS, ASSIGNMENT_QUIZZES, ASSIGNMENT_DISCUSSIONS, GROUPS_DASHBOARD, GROUPS_FILES, GROUPS_ANNOUNCEMENTS, GROUPS_DISCUSSIONS, GROUPS_PAGES, GROUPS_PEOPLE, EVENTS_DISCUSSIONS, EVENTS_QUIZZES, EVENTS_ASSIGNMENTS, EVENTS_NOTIFICATIONS, - MODULES_ASSIGNMENTS, MODULES_DISCUSSIONS, MODULES_FILES, MODULES_PAGES, MODULES_QUIZZES + MODULES_ASSIGNMENTS, MODULES_DISCUSSIONS, MODULES_FILES, MODULES_PAGES, MODULES_QUIZZES, OFFLINE_MODE } enum class TestCategory { From f4b7a4b6610a2dbfd65916954707127ee8715baa Mon Sep 17 00:00:00 2001 From: Akos Hermann <72087159+hermannakos@users.noreply.github.com> Date: Wed, 22 Nov 2023 14:34:05 +0100 Subject: [PATCH 016/132] [MBL-17212][Student][Teacher] Open external routes from discussions (#2258) refs: MBL-17212 affects: Student, Teacher release note: none test plan: Navigate to an external route from the discussion redesign page. --- .../student/navigation/StudentWebViewRouter.kt | 5 +++++ .../teacher/navigation/TeacherWebViewRouter.kt | 14 ++++++++++++++ .../details/DiscussionDetailsWebViewFragment.kt | 2 +- .../pandautils/navigation/WebViewRouter.kt | 2 ++ 4 files changed, 22 insertions(+), 1 deletion(-) diff --git a/apps/student/src/main/java/com/instructure/student/navigation/StudentWebViewRouter.kt b/apps/student/src/main/java/com/instructure/student/navigation/StudentWebViewRouter.kt index 6eb681b270..7ae5299c74 100644 --- a/apps/student/src/main/java/com/instructure/student/navigation/StudentWebViewRouter.kt +++ b/apps/student/src/main/java/com/instructure/student/navigation/StudentWebViewRouter.kt @@ -19,6 +19,7 @@ package com.instructure.student.navigation import androidx.fragment.app.FragmentActivity import com.instructure.canvasapi2.utils.ApiPrefs import com.instructure.pandautils.navigation.WebViewRouter +import com.instructure.student.fragment.InternalWebviewFragment import com.instructure.student.router.RouteMatcher class StudentWebViewRouter(val activity: FragmentActivity) : WebViewRouter { @@ -34,4 +35,8 @@ class StudentWebViewRouter(val activity: FragmentActivity) : WebViewRouter { override fun openMedia(url: String) { RouteMatcher.openMedia(activity, url) } + + override fun routeExternally(url: String) { + RouteMatcher.route(activity, InternalWebviewFragment.makeRoute(url, url, false, "")) + } } \ No newline at end of file diff --git a/apps/teacher/src/main/java/com/instructure/teacher/navigation/TeacherWebViewRouter.kt b/apps/teacher/src/main/java/com/instructure/teacher/navigation/TeacherWebViewRouter.kt index 2f457b39c3..07c508bb48 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/navigation/TeacherWebViewRouter.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/navigation/TeacherWebViewRouter.kt @@ -17,8 +17,12 @@ package com.instructure.teacher.navigation import androidx.fragment.app.FragmentActivity +import com.instructure.canvasapi2.models.CanvasContext import com.instructure.canvasapi2.utils.ApiPrefs +import com.instructure.interactions.router.Route import com.instructure.pandautils.navigation.WebViewRouter +import com.instructure.teacher.fragments.FullscreenInternalWebViewFragment +import com.instructure.teacher.fragments.InternalWebViewFragment import com.instructure.teacher.router.RouteMatcher class TeacherWebViewRouter(val activity: FragmentActivity) : WebViewRouter { @@ -34,4 +38,14 @@ class TeacherWebViewRouter(val activity: FragmentActivity) : WebViewRouter { override fun openMedia(url: String) { RouteMatcher.openMedia(activity, url) } + + override fun routeExternally(url: String) { + val bundle = InternalWebViewFragment.makeBundle(url, url, false, "") + RouteMatcher.route( + activity, Route( + FullscreenInternalWebViewFragment::class.java, + CanvasContext.emptyUserContext(), bundle + ) + ) + } } \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/discussion/details/DiscussionDetailsWebViewFragment.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/discussion/details/DiscussionDetailsWebViewFragment.kt index 3d706811c4..c402282c08 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/discussion/details/DiscussionDetailsWebViewFragment.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/discussion/details/DiscussionDetailsWebViewFragment.kt @@ -96,7 +96,7 @@ class DiscussionDetailsWebViewFragment : Fragment() { override fun routeInternallyCallback(url: String) { if (!webViewRouter.canRouteInternally(url, routeIfPossible = true)) { - webViewRouter.routeInternally(url) + webViewRouter.routeExternally(url) } } diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/navigation/WebViewRouter.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/navigation/WebViewRouter.kt index 2816a16f53..f577adddc1 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/navigation/WebViewRouter.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/navigation/WebViewRouter.kt @@ -23,4 +23,6 @@ interface WebViewRouter { fun routeInternally(url: String) fun openMedia(url: String) + + fun routeExternally(url: String) } \ No newline at end of file From f36a3bda027be7edb5d044780569e6a11034f87e Mon Sep 17 00:00:00 2001 From: Kristof Deak <92309696+kdeakinstructure@users.noreply.github.com> Date: Wed, 22 Nov 2023 15:47:22 +0100 Subject: [PATCH 017/132] [MBL-17146][Student] - Workaround to stabilize flaky Inbox E2E tests (#2252) --- .../student/ui/e2e/InboxE2ETest.kt | 18 +++++++-- .../instructure/student/ui/pages/InboxPage.kt | 5 +-- .../teacher/ui/e2e/InboxE2ETest.kt | 28 ++++++++++---- .../com/instructure/espresso/TestingUtils.kt | 38 +++++++++++++++++++ 4 files changed, 75 insertions(+), 14 deletions(-) diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/InboxE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/InboxE2ETest.kt index b2d370238f..35baba10c0 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/InboxE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/InboxE2ETest.kt @@ -25,6 +25,8 @@ import com.instructure.canvas.espresso.refresh import com.instructure.dataseeding.api.ConversationsApi import com.instructure.dataseeding.api.GroupsApi import com.instructure.dataseeding.model.CanvasUserApiModel +import com.instructure.espresso.retry +import com.instructure.espresso.retryWithIncreasingDelay import com.instructure.panda_annotations.FeatureCategory import com.instructure.panda_annotations.Priority import com.instructure.panda_annotations.TestCategory @@ -113,6 +115,7 @@ class InboxE2ETest: StudentTest() { inboxPage.selectConversation(seededConversation) inboxPage.assertSelectedConversationNumber("1") inboxPage.clickUnArchive() + inboxPage.assertInboxEmpty() inboxPage.assertConversationNotDisplayed(seededConversation.subject) @@ -127,7 +130,10 @@ class InboxE2ETest: StudentTest() { inboxPage.selectConversations(listOf(seededConversation.subject)) inboxPage.assertSelectedConversationNumber("1") inboxPage.clickUnstar() - inboxPage.assertConversationNotStarred(seededConversation.subject) + + retryWithIncreasingDelay(times = 10, maxDelay = 3000) { + inboxPage.assertConversationNotStarred(seededConversation.subject) + } Log.d(STEP_TAG, "Select the conversations (${seededConversation.subject} and archive it. Assert that it has not displayed in the 'INBOX' scope.") inboxPage.selectConversations(listOf(seededConversation.subject)) @@ -161,7 +167,10 @@ class InboxE2ETest: StudentTest() { Log.d(STEP_TAG, "Select the conversation. Unarchive it, and assert that it has not displayed in the 'ARCHIVED' scope.") inboxPage.selectConversations(listOf(seededConversation.subject)) inboxPage.clickUnArchive() - inboxPage.assertConversationNotDisplayed(seededConversation.subject) + + retryWithIncreasingDelay(times = 10, maxDelay = 3000) { + inboxPage.assertConversationNotDisplayed(seededConversation.subject) + } Log.d(STEP_TAG, "Navigate to 'STARRED' scope and assert that the conversations is displayed there.") inboxPage.filterInbox("Starred") @@ -346,7 +355,10 @@ class InboxE2ETest: StudentTest() { Log.d(STEP_TAG, "Navigate to 'STARRED' scope. Assert that the conversation is displayed in the 'STARRED' scope.") inboxPage.filterInbox("Starred") - inboxPage.assertConversationDisplayed(seededConversation.subject) + + retry(times = 10, delay = 3000) { + inboxPage.assertConversationDisplayed(seededConversation.subject) + } Log.d(STEP_TAG, "Swipe '${seededConversation.subject}' left and assert it is removed from the 'STARRED' scope because it has became unstarred.") inboxPage.swipeConversationLeft(seededConversation) diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/InboxPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/InboxPage.kt index 47ac688d03..8626f8d2a0 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/InboxPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/InboxPage.kt @@ -145,7 +145,6 @@ class InboxPage : BasePage(R.id.inboxPage) { hasSibling(allOf(withId(R.id.subjectView), withText(subject)))) waitForMatcherWithRefreshes(matcher) // May need to refresh before the star shows up onView(matcher).scrollTo().assertDisplayed() - } fun assertConversationNotStarred(subject: String) { @@ -154,9 +153,7 @@ class InboxPage : BasePage(R.id.inboxPage) { hasSibling(withId(R.id.userName)), hasSibling(withId(R.id.date)), hasSibling(allOf(withId(R.id.subjectView), withText(subject)))) - waitForMatcherWithRefreshes(matcher) // May need to refresh before the star shows up onView(matcher).check(matches(withEffectiveVisibility(ViewMatchers.Visibility.GONE))) - } fun assertUnreadMarkerVisibility(conversation: Conversation, visibility: ViewMatchers.Visibility) { @@ -192,7 +189,7 @@ class InboxPage : BasePage(R.id.inboxPage) { } } - fun assertInboxEmpty() { + fun assertInboxEmpty() { waitForView(withId(R.id.emptyInboxView)).assertDisplayed() } diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/InboxE2ETest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/InboxE2ETest.kt index f9ecf29cbc..3fe3c27741 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/InboxE2ETest.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/InboxE2ETest.kt @@ -4,10 +4,13 @@ import android.util.Log import androidx.test.espresso.Espresso import androidx.test.espresso.matcher.ViewMatchers import com.instructure.canvas.espresso.E2E +import com.instructure.canvas.espresso.refresh import com.instructure.dataseeding.api.ConversationsApi import com.instructure.dataseeding.api.GroupsApi import com.instructure.dataseeding.model.CanvasUserApiModel import com.instructure.dataseeding.model.CourseApiModel +import com.instructure.espresso.retry +import com.instructure.espresso.retryWithIncreasingDelay import com.instructure.panda_annotations.FeatureCategory import com.instructure.panda_annotations.Priority import com.instructure.panda_annotations.TestCategory @@ -218,7 +221,11 @@ class InboxE2ETest : TeacherTest() { Log.d(STEP_TAG, "Select 'ARCHIVED' scope and assert that '${seedConversation2[0].subject}' conversation is displayed in the 'ARCHIVED' scope.") inboxPage.filterMessageScope("Archived") - inboxPage.assertConversationDisplayed(seedConversation2[0].subject) + + retry(times = 10, delay = 3000) { + refresh() + inboxPage.assertConversationDisplayed(seedConversation2[0].subject) + } Log.d(STEP_TAG, "Select '${seedConversation2[0].subject}' conversation and unarchive it." + "Assert that the selected number of conversation on the toolbar is 1 and '${seedConversation2[0].subject}' conversation is not displayed in the 'ARCHIVED' scope.") @@ -372,21 +379,28 @@ class InboxE2ETest : TeacherTest() { Log.d(STEP_TAG, "Select both of the conversations. Star them and mark the unread.") inboxPage.selectConversations(listOf(seedConversation2[0].subject, seedConversation3[0].subject)) - inboxPage.clickStar() inboxPage.clickMarkAsRead() + retry(times = 10, delay = 3000) { + Log.d(STEP_TAG, "Assert that '${seedConversation3[0].subject}' conversation is read.") + inboxPage.assertUnreadMarkerVisibility(seedConversation3[0].subject, ViewMatchers.Visibility.GONE) + } + + Log.d(STEP_TAG, "Select both of the conversations. Star them and mark the unread.") + inboxPage.clickStar() + Log.d(STEP_TAG, "Navigate to 'STARRED' scope. Assert that both of the conversation are displayed in the 'STARRED' scope.") inboxPage.filterMessageScope("Starred") - inboxPage.assertConversationDisplayed(seedConversation2[0].subject) - inboxPage.assertConversationDisplayed(seedConversation3[0].subject) + + retryWithIncreasingDelay(times = 10, maxDelay = 3000) { + inboxPage.assertConversationDisplayed(seedConversation2[0].subject) + inboxPage.assertConversationDisplayed(seedConversation3[0].subject) + } Log.d(STEP_TAG, "Swipe '${seedConversation2[0].subject}' left and assert it is removed from the 'STARRED' scope because it has became unstarred.") inboxPage.swipeConversationLeft(seedConversation2[0]) inboxPage.assertConversationNotDisplayed(seedConversation2[0].subject) - Log.d(STEP_TAG, "Assert that '${seedConversation3[0].subject}' conversation is read.") - inboxPage.assertUnreadMarkerVisibility(seedConversation3[0].subject, ViewMatchers.Visibility.GONE) - Log.d(STEP_TAG, "Swipe '${seedConversation3[0].subject}' conversation right and assert that it has became unread.") inboxPage.swipeConversationRight(seedConversation3[0].subject) inboxPage.assertUnreadMarkerVisibility(seedConversation3[0].subject, ViewMatchers.Visibility.VISIBLE) diff --git a/automation/espresso/src/main/kotlin/com/instructure/espresso/TestingUtils.kt b/automation/espresso/src/main/kotlin/com/instructure/espresso/TestingUtils.kt index 254db9d454..3c21299ac9 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/espresso/TestingUtils.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/espresso/TestingUtils.kt @@ -49,4 +49,42 @@ fun getCurrentDateInCanvasFormat(): String { val dayString = expectedDate.dayOfMonth val yearString = expectedDate.year return "$monthString $dayString, $yearString" +} + +fun retry( + times: Int = 3, + delay: Long = 1000, + block: () -> Unit +) { + repeat(times - 1) { + try { + block() + return + } catch (e: Throwable) { + e.printStackTrace() + } + Thread.sleep(delay) + } + block() +} + +fun retryWithIncreasingDelay( + times: Int = 3, + initialDelay: Long = 100, + maxDelay: Long = 1000, + factor: Double = 2.0, + block: () -> Unit +) { + var currentDelay = initialDelay + repeat(times - 1) { + try { + block() + return + } catch (e: Throwable) { + e.printStackTrace() + } + Thread.sleep(currentDelay) + currentDelay = (currentDelay * factor).toLong().coerceAtMost(maxDelay) + } + block() } \ No newline at end of file From 2211a4f5737dd9f8d1007349abbd59e37cd8d06e Mon Sep 17 00:00:00 2001 From: Akos Hermann <72087159+hermannakos@users.noreply.github.com> Date: Wed, 22 Nov 2023 14:34:05 +0100 Subject: [PATCH 018/132] [MBL-17212][Student][Teacher] Open external routes from discussions (#2258) refs: MBL-17212 affects: Student, Teacher release note: none test plan: Navigate to an external route from the discussion redesign page. --- .../student/navigation/StudentWebViewRouter.kt | 5 +++++ .../teacher/navigation/TeacherWebViewRouter.kt | 14 ++++++++++++++ .../details/DiscussionDetailsWebViewFragment.kt | 2 +- .../pandautils/navigation/WebViewRouter.kt | 2 ++ 4 files changed, 22 insertions(+), 1 deletion(-) diff --git a/apps/student/src/main/java/com/instructure/student/navigation/StudentWebViewRouter.kt b/apps/student/src/main/java/com/instructure/student/navigation/StudentWebViewRouter.kt index 6eb681b270..7ae5299c74 100644 --- a/apps/student/src/main/java/com/instructure/student/navigation/StudentWebViewRouter.kt +++ b/apps/student/src/main/java/com/instructure/student/navigation/StudentWebViewRouter.kt @@ -19,6 +19,7 @@ package com.instructure.student.navigation import androidx.fragment.app.FragmentActivity import com.instructure.canvasapi2.utils.ApiPrefs import com.instructure.pandautils.navigation.WebViewRouter +import com.instructure.student.fragment.InternalWebviewFragment import com.instructure.student.router.RouteMatcher class StudentWebViewRouter(val activity: FragmentActivity) : WebViewRouter { @@ -34,4 +35,8 @@ class StudentWebViewRouter(val activity: FragmentActivity) : WebViewRouter { override fun openMedia(url: String) { RouteMatcher.openMedia(activity, url) } + + override fun routeExternally(url: String) { + RouteMatcher.route(activity, InternalWebviewFragment.makeRoute(url, url, false, "")) + } } \ No newline at end of file diff --git a/apps/teacher/src/main/java/com/instructure/teacher/navigation/TeacherWebViewRouter.kt b/apps/teacher/src/main/java/com/instructure/teacher/navigation/TeacherWebViewRouter.kt index 2f457b39c3..07c508bb48 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/navigation/TeacherWebViewRouter.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/navigation/TeacherWebViewRouter.kt @@ -17,8 +17,12 @@ package com.instructure.teacher.navigation import androidx.fragment.app.FragmentActivity +import com.instructure.canvasapi2.models.CanvasContext import com.instructure.canvasapi2.utils.ApiPrefs +import com.instructure.interactions.router.Route import com.instructure.pandautils.navigation.WebViewRouter +import com.instructure.teacher.fragments.FullscreenInternalWebViewFragment +import com.instructure.teacher.fragments.InternalWebViewFragment import com.instructure.teacher.router.RouteMatcher class TeacherWebViewRouter(val activity: FragmentActivity) : WebViewRouter { @@ -34,4 +38,14 @@ class TeacherWebViewRouter(val activity: FragmentActivity) : WebViewRouter { override fun openMedia(url: String) { RouteMatcher.openMedia(activity, url) } + + override fun routeExternally(url: String) { + val bundle = InternalWebViewFragment.makeBundle(url, url, false, "") + RouteMatcher.route( + activity, Route( + FullscreenInternalWebViewFragment::class.java, + CanvasContext.emptyUserContext(), bundle + ) + ) + } } \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/discussion/details/DiscussionDetailsWebViewFragment.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/discussion/details/DiscussionDetailsWebViewFragment.kt index 3d706811c4..c402282c08 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/discussion/details/DiscussionDetailsWebViewFragment.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/discussion/details/DiscussionDetailsWebViewFragment.kt @@ -96,7 +96,7 @@ class DiscussionDetailsWebViewFragment : Fragment() { override fun routeInternallyCallback(url: String) { if (!webViewRouter.canRouteInternally(url, routeIfPossible = true)) { - webViewRouter.routeInternally(url) + webViewRouter.routeExternally(url) } } diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/navigation/WebViewRouter.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/navigation/WebViewRouter.kt index 2816a16f53..f577adddc1 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/navigation/WebViewRouter.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/navigation/WebViewRouter.kt @@ -23,4 +23,6 @@ interface WebViewRouter { fun routeInternally(url: String) fun openMedia(url: String) + + fun routeExternally(url: String) } \ No newline at end of file From 1c2a47ba6b56d593d8ba1c703de5e131b61aeb94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81kos=20Hermann?= Date: Thu, 23 Nov 2023 12:42:52 +0100 Subject: [PATCH 019/132] file null url error handling --- .../com/instructure/student/fragment/ParentFragment.kt | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/apps/student/src/main/java/com/instructure/student/fragment/ParentFragment.kt b/apps/student/src/main/java/com/instructure/student/fragment/ParentFragment.kt index 4cbdbeb85e..24f868fa74 100644 --- a/apps/student/src/main/java/com/instructure/student/fragment/ParentFragment.kt +++ b/apps/student/src/main/java/com/instructure/student/fragment/ParentFragment.kt @@ -60,6 +60,7 @@ import com.instructure.pandautils.utils.LoaderUtils import com.instructure.pandautils.utils.PermissionUtils import com.instructure.pandautils.utils.getDrawableCompat import com.instructure.pandautils.utils.hasPermissions +import com.instructure.pandautils.utils.toast import com.instructure.pandautils.views.EmptyView import com.instructure.student.R import com.instructure.student.activity.VideoViewActivity @@ -381,7 +382,12 @@ abstract class ParentFragment : DialogFragment(), FragmentInteractions, Navigati } onMainThread { - LoaderUtils.restartLoaderWithBundle>(LoaderManager.getInstance(owner), openMediaBundle, loaderCallbacks, R.id.openMediaLoaderID) + try { + LoaderUtils.restartLoaderWithBundle>(LoaderManager.getInstance(owner), openMediaBundle, loaderCallbacks, R.id.openMediaLoaderID) + } catch (e: Exception) { + toast(R.string.unexpectedErrorOpeningFile) + onMediaLoadingComplete() + } } } From 4fb93c6df99bd52f177d5f84df2c250f26758cf4 Mon Sep 17 00:00:00 2001 From: "kristof.nemere" Date: Thu, 23 Nov 2023 12:55:25 +0100 Subject: [PATCH 020/132] Wait for user --- .../loginapi/login/viewmodel/LoginViewModel.kt | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/libs/login-api-2/src/main/java/com/instructure/loginapi/login/viewmodel/LoginViewModel.kt b/libs/login-api-2/src/main/java/com/instructure/loginapi/login/viewmodel/LoginViewModel.kt index 6bfee40ec4..35e523ced4 100644 --- a/libs/login-api-2/src/main/java/com/instructure/loginapi/login/viewmodel/LoginViewModel.kt +++ b/libs/login-api-2/src/main/java/com/instructure/loginapi/login/viewmodel/LoginViewModel.kt @@ -24,10 +24,10 @@ import com.instructure.canvasapi2.managers.OAuthManager import com.instructure.canvasapi2.managers.UserManager import com.instructure.canvasapi2.utils.ApiPrefs import com.instructure.pandautils.mvvm.Event -import com.instructure.pandautils.utils.FEATURE_FLAG_OFFLINE import com.instructure.pandautils.utils.FeatureFlagProvider import com.instructure.pandautils.utils.NetworkStateProvider import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.delay import kotlinx.coroutines.launch import javax.inject.Inject @@ -49,6 +49,7 @@ class LoginViewModel @Inject constructor( fun checkLogin(checkToken: Boolean, checkElementary: Boolean): LiveData> { viewModelScope.launch { try { + waitForUser() val offlineEnabled = featureFlagProvider.offlineEnabled() val offlineLogin = offlineEnabled && !networkStateProvider.isOnline() if (checkToken && !offlineLogin) { @@ -70,6 +71,14 @@ class LoginViewModel @Inject constructor( return loginResultAction } + // We need to wait for the user to be set in ApiPrefs + private suspend fun waitForUser() { + repeat(30) { + if (ApiPrefs.user != null) return + delay(100) + } + } + private suspend fun checkTermsAcceptance(canvasForElementary: Boolean, offlineLogin: Boolean = false) { val authenticatedSession = oauthManager.getAuthenticatedSessionAsync("${apiPrefs.fullDomain}/users/self").await() val requiresTermsAcceptance = authenticatedSession.dataOrNull?.requiresTermsAcceptance ?: false From 8cf5726fd5ca6ea0df0b95636cb57554429e9df8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81kos=20Hermann?= Date: Thu, 23 Nov 2023 13:02:25 +0100 Subject: [PATCH 021/132] version bump --- apps/student/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/student/build.gradle b/apps/student/build.gradle index 9aa4e3023d..644403aff7 100644 --- a/apps/student/build.gradle +++ b/apps/student/build.gradle @@ -50,8 +50,8 @@ android { applicationId "com.instructure.candroid" minSdkVersion Versions.MIN_SDK targetSdkVersion Versions.TARGET_SDK - versionCode = 256 - versionName = '7.0.0' + versionCode = 257 + versionName = '7.0.1' vectorDrawables.useSupportLibrary = true multiDexEnabled = true From e30d261ad73744bacdb64288eb62a3f49121dd36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81kos=20Hermann?= Date: Thu, 23 Nov 2023 13:18:06 +0100 Subject: [PATCH 022/132] fix tests --- .../com/instructure/loginapi/login/viewmodel/LoginViewModel.kt | 2 +- .../instructure/loginapi/login/viewmodel/LoginViewModelTest.kt | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/libs/login-api-2/src/main/java/com/instructure/loginapi/login/viewmodel/LoginViewModel.kt b/libs/login-api-2/src/main/java/com/instructure/loginapi/login/viewmodel/LoginViewModel.kt index 35e523ced4..562e9cf516 100644 --- a/libs/login-api-2/src/main/java/com/instructure/loginapi/login/viewmodel/LoginViewModel.kt +++ b/libs/login-api-2/src/main/java/com/instructure/loginapi/login/viewmodel/LoginViewModel.kt @@ -74,7 +74,7 @@ class LoginViewModel @Inject constructor( // We need to wait for the user to be set in ApiPrefs private suspend fun waitForUser() { repeat(30) { - if (ApiPrefs.user != null) return + if (apiPrefs.user != null) return delay(100) } } diff --git a/libs/login-api-2/src/test/java/com/instructure/loginapi/login/viewmodel/LoginViewModelTest.kt b/libs/login-api-2/src/test/java/com/instructure/loginapi/login/viewmodel/LoginViewModelTest.kt index 7d8548de08..0e576df331 100644 --- a/libs/login-api-2/src/test/java/com/instructure/loginapi/login/viewmodel/LoginViewModelTest.kt +++ b/libs/login-api-2/src/test/java/com/instructure/loginapi/login/viewmodel/LoginViewModelTest.kt @@ -67,6 +67,8 @@ class LoginViewModelTest { fun setUp() { lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE) Dispatchers.setMain(testDispatcher) + + every { apiPrefs.user } returns mockk() } @After From 78bf26b6ca876190edccdaab7dc372c176e5ef6c Mon Sep 17 00:00:00 2001 From: Kristof Deak <92309696+kdeakinstructure@users.noreply.github.com> Date: Tue, 28 Nov 2023 09:59:31 +0100 Subject: [PATCH 023/132] [MBL-17116][Student] - Create E2E test for CourseBrowser Page Offline Unavailable menus (#2261) --- .../offline/OfflineCourseBrowserE2ETest.kt | 147 ++++++++++++++++++ .../student/ui/pages/CourseBrowserPage.kt | 16 ++ .../student/ui/pages/DashboardPage.kt | 16 ++ .../com/instructure/espresso/TestingUtils.kt | 6 +- 4 files changed, 182 insertions(+), 3 deletions(-) create mode 100644 apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineCourseBrowserE2ETest.kt diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineCourseBrowserE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineCourseBrowserE2ETest.kt new file mode 100644 index 0000000000..59244510b3 --- /dev/null +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineCourseBrowserE2ETest.kt @@ -0,0 +1,147 @@ +/* + * Copyright (C) 2023 - 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.student.ui.e2e.offline + +import android.util.Log +import androidx.test.espresso.Espresso +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.uiautomator.UiDevice +import com.instructure.canvas.espresso.OfflineE2E +import com.instructure.panda_annotations.FeatureCategory +import com.instructure.panda_annotations.Priority +import com.instructure.panda_annotations.TestCategory +import com.instructure.panda_annotations.TestMetaData +import com.instructure.student.ui.e2e.offline.utils.OfflineTestUtils +import com.instructure.student.ui.pages.CourseBrowserPage +import com.instructure.student.ui.utils.StudentTest +import com.instructure.student.ui.utils.seedData +import com.instructure.student.ui.utils.tokenLogin +import dagger.hilt.android.testing.HiltAndroidTest +import org.junit.After +import org.junit.Test +import java.lang.Thread.sleep + +@HiltAndroidTest +class OfflineCourseBrowserE2ETest : StudentTest() { + override fun displaysPageObjects() = Unit + + override fun enableAndConfigureAccessibilityChecks() = Unit + + @OfflineE2E + @Test + @TestMetaData(Priority.MANDATORY, FeatureCategory.DASHBOARD, TestCategory.E2E) + fun testOfflineCourseBrowserPageUnavailableE2E() { + + Log.d(PREPARATION_TAG,"Seeding data.") + val data = seedData(students = 1, teachers = 1, courses = 1, announcements = 1) + val student = data.studentsList[0] + val course1 = data.coursesList[0] + val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) + + Log.d(STEP_TAG,"Login with user: ${student.name}, login id: ${student.loginId}.") + tokenLogin(student) + dashboardPage.waitForRender() + + Log.d(STEP_TAG, "Open global 'Manage Offline Content' page via the more menu of the Dashboard Page.") + dashboardPage.openGlobalManageOfflineContentPage() + + Log.d(STEP_TAG, "Select the entire '${course1.name}' course for sync. Click on the 'Sync' button.") + Log.d(STEP_TAG, "Expand '${course1.name}' course. Select only the 'Announcements' of the '${course1.name}' course. Click on the 'Sync' button and confirm the sync process.") + manageOfflineContentPage.expandCollapseItem(course1.name) + manageOfflineContentPage.changeItemSelectionState("Announcements") + manageOfflineContentPage.clickOnSyncButtonAndConfirm() + + Log.d(STEP_TAG, "Wait for the 'Download Started' dashboard notification to be displayed, and the to disappear.") + dashboardPage.waitForRender() + dashboardPage.waitForSyncProgressDownloadStartedNotification() + dashboardPage.waitForSyncProgressDownloadStartedNotificationToDisappear() + + Log.d(STEP_TAG, "Wait for the 'Syncing Offline Content' dashboard notification to be displayed, and the to disappear. (It should be displayed after the 'Download Started' notification immediately.)") + dashboardPage.waitForSyncProgressStartingNotification() + dashboardPage.waitForSyncProgressStartingNotificationToDisappear() + + Log.d(PREPARATION_TAG, "Turn off the Wi-Fi and Mobile Data on the device, so it will go offline.") + OfflineTestUtils.turnOffConnectionViaADB() + + Log.d(STEP_TAG, "Wait for the Dashboard Page to be rendered. Refresh the page.") + dashboardPage.waitForRender() + + Log.d(STEP_TAG, "Select '${course1.name}' course and open 'Announcements' menu.") + sleep(5000) //Need to wait a bit here because of a UI glitch that when network state change, the dashboard page 'pops' a bit and it can confuse the automation script. + dashboardPage.selectCourse(course1) + + Log.d(STEP_TAG, "Assert that only the 'Announcements' tab is enabled because it is the only one which has been synced, and assert that all the other, previously synced tabs are disabled, because they weren't synced now.") + var enabledTabs = arrayOf("Announcements") + var disabledTabs = arrayOf("Discussions", "Grades", "People", "Syllabus", "BigBlueButton") + assertTabsEnabled(courseBrowserPage, enabledTabs) + assertTabsDisabled(courseBrowserPage, disabledTabs) + + Log.d(STEP_TAG, "Navigate back to Dashboard Page.Turn back on the Wi-Fi and Mobile Data on the device, and wait for it to come online.") + Espresso.pressBack() + OfflineTestUtils.turnOnConnectionViaADB() + dashboardPage.waitForNetworkComeBack() + + Log.d(STEP_TAG, "Open global 'Manage Offline Content' page via the more menu of the Dashboard Page.") + dashboardPage.openGlobalManageOfflineContentPage() + + Log.d(STEP_TAG, "Deselect the entire '${course1.name}' course for sync.") + manageOfflineContentPage.changeItemSelectionState(course1.name) + manageOfflineContentPage.clickOnSyncButtonAndConfirm() + + Log.d(STEP_TAG, "Wait for the 'Download Started' dashboard notification to be displayed, and the to disappear.") + dashboardPage.waitForRender() + dashboardPage.waitForSyncProgressDownloadStartedNotification() + dashboardPage.waitForSyncProgressDownloadStartedNotificationToDisappear() + + Log.d(STEP_TAG, "Wait for the 'Syncing Offline Content' dashboard notification to be displayed, and the to disappear. (It should be displayed after the 'Download Started' notification immediately.)") + dashboardPage.waitForSyncProgressStartingNotification() + dashboardPage.waitForSyncProgressStartingNotificationToDisappear() + + Log.d(PREPARATION_TAG, "Turn off the Wi-Fi and Mobile Data on the device, so it will go offline.") + OfflineTestUtils.turnOffConnectionViaADB() + + Log.d(STEP_TAG, "Select '${course1.name}' course and open 'Announcements' menu.") + sleep(10000) //Need to wait a bit here because of a UI glitch that when network state change, the dashboard page 'pops' a bit and it can confuse the automation script. + device.waitForIdle() + device.waitForWindowUpdate(null, 10000) + dashboardPage.selectCourse(course1) + + Log.d(STEP_TAG, "Assert that the 'Google Drive' and 'Collaborations' tabs are disabled because they aren't supported in offline mode, but the rest of the tabs are enabled because the whole course has been synced.") + enabledTabs = arrayOf("Announcements", "Discussions", "Grades", "People", "Syllabus", "BigBlueButton") + disabledTabs = arrayOf("Google Drive", "Collaborations") + assertTabsEnabled(courseBrowserPage, enabledTabs) + assertTabsDisabled(courseBrowserPage, disabledTabs) + } + + @After + fun tearDown() { + Log.d(PREPARATION_TAG, "Turn back on the Wi-Fi and Mobile Data on the device, so it will come back online.") + OfflineTestUtils.turnOnConnectionViaADB() + } + + private fun assertTabsEnabled(courseBrowserPage: CourseBrowserPage, tabs: Array) { + tabs.forEach { tab -> + courseBrowserPage.assertTabEnabled(tab) + } + } + + private fun assertTabsDisabled(courseBrowserPage: CourseBrowserPage, tabs: Array) { + tabs.forEach { tab -> + courseBrowserPage.assertTabDisabled(tab) + } + } +} \ No newline at end of file diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/CourseBrowserPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/CourseBrowserPage.kt index d3a9a95924..87cbc0de71 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/CourseBrowserPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/CourseBrowserPage.kt @@ -17,12 +17,15 @@ package com.instructure.student.ui.pages import android.view.View +import android.widget.LinearLayout +import androidx.constraintlayout.widget.ConstraintLayout import androidx.test.espresso.Espresso.onView import androidx.test.espresso.PerformException import androidx.test.espresso.UiController import androidx.test.espresso.ViewAction import androidx.test.espresso.action.ViewActions import androidx.test.espresso.assertion.ViewAssertions.doesNotExist +import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.matcher.ViewMatchers.* import com.instructure.canvas.espresso.scrollRecyclerView import com.instructure.canvas.espresso.withCustomConstraints @@ -34,11 +37,15 @@ import com.instructure.espresso.WaitForViewWithId import com.instructure.espresso.assertHasText import com.instructure.espresso.click import com.instructure.espresso.page.BasePage +import com.instructure.espresso.page.plus +import com.instructure.espresso.scrollTo import com.instructure.espresso.swipeUp import com.instructure.pandautils.views.SwipeRefreshLayoutAppBar import com.instructure.student.R import org.hamcrest.Matcher import org.hamcrest.Matchers.allOf +import org.hamcrest.Matchers.anyOf +import org.hamcrest.Matchers.not open class CourseBrowserPage : BasePage(R.id.courseBrowserPage) { @@ -140,6 +147,15 @@ open class CourseBrowserPage : BasePage(R.id.courseBrowserPage) { onView(allOf(withText(tabTitle), withId(R.id.label))).check(doesNotExist()) } + //OfflineMethod + fun assertTabDisabled(tabTitle: String) { + onView(allOf(anyOf(isAssignableFrom(LinearLayout::class.java), isAssignableFrom(ConstraintLayout::class.java)), withChild(anyOf(withId(R.id.label), withId(R.id.unsupportedLabel)) + withText(tabTitle)))).scrollTo().check(matches(not(isEnabled()))) + } + + fun assertTabEnabled(tabTitle: String) { + onView(allOf(anyOf(isAssignableFrom(LinearLayout::class.java), isAssignableFrom(ConstraintLayout::class.java)), withChild(anyOf(withId(R.id.label), withId(R.id.unsupportedLabel)) + withText(tabTitle)))).scrollTo().check(matches(isEnabled())) + } + // Minimizes toolbar if it is not already minimized private fun minimizeToolbar() { try { diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/DashboardPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/DashboardPage.kt index a2a1c189f4..c4d2b55143 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/DashboardPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/DashboardPage.kt @@ -354,6 +354,22 @@ class DashboardPage : BasePage(R.id.dashboardPage) { onView(withId(R.id.offlineIndicator)).check(matches(withEffectiveVisibility(Visibility.GONE))) } + //OfflineMethod + fun waitForNetworkComeBack() { + assertDisplaysCourses() + retry(times = 5, delay = 2000) { + assertOfflineIndicatorNotDisplayed() + } + } + + //OfflineMethod + fun waitForNetworkOff() { + assertDisplaysCourses() + retry(times = 5, delay = 2000) { + assertOfflineIndicatorDisplayed() + } + } + //OfflineMethod fun assertCourseOfflineSyncIconVisible(courseName: String) { waitForView(withId(R.id.offlineSyncIcon) + hasSibling(withId(R.id.titleTextView) + withText(courseName))).check(matches(withEffectiveVisibility(Visibility.VISIBLE))) diff --git a/automation/espresso/src/main/kotlin/com/instructure/espresso/TestingUtils.kt b/automation/espresso/src/main/kotlin/com/instructure/espresso/TestingUtils.kt index 3c21299ac9..101695828e 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/espresso/TestingUtils.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/espresso/TestingUtils.kt @@ -62,8 +62,8 @@ fun retry( return } catch (e: Throwable) { e.printStackTrace() + Thread.sleep(delay) } - Thread.sleep(delay) } block() } @@ -82,9 +82,9 @@ fun retryWithIncreasingDelay( return } catch (e: Throwable) { e.printStackTrace() + Thread.sleep(currentDelay) + currentDelay = (currentDelay * factor).toLong().coerceAtMost(maxDelay) } - Thread.sleep(currentDelay) - currentDelay = (currentDelay * factor).toLong().coerceAtMost(maxDelay) } block() } \ No newline at end of file From 5d54fedd78da3a91169efe2ba534c8a1434fcf6d Mon Sep 17 00:00:00 2001 From: Kristof Deak <92309696+kdeakinstructure@users.noreply.github.com> Date: Tue, 28 Nov 2023 11:43:50 +0100 Subject: [PATCH 024/132] Nightly - Stabilize SpeedGrader and Todo E2E tests. (#2262) --- .../com/instructure/student/ui/e2e/InboxE2ETest.kt | 4 ++-- .../com/instructure/student/ui/e2e/TodoE2ETest.kt | 12 +++++++----- .../com/instructure/teacher/ui/e2e/InboxE2ETest.kt | 10 +++++----- .../instructure/teacher/ui/e2e/SpeedGraderE2ETest.kt | 11 +++++++++-- .../kotlin/com/instructure/espresso/TestingUtils.kt | 4 ++++ 5 files changed, 27 insertions(+), 14 deletions(-) diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/InboxE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/InboxE2ETest.kt index 35baba10c0..0f5b709514 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/InboxE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/InboxE2ETest.kt @@ -131,7 +131,7 @@ class InboxE2ETest: StudentTest() { inboxPage.assertSelectedConversationNumber("1") inboxPage.clickUnstar() - retryWithIncreasingDelay(times = 10, maxDelay = 3000) { + retryWithIncreasingDelay(times = 10, maxDelay = 3000, catchBlock = { refresh() }) { inboxPage.assertConversationNotStarred(seededConversation.subject) } @@ -168,7 +168,7 @@ class InboxE2ETest: StudentTest() { inboxPage.selectConversations(listOf(seededConversation.subject)) inboxPage.clickUnArchive() - retryWithIncreasingDelay(times = 10, maxDelay = 3000) { + retryWithIncreasingDelay(times = 10, maxDelay = 3000, catchBlock = { refresh() }) { inboxPage.assertConversationNotDisplayed(seededConversation.subject) } diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/TodoE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/TodoE2ETest.kt index 2c98efb118..4968c7105f 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/TodoE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/TodoE2ETest.kt @@ -15,6 +15,7 @@ import com.instructure.dataseeding.model.SubmissionType import com.instructure.dataseeding.util.days import com.instructure.dataseeding.util.fromNow import com.instructure.dataseeding.util.iso8601 +import com.instructure.espresso.retry import com.instructure.panda_annotations.FeatureCategory import com.instructure.panda_annotations.Priority import com.instructure.panda_annotations.TestCategory @@ -71,12 +72,13 @@ class TodoE2ETest: StudentTest() { dashboardPage.clickTodoTab() Log.d(STEP_TAG,"Assert that ${testAssignment.name} assignment is displayed and ${borderDateAssignment.name} is displayed because it's 7 days away from now..") - todoPage.assertAssignmentDisplayed(testAssignment) - todoPage.assertAssignmentDisplayed(borderDateAssignment) - Log.d(STEP_TAG,"Assert that ${quiz.title} quiz is displayed and ${tooFarAwayQuiz.title} quiz is not displayed because it's end date is more than a week away..") - todoPage.assertQuizDisplayed(quiz) - todoPage.assertQuizNotDisplayed(tooFarAwayQuiz) + retry(times = 5, delay = 3000, catchBlock = { refresh() } ) { + todoPage.assertAssignmentDisplayed(testAssignment) + todoPage.assertAssignmentDisplayed(borderDateAssignment) + todoPage.assertQuizDisplayed(quiz) + todoPage.assertQuizNotDisplayed(tooFarAwayQuiz) + } Log.d(PREPARATION_TAG,"Submit ${testAssignment.name} assignment for ${student.name} student.") SubmissionsApi.seedAssignmentSubmission(SubmissionsApi.SubmissionSeedRequest( diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/InboxE2ETest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/InboxE2ETest.kt index 3fe3c27741..4bcbed23a2 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/InboxE2ETest.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/InboxE2ETest.kt @@ -222,10 +222,10 @@ class InboxE2ETest : TeacherTest() { Log.d(STEP_TAG, "Select 'ARCHIVED' scope and assert that '${seedConversation2[0].subject}' conversation is displayed in the 'ARCHIVED' scope.") inboxPage.filterMessageScope("Archived") - retry(times = 10, delay = 3000) { + retry(times = 10, delay = 3000, block = { refresh() inboxPage.assertConversationDisplayed(seedConversation2[0].subject) - } + }) Log.d(STEP_TAG, "Select '${seedConversation2[0].subject}' conversation and unarchive it." + "Assert that the selected number of conversation on the toolbar is 1 and '${seedConversation2[0].subject}' conversation is not displayed in the 'ARCHIVED' scope.") @@ -381,10 +381,10 @@ class InboxE2ETest : TeacherTest() { inboxPage.selectConversations(listOf(seedConversation2[0].subject, seedConversation3[0].subject)) inboxPage.clickMarkAsRead() - retry(times = 10, delay = 3000) { + retry(times = 10, delay = 3000, block = { Log.d(STEP_TAG, "Assert that '${seedConversation3[0].subject}' conversation is read.") inboxPage.assertUnreadMarkerVisibility(seedConversation3[0].subject, ViewMatchers.Visibility.GONE) - } + }) Log.d(STEP_TAG, "Select both of the conversations. Star them and mark the unread.") inboxPage.clickStar() @@ -392,7 +392,7 @@ class InboxE2ETest : TeacherTest() { Log.d(STEP_TAG, "Navigate to 'STARRED' scope. Assert that both of the conversation are displayed in the 'STARRED' scope.") inboxPage.filterMessageScope("Starred") - retryWithIncreasingDelay(times = 10, maxDelay = 3000) { + retryWithIncreasingDelay(times = 10, maxDelay = 3000, catchBlock = { refresh() }) { inboxPage.assertConversationDisplayed(seedConversation2[0].subject) inboxPage.assertConversationDisplayed(seedConversation3[0].subject) } diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/SpeedGraderE2ETest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/SpeedGraderE2ETest.kt index 175815be85..cd33a35bd1 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/SpeedGraderE2ETest.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/SpeedGraderE2ETest.kt @@ -33,12 +33,17 @@ import com.instructure.dataseeding.model.SubmissionType import com.instructure.dataseeding.util.days import com.instructure.dataseeding.util.fromNow import com.instructure.dataseeding.util.iso8601 +import com.instructure.espresso.retry import com.instructure.panda_annotations.FeatureCategory import com.instructure.panda_annotations.Priority import com.instructure.panda_annotations.TestCategory import com.instructure.panda_annotations.TestMetaData import com.instructure.teacher.R -import com.instructure.teacher.ui.utils.* +import com.instructure.teacher.ui.utils.TeacherTest +import com.instructure.teacher.ui.utils.seedAssignmentSubmission +import com.instructure.teacher.ui.utils.seedAssignments +import com.instructure.teacher.ui.utils.seedData +import com.instructure.teacher.ui.utils.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test @@ -153,7 +158,9 @@ class SpeedGraderE2ETest : TeacherTest() { assignmentSubmissionListPage.clickFilterDialogOk() Log.d(STEP_TAG,"Assert that there is one submission displayed.") - assignmentSubmissionListPage.assertHasSubmission(1) + retry(times = 5, delay = 3000, catchBlock = { refresh() }) { + assignmentSubmissionListPage.assertHasSubmission(1) + } Log.d(STEP_TAG, "Navigate back assignment's details page.") Espresso.pressBack() diff --git a/automation/espresso/src/main/kotlin/com/instructure/espresso/TestingUtils.kt b/automation/espresso/src/main/kotlin/com/instructure/espresso/TestingUtils.kt index 101695828e..2901820aae 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/espresso/TestingUtils.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/espresso/TestingUtils.kt @@ -54,6 +54,7 @@ fun getCurrentDateInCanvasFormat(): String { fun retry( times: Int = 3, delay: Long = 1000, + catchBlock: (() -> Unit)? = null, block: () -> Unit ) { repeat(times - 1) { @@ -63,6 +64,7 @@ fun retry( } catch (e: Throwable) { e.printStackTrace() Thread.sleep(delay) + catchBlock?.invoke() } } block() @@ -73,6 +75,7 @@ fun retryWithIncreasingDelay( initialDelay: Long = 100, maxDelay: Long = 1000, factor: Double = 2.0, + catchBlock: (() -> Unit)? = null, block: () -> Unit ) { var currentDelay = initialDelay @@ -84,6 +87,7 @@ fun retryWithIncreasingDelay( e.printStackTrace() Thread.sleep(currentDelay) currentDelay = (currentDelay * factor).toLong().coerceAtMost(maxDelay) + catchBlock?.invoke() } } block() From dcebcb47e68781f6d5fb78f59ee96ebb9af012c6 Mon Sep 17 00:00:00 2001 From: Kristof Deak <92309696+kdeakinstructure@users.noreply.github.com> Date: Tue, 28 Nov 2023 12:13:07 +0100 Subject: [PATCH 025/132] Nightly - Offline - Attempt to fix CI UnknownHostException. (#2264) --- .../offline/ManageOfflineContentE2ETest.kt | 5 +- .../offline/OfflineCourseBrowserE2ETest.kt | 9 +- .../ui/e2e/offline/OfflineDashboardE2ETest.kt | 6 +- .../e2e/offline/OfflineSyncProgressE2ETest.kt | 4 +- .../e2e/offline/OfflineSyncSettingsE2ETest.kt | 3 +- .../ui/e2e/offline/utils/OfflineTestUtils.kt | 18 -- automation/espresso/build.gradle | 2 +- .../espresso/src/main/AndroidManifest.xml | 2 + .../instructure/canvas/espresso/CanvasTest.kt | 181 +++++++++++------- 9 files changed, 132 insertions(+), 98 deletions(-) diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/ManageOfflineContentE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/ManageOfflineContentE2ETest.kt index aa8a2108a5..3d300a2e1a 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/ManageOfflineContentE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/ManageOfflineContentE2ETest.kt @@ -24,7 +24,6 @@ import com.instructure.panda_annotations.FeatureCategory import com.instructure.panda_annotations.Priority import com.instructure.panda_annotations.TestCategory import com.instructure.panda_annotations.TestMetaData -import com.instructure.student.ui.e2e.offline.utils.OfflineTestUtils import com.instructure.student.ui.utils.StudentTest import com.instructure.student.ui.utils.seedData import com.instructure.student.ui.utils.tokenLogin @@ -197,7 +196,7 @@ class ManageOfflineContentE2ETest : StudentTest() { dashboardPage.waitForSyncProgressStartingNotificationToDisappear() Log.d(PREPARATION_TAG, "Turn off the Wi-Fi and Mobile Data on the device, so it will go offline.") - OfflineTestUtils.turnOffConnectionViaADB() + turnOffConnectionViaADB() dashboardPage.waitForRender() Log.d(STEP_TAG, "Select '${course2.name}' course and open 'Grades' menu to check if it's really synced and can be seen in offline mode.") @@ -211,7 +210,7 @@ class ManageOfflineContentE2ETest : StudentTest() { @After fun tearDown() { Log.d(PREPARATION_TAG, "Turn back on the Wi-Fi and Mobile Data on the device via ADB, so it will come back online.") - OfflineTestUtils.turnOnConnectionViaADB() + turnOnConnectionViaADB() } } \ No newline at end of file diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineCourseBrowserE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineCourseBrowserE2ETest.kt index 59244510b3..04a6fc4337 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineCourseBrowserE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineCourseBrowserE2ETest.kt @@ -25,7 +25,6 @@ import com.instructure.panda_annotations.FeatureCategory import com.instructure.panda_annotations.Priority import com.instructure.panda_annotations.TestCategory import com.instructure.panda_annotations.TestMetaData -import com.instructure.student.ui.e2e.offline.utils.OfflineTestUtils import com.instructure.student.ui.pages.CourseBrowserPage import com.instructure.student.ui.utils.StudentTest import com.instructure.student.ui.utils.seedData @@ -75,7 +74,7 @@ class OfflineCourseBrowserE2ETest : StudentTest() { dashboardPage.waitForSyncProgressStartingNotificationToDisappear() Log.d(PREPARATION_TAG, "Turn off the Wi-Fi and Mobile Data on the device, so it will go offline.") - OfflineTestUtils.turnOffConnectionViaADB() + turnOffConnectionViaADB() Log.d(STEP_TAG, "Wait for the Dashboard Page to be rendered. Refresh the page.") dashboardPage.waitForRender() @@ -92,7 +91,7 @@ class OfflineCourseBrowserE2ETest : StudentTest() { Log.d(STEP_TAG, "Navigate back to Dashboard Page.Turn back on the Wi-Fi and Mobile Data on the device, and wait for it to come online.") Espresso.pressBack() - OfflineTestUtils.turnOnConnectionViaADB() + turnOnConnectionViaADB() dashboardPage.waitForNetworkComeBack() Log.d(STEP_TAG, "Open global 'Manage Offline Content' page via the more menu of the Dashboard Page.") @@ -112,7 +111,7 @@ class OfflineCourseBrowserE2ETest : StudentTest() { dashboardPage.waitForSyncProgressStartingNotificationToDisappear() Log.d(PREPARATION_TAG, "Turn off the Wi-Fi and Mobile Data on the device, so it will go offline.") - OfflineTestUtils.turnOffConnectionViaADB() + turnOffConnectionViaADB() Log.d(STEP_TAG, "Select '${course1.name}' course and open 'Announcements' menu.") sleep(10000) //Need to wait a bit here because of a UI glitch that when network state change, the dashboard page 'pops' a bit and it can confuse the automation script. @@ -130,7 +129,7 @@ class OfflineCourseBrowserE2ETest : StudentTest() { @After fun tearDown() { Log.d(PREPARATION_TAG, "Turn back on the Wi-Fi and Mobile Data on the device, so it will come back online.") - OfflineTestUtils.turnOnConnectionViaADB() + turnOnConnectionViaADB() } private fun assertTabsEnabled(courseBrowserPage: CourseBrowserPage, tabs: Array) { diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineDashboardE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineDashboardE2ETest.kt index 309f23f989..21a3fa625b 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineDashboardE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineDashboardE2ETest.kt @@ -75,7 +75,7 @@ class OfflineDashboardE2ETest : StudentTest() { dashboardPage.waitForSyncProgressStartingNotificationToDisappear() Log.d(PREPARATION_TAG, "Turn off the Wi-Fi and Mobile Data on the device, so it will go offline.") - OfflineTestUtils.turnOffConnectionViaADB() + turnOffConnectionViaADB() device.waitForIdle() Log.d(STEP_TAG, "Wait for the Dashboard Page to be rendered. Refresh the page.") @@ -130,7 +130,7 @@ class OfflineDashboardE2ETest : StudentTest() { dashboardPage.waitForSyncProgressStartingNotificationToDisappear() Log.d(PREPARATION_TAG, "Turn off the Wi-Fi and Mobile Data on the device, so it will go offline.") - OfflineTestUtils.turnOffConnectionViaADB() + turnOffConnectionViaADB() device.waitForIdle() Log.d(STEP_TAG, "Wait for the Dashboard Page to be rendered. Refresh the page.") @@ -157,7 +157,7 @@ class OfflineDashboardE2ETest : StudentTest() { @After fun tearDown() { Log.d(PREPARATION_TAG, "Turn back on the Wi-Fi and Mobile Data on the device, so it will come back online.") - OfflineTestUtils.turnOnConnectionViaADB() + turnOnConnectionViaADB() } } \ No newline at end of file diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineSyncProgressE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineSyncProgressE2ETest.kt index 77cea19bc2..26637e2f1a 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineSyncProgressE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineSyncProgressE2ETest.kt @@ -80,7 +80,7 @@ class OfflineSyncProgressE2ETest : StudentTest() { Espresso.pressBack() Log.d(PREPARATION_TAG, "Turn off the Wi-Fi and Mobile Data on the device, so it will go offline.") - OfflineTestUtils.turnOffConnectionViaADB() + turnOffConnectionViaADB() dashboardPage.waitForRender() Log.d(STEP_TAG, "Assert that the Offline Indicator (bottom banner) is displayed on the Dashboard Page.") @@ -101,7 +101,7 @@ class OfflineSyncProgressE2ETest : StudentTest() { @After fun tearDown() { Log.d(PREPARATION_TAG, "Turn back on the Wi-Fi and Mobile Data on the device via ADB, so it will come back online.") - OfflineTestUtils.turnOnConnectionViaADB() + turnOnConnectionViaADB() } } \ No newline at end of file diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineSyncSettingsE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineSyncSettingsE2ETest.kt index fac7879901..107d91c4c6 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineSyncSettingsE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineSyncSettingsE2ETest.kt @@ -23,7 +23,6 @@ import com.instructure.panda_annotations.Priority import com.instructure.panda_annotations.TestCategory import com.instructure.panda_annotations.TestMetaData import com.instructure.pandautils.R -import com.instructure.student.ui.e2e.offline.utils.OfflineTestUtils import com.instructure.student.ui.utils.StudentTest import com.instructure.student.ui.utils.ViewUtils import com.instructure.student.ui.utils.seedData @@ -139,7 +138,7 @@ class OfflineSyncSettingsE2ETest : StudentTest() { @After fun tearDown() { Log.d(PREPARATION_TAG, "Turn back on the Wi-Fi and Mobile Data on the device via ADB, so it will come back online.") - OfflineTestUtils.turnOnConnectionViaADB() + turnOnConnectionViaADB() } } \ No newline at end of file diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/utils/OfflineTestUtils.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/utils/OfflineTestUtils.kt index 7cd51b7a66..0696eec9b3 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/utils/OfflineTestUtils.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/utils/OfflineTestUtils.kt @@ -31,24 +31,6 @@ import org.hamcrest.CoreMatchers.allOf object OfflineTestUtils { - private const val ENABLE_WIFI_COMMAND: String = "svc wifi enable" - private const val DISABLE_WIFI_COMMAND: String = "svc wifi disable" - - private const val ENABLE_MOBILE_DATA_COMMAND: String = "svc data enable" - private const val DISABLE_MOBILE_DATA_COMMAND: String = "svc data disable" - - fun turnOffConnectionViaADB() { - val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) - device.executeShellCommand(DISABLE_WIFI_COMMAND) - device.executeShellCommand(DISABLE_MOBILE_DATA_COMMAND) - } - - fun turnOnConnectionViaADB() { - val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) - device.executeShellCommand(ENABLE_WIFI_COMMAND) - device.executeShellCommand(ENABLE_MOBILE_DATA_COMMAND) - } - fun turnOffConnectionOnUI() { val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) diff --git a/automation/espresso/build.gradle b/automation/espresso/build.gradle index 3439d3cc76..195d693576 100644 --- a/automation/espresso/build.gradle +++ b/automation/espresso/build.gradle @@ -45,7 +45,7 @@ android { buildToolsVersion '28.0.3' defaultConfig { - minSdkVersion 17 + minSdkVersion 24 targetSdkVersion 28 } diff --git a/automation/espresso/src/main/AndroidManifest.xml b/automation/espresso/src/main/AndroidManifest.xml index 649274d1a5..a6af28ef54 100644 --- a/automation/espresso/src/main/AndroidManifest.xml +++ b/automation/espresso/src/main/AndroidManifest.xml @@ -20,6 +20,8 @@ package="com.instructure.espresso"> + + diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/CanvasTest.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/CanvasTest.kt index 185d7aa675..adf8d1f501 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/CanvasTest.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/CanvasTest.kt @@ -21,6 +21,9 @@ import android.app.Activity import android.content.Context import android.content.res.Configuration import android.content.res.Resources +import android.net.ConnectivityManager +import android.net.Network +import android.net.NetworkCapabilities import android.os.Build import android.os.Environment import android.util.DisplayMetrics @@ -31,6 +34,7 @@ import androidx.test.core.app.ApplicationProvider import androidx.test.espresso.matcher.ViewMatchers import androidx.test.platform.app.InstrumentationRegistry import androidx.test.rule.GrantPermissionRule +import androidx.test.uiautomator.UiDevice import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckResultUtils.matchesCheckNames import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckResultUtils.matchesViews import com.google.android.apps.common.testing.accessibility.framework.AccessibilityViewCheckResult @@ -74,6 +78,8 @@ abstract class CanvasTest : InstructureTestingContract { var extraAccessibilitySupressions: Matcher? = Matchers.anyOf() + val connectivityManager = InstrumentationRegistry.getInstrumentation().context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager + @Rule(order = 1) override fun chain(): TestRule { return RuleChain @@ -107,70 +113,20 @@ abstract class CanvasTest : InstructureTestingContract { // Only continue if we're on Bitrise // (More accurately, if we are on FTL launched from Bitrise.) if(splunkToken != null && !splunkToken.isEmpty()) { - val bitriseWorkflow = InstrumentationRegistry.getArguments().getString("BITRISE_TRIGGERED_WORKFLOW_ID") - val bitriseApp = InstrumentationRegistry.getArguments().getString("BITRISE_APP_TITLE") - val bitriseBranch = InstrumentationRegistry.getArguments().getString("BITRISE_GIT_BRANCH") - val bitriseBuildNumber = InstrumentationRegistry.getArguments().getString("BITRISE_BUILD_NUMBER") - - val eventObject = JSONObject() - eventObject.put("workflow", bitriseWorkflow) - eventObject.put("branch", bitriseBranch) - eventObject.put("bitriseApp", bitriseApp) - eventObject.put("status", disposition) - eventObject.put("testName", testMethod) - eventObject.put("testClass", testClass) - eventObject.put("stackTrace", error.stackTrace.take(15).joinToString(", ")) - eventObject.put("osVersion", Build.VERSION.SDK_INT.toString()) - // Limit our error message to 4096 chars; they can be unreasonably long (e.g., 137K!) when - // they contain a view hierarchy, and there is typically not much useful info after the - // first few lines. - eventObject.put("message", error.toString().take(4096)) - - val payloadObject = JSONObject() - payloadObject.put("sourcetype", "mobile-android-qa-testresult") - payloadObject.put("event", eventObject) - - val payload = payloadObject.toString() - Log.d("CanvasTest", "payload = $payload") - - // Can't run a curl command from FTL, so let's do this the hard way - var os : OutputStream? = null - var inputStream : InputStream? = null - var conn : HttpURLConnection? = null - - try { - - // Set up our url/connection - val url = URL("https://http-inputs-inst.splunkcloud.com:443/services/collector") - conn = url.openConnection() as HttpURLConnection - conn.requestMethod = "POST" - conn.setRequestProperty("Authorization", "Splunk $splunkToken") - conn.setRequestProperty("Content-Type", "application/json; utf-8") - conn.setRequestProperty("Accept", "application/json") - conn.setDoInput(true) - conn.setDoOutput(true) - - // Connect - conn.connect() - - // Send out our post body - os = BufferedOutputStream(conn.outputStream) - os.write(payload.toByteArray()) - os.flush() - - // Report the result summary - Log.d("CanvasTest", "Response code: ${conn.responseCode}, message: ${conn.responseMessage}") - - // Report the splunk result JSON - inputStream = conn.inputStream - val content = inputStream.bufferedReader().use(BufferedReader::readText) - Log.d("CanvasTest", "Response: $content") - } - finally { - // Clean up our mess - if(os != null) os.close() - if(inputStream != null) inputStream.close() - if(conn != null) conn.disconnect() + val networkCapabilities = connectivityManager.getNetworkCapabilities(connectivityManager.activeNetwork) + val hasActiveNetwork = networkCapabilities?.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) ?: false + + if (hasActiveNetwork) { + reportToSplunk(disposition, testMethod, testClass, error, splunkToken) + } else { + turnOnConnectionViaADB() + connectivityManager.registerDefaultNetworkCallback(object : ConnectivityManager.NetworkCallback() { + override fun onAvailable(network: Network) { + super.onAvailable(network) + connectivityManager.unregisterNetworkCallback(this) + reportToSplunk(disposition, testMethod, testClass, error, splunkToken) + } + }) } } @@ -181,6 +137,84 @@ abstract class CanvasTest : InstructureTestingContract { } + private fun reportToSplunk( + disposition: String, + testMethod: String, + testClass: String, + error: Throwable, + splunkToken: String? + ) { + val bitriseWorkflow = + InstrumentationRegistry.getArguments().getString("BITRISE_TRIGGERED_WORKFLOW_ID") + val bitriseApp = InstrumentationRegistry.getArguments().getString("BITRISE_APP_TITLE") + val bitriseBranch = InstrumentationRegistry.getArguments().getString("BITRISE_GIT_BRANCH") + val bitriseBuildNumber = + InstrumentationRegistry.getArguments().getString("BITRISE_BUILD_NUMBER") + + val eventObject = JSONObject() + eventObject.put("workflow", bitriseWorkflow) + eventObject.put("branch", bitriseBranch) + eventObject.put("bitriseApp", bitriseApp) + eventObject.put("status", disposition) + eventObject.put("testName", testMethod) + eventObject.put("testClass", testClass) + eventObject.put("stackTrace", error.stackTrace.take(15).joinToString(", ")) + eventObject.put("osVersion", Build.VERSION.SDK_INT.toString()) + // Limit our error message to 4096 chars; they can be unreasonably long (e.g., 137K!) when + // they contain a view hierarchy, and there is typically not much useful info after the + // first few lines. + eventObject.put("message", error.toString().take(4096)) + + val payloadObject = JSONObject() + payloadObject.put("sourcetype", "mobile-android-qa-testresult") + payloadObject.put("event", eventObject) + + val payload = payloadObject.toString() + Log.d("CanvasTest", "payload = $payload") + + // Can't run a curl command from FTL, so let's do this the hard way + var os: OutputStream? = null + var inputStream: InputStream? = null + var conn: HttpURLConnection? = null + + try { + + // Set up our url/connection + val url = URL("https://http-inputs-inst.splunkcloud.com:443/services/collector") + conn = url.openConnection() as HttpURLConnection + conn.requestMethod = "POST" + conn.setRequestProperty("Authorization", "Splunk $splunkToken") + conn.setRequestProperty("Content-Type", "application/json; utf-8") + conn.setRequestProperty("Accept", "application/json") + conn.setDoInput(true) + conn.setDoOutput(true) + + // Connect + conn.connect() + + // Send out our post body + os = BufferedOutputStream(conn.outputStream) + os.write(payload.toByteArray()) + os.flush() + + // Report the result summary + Log.d( + "CanvasTest", + "Response code: ${conn.responseCode}, message: ${conn.responseMessage}" + ) + + // Report the splunk result JSON + inputStream = conn.inputStream + val content = inputStream.bufferedReader().use(BufferedReader::readText) + Log.d("CanvasTest", "Response: $content") + } finally { + // Clean up our mess + if (os != null) os.close() + if (inputStream != null) inputStream.close() + if (conn != null) conn.disconnect() + } + } + // Creates an /sdcard/coverage folder if it does not already exist. // This is necessary for us to generate/process code coverage data. private fun setupCoverageFolder() { @@ -464,6 +498,12 @@ abstract class CanvasTest : InstructureTestingContract { private var configChecked = false + private const val ENABLE_WIFI_COMMAND: String = "svc wifi enable" + private const val DISABLE_WIFI_COMMAND: String = "svc wifi disable" + + private const val ENABLE_MOBILE_DATA_COMMAND: String = "svc data enable" + private const val DISABLE_MOBILE_DATA_COMMAND: String = "svc data disable" + private fun getDeviceOrientation(context: Context): Int { val configuration = context.resources.configuration return configuration.orientation @@ -481,6 +521,19 @@ abstract class CanvasTest : InstructureTestingContract { fun isLowResDevice() : Boolean { return ApplicationProvider.getApplicationContext().resources.displayMetrics.densityDpi < DisplayMetrics.DENSITY_HIGH } + + fun turnOffConnectionViaADB() { + val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) + device.executeShellCommand(DISABLE_WIFI_COMMAND) + device.executeShellCommand(DISABLE_MOBILE_DATA_COMMAND) + } + + fun turnOnConnectionViaADB() { + val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) + device.executeShellCommand(ENABLE_WIFI_COMMAND) + device.executeShellCommand(ENABLE_MOBILE_DATA_COMMAND) + } + } } From 2bbc135e8a46c6aaada3e4cfdbfca4decc945604 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81kos=20Hermann?= Date: Wed, 29 Nov 2023 11:12:26 +0100 Subject: [PATCH 026/132] fix login crash --- .../student/di/ApplicationModule.kt | 36 +++++++++++++++++++ .../student/util/StudentLogoutHelper.kt | 30 ++++++++++++++++ .../teacher/di/ApplicationModule.kt | 8 ++++- .../teacher/utils/TeacherLogoutHelper.kt | 31 ++++++++++++++++ .../instructure/canvasapi2/utils/ApiPrefs.kt | 4 +-- .../instructure/canvasapi2/utils/PrefUtils.kt | 11 +++++- .../login/viewmodel/LoginViewModel.kt | 9 ----- .../di/OfflineDatabaseProviderModule.kt | 6 ++-- .../room/offline/OfflineDatabaseProvider.kt | 20 +++++++++-- .../pandautils/utils/LogoutHelper.kt | 26 ++++++++++++++ 10 files changed, 164 insertions(+), 17 deletions(-) create mode 100644 apps/student/src/main/java/com/instructure/student/di/ApplicationModule.kt create mode 100644 apps/student/src/main/java/com/instructure/student/util/StudentLogoutHelper.kt create mode 100644 apps/teacher/src/main/java/com/instructure/teacher/utils/TeacherLogoutHelper.kt create mode 100644 libs/pandautils/src/main/java/com/instructure/pandautils/utils/LogoutHelper.kt diff --git a/apps/student/src/main/java/com/instructure/student/di/ApplicationModule.kt b/apps/student/src/main/java/com/instructure/student/di/ApplicationModule.kt new file mode 100644 index 0000000000..38d3f681b2 --- /dev/null +++ b/apps/student/src/main/java/com/instructure/student/di/ApplicationModule.kt @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2023 - 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.student.di + +import com.instructure.pandautils.utils.LogoutHelper +import com.instructure.student.util.StudentLogoutHelper +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent + +@Module +@InstallIn(SingletonComponent::class) +class ApplicationModule { + + @Provides + fun provideLogoutHelper(): LogoutHelper { + return StudentLogoutHelper() + } +} \ No newline at end of file diff --git a/apps/student/src/main/java/com/instructure/student/util/StudentLogoutHelper.kt b/apps/student/src/main/java/com/instructure/student/util/StudentLogoutHelper.kt new file mode 100644 index 0000000000..620eb7d127 --- /dev/null +++ b/apps/student/src/main/java/com/instructure/student/util/StudentLogoutHelper.kt @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2023 - 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.student.util + +import com.instructure.loginapi.login.tasks.LogoutTask +import com.instructure.pandautils.room.offline.DatabaseProvider +import com.instructure.pandautils.utils.LogoutHelper +import com.instructure.student.tasks.StudentLogoutTask + +class StudentLogoutHelper : LogoutHelper { + override fun logout(databaseProvider: DatabaseProvider) { + StudentLogoutTask(LogoutTask.Type.LOGOUT, databaseProvider = databaseProvider).execute() + } +} \ No newline at end of file diff --git a/apps/teacher/src/main/java/com/instructure/teacher/di/ApplicationModule.kt b/apps/teacher/src/main/java/com/instructure/teacher/di/ApplicationModule.kt index c4cd8b0482..dba25af48c 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/di/ApplicationModule.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/di/ApplicationModule.kt @@ -16,12 +16,13 @@ */ package com.instructure.teacher.di +import com.instructure.pandautils.utils.LogoutHelper +import com.instructure.teacher.utils.TeacherLogoutHelper import com.instructure.teacher.utils.TeacherPrefs import dagger.Module import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent -import javax.inject.Singleton /** * Module that provides all the application scope dependencies, that are not related to other module. @@ -34,4 +35,9 @@ class ApplicationModule { fun provideTeacherPrefs(): TeacherPrefs { return TeacherPrefs } + + @Provides + fun provideLogoutHelper(): LogoutHelper { + return TeacherLogoutHelper() + } } \ No newline at end of file diff --git a/apps/teacher/src/main/java/com/instructure/teacher/utils/TeacherLogoutHelper.kt b/apps/teacher/src/main/java/com/instructure/teacher/utils/TeacherLogoutHelper.kt new file mode 100644 index 0000000000..aa905c8d21 --- /dev/null +++ b/apps/teacher/src/main/java/com/instructure/teacher/utils/TeacherLogoutHelper.kt @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2023 - 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.teacher.utils + +import com.instructure.loginapi.login.tasks.LogoutTask +import com.instructure.pandautils.room.offline.DatabaseProvider +import com.instructure.pandautils.utils.LogoutHelper +import com.instructure.teacher.tasks.TeacherLogoutTask + +class TeacherLogoutHelper : LogoutHelper { + + override fun logout(databaseProvider: DatabaseProvider) { + TeacherLogoutTask(LogoutTask.Type.LOGOUT).execute() + } +} \ No newline at end of file diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/utils/ApiPrefs.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/utils/ApiPrefs.kt index 33d12155f4..d7fcb748d6 100644 --- a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/utils/ApiPrefs.kt +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/utils/ApiPrefs.kt @@ -65,7 +65,7 @@ object ApiPrefs : PrefManager(PREFERENCE_FILE_NAME) { /* Non-masquerading Prefs */ internal var originalDomain by StringPref("", "domain") - private var originalUser: User? by GsonPref(User::class.java, null, "user") + private var originalUser: User? by GsonPref(User::class.java, null, "user", false) var selectedLocale by StringPref(ACCOUNT_LOCALE) @@ -84,7 +84,7 @@ object ApiPrefs : PrefManager(PREFERENCE_FILE_NAME) { var isMasqueradingFromQRCode by BooleanPref() var masqueradeId by LongPref(-1L) internal var masqueradeDomain by StringPref() - internal var masqueradeUser: User? by GsonPref(User::class.java, null, "masq-user") + internal var masqueradeUser: User? by GsonPref(User::class.java, null, "masq-user", false) // Used to determine if a student can generate a pairing code, saved during splash var canGeneratePairingCode by NBooleanPref() diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/utils/PrefUtils.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/utils/PrefUtils.kt index c836664ff0..eed66ffbfd 100644 --- a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/utils/PrefUtils.kt +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/utils/PrefUtils.kt @@ -390,7 +390,8 @@ class ColorPref(@ColorRes defaultValue: Int, keyName: String? = null) : Pref( private val klazz: Class, defaultValue: T? = null, - keyName: String? = null + private val keyName: String? = null, + private val async: Boolean = true ) : Pref(defaultValue, keyName) { private var cachedObject: T? = null @@ -413,6 +414,14 @@ class GsonPref( } return this } + + override fun setValue(thisRef: PrefManager, property: KProperty<*>, value: T?) { + if (async) { + super.setValue(thisRef, property, value) + } else { + thisRef.editor.setValue(keyName ?: property.name, value).commit() + } + } } /** diff --git a/libs/login-api-2/src/main/java/com/instructure/loginapi/login/viewmodel/LoginViewModel.kt b/libs/login-api-2/src/main/java/com/instructure/loginapi/login/viewmodel/LoginViewModel.kt index 562e9cf516..3cbfbd4573 100644 --- a/libs/login-api-2/src/main/java/com/instructure/loginapi/login/viewmodel/LoginViewModel.kt +++ b/libs/login-api-2/src/main/java/com/instructure/loginapi/login/viewmodel/LoginViewModel.kt @@ -49,7 +49,6 @@ class LoginViewModel @Inject constructor( fun checkLogin(checkToken: Boolean, checkElementary: Boolean): LiveData> { viewModelScope.launch { try { - waitForUser() val offlineEnabled = featureFlagProvider.offlineEnabled() val offlineLogin = offlineEnabled && !networkStateProvider.isOnline() if (checkToken && !offlineLogin) { @@ -71,14 +70,6 @@ class LoginViewModel @Inject constructor( return loginResultAction } - // We need to wait for the user to be set in ApiPrefs - private suspend fun waitForUser() { - repeat(30) { - if (apiPrefs.user != null) return - delay(100) - } - } - private suspend fun checkTermsAcceptance(canvasForElementary: Boolean, offlineLogin: Boolean = false) { val authenticatedSession = oauthManager.getAuthenticatedSessionAsync("${apiPrefs.fullDomain}/users/self").await() val requiresTermsAcceptance = authenticatedSession.dataOrNull?.requiresTermsAcceptance ?: false diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/di/OfflineDatabaseProviderModule.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/di/OfflineDatabaseProviderModule.kt index fea3f2b3d2..9f902d14a0 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/di/OfflineDatabaseProviderModule.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/di/OfflineDatabaseProviderModule.kt @@ -19,8 +19,10 @@ package com.instructure.pandautils.di import android.content.Context +import com.google.firebase.crashlytics.FirebaseCrashlytics import com.instructure.pandautils.room.offline.DatabaseProvider import com.instructure.pandautils.room.offline.OfflineDatabaseProvider +import com.instructure.pandautils.utils.LogoutHelper import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -34,7 +36,7 @@ class OfflineDatabaseProviderModule { @Provides @Singleton - fun provideOfflineDatabaseProvider(@ApplicationContext context: Context): DatabaseProvider { - return OfflineDatabaseProvider(context) + fun provideOfflineDatabaseProvider(@ApplicationContext context: Context, logoutHelper: LogoutHelper, firebaseCrashlytics: FirebaseCrashlytics): DatabaseProvider { + return OfflineDatabaseProvider(context, logoutHelper, firebaseCrashlytics) } } \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/OfflineDatabaseProvider.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/OfflineDatabaseProvider.kt index f429f63454..020eff3ee9 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/OfflineDatabaseProvider.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/OfflineDatabaseProvider.kt @@ -18,16 +18,32 @@ package com.instructure.pandautils.room.offline import android.content.Context +import android.widget.Toast +import androidx.appcompat.app.AlertDialog import androidx.room.Room +import com.google.firebase.crashlytics.FirebaseCrashlytics +import com.instructure.pandautils.R +import com.instructure.pandautils.utils.LogoutHelper +import com.instructure.pandautils.utils.toast private const val OFFLINE_DB_PREFIX = "offline-db-" -class OfflineDatabaseProvider(private val context: Context) : DatabaseProvider { +class OfflineDatabaseProvider( + private val context: Context, + private val logoutHelper: LogoutHelper, + private val firebaseCrashlytics: FirebaseCrashlytics +) : DatabaseProvider { private val dbMap = mutableMapOf() override fun getDatabase(userId: Long?): OfflineDatabase { - if (userId == null) throw IllegalStateException("You can't access the database while logged out") + if (userId == null) { + logoutHelper.logout(this) + firebaseCrashlytics.recordException(IllegalStateException("You can't access the database while logged out")) + return Room.databaseBuilder(context, OfflineDatabase::class.java, "dummy-db") + .addMigrations(*offlineDatabaseMigrations) + .build() + } return dbMap.getOrPut(userId) { Room.databaseBuilder(context, OfflineDatabase::class.java, "$OFFLINE_DB_PREFIX$userId") diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/utils/LogoutHelper.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/utils/LogoutHelper.kt new file mode 100644 index 0000000000..dc2a418863 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/utils/LogoutHelper.kt @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2023 - 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.pandautils.room.offline.DatabaseProvider + +interface LogoutHelper { + + fun logout(databaseProvider: DatabaseProvider) +} \ No newline at end of file From 46c4c3ee944d10bf20caaa73a35adde7f17ad7fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81kos=20Hermann?= Date: Wed, 29 Nov 2023 11:13:30 +0100 Subject: [PATCH 027/132] version bump --- apps/student/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/student/build.gradle b/apps/student/build.gradle index 644403aff7..df8c0ecaa2 100644 --- a/apps/student/build.gradle +++ b/apps/student/build.gradle @@ -50,8 +50,8 @@ android { applicationId "com.instructure.candroid" minSdkVersion Versions.MIN_SDK targetSdkVersion Versions.TARGET_SDK - versionCode = 257 - versionName = '7.0.1' + versionCode = 258 + versionName = '7.0.2' vectorDrawables.useSupportLibrary = true multiDexEnabled = true From c4afa72d1cceddd18f5d43deec7a9250f3675350 Mon Sep 17 00:00:00 2001 From: Tamas Kozmer <72397075+tamaskozmer@users.noreply.github.com> Date: Mon, 4 Dec 2023 15:28:09 +0100 Subject: [PATCH 028/132] [MBL-17230][Student][Teacher] Update necessary dependencies for Jetpack Compose (#2265) Test plan: Smoke test the app. Some important build dependencies were changed that could affect the whole app. Also smoke test the places where PSPDFKit is used: - Student Annotation submission (Student) - View PDF submissions (Student) - View and edit PDF files from the files list (Student) - Annotate PDF submissions (Teacher) - View PDF files from the files list (Teacher) refs: MBL-17230 affects: Student, Teacher release note: none --- android-vault | 2 +- apps/flutter_parent/android/app/build.gradle | 2 +- apps/student/build.gradle | 5 ++++- .../instructure/student/test/util/TestUtils.kt | 1 - apps/teacher/build.gradle | 2 ++ .../view/grade_slider/PossiblePointView.kt | 6 +++--- .../teacher/unit/utils/MobiusTestUtils.kt | 1 - automation/espresso/build.gradle | 2 +- buildSrc/build.gradle.kts | 1 + buildSrc/src/main/java/GlobalDependencies.kt | 18 +++++++++++------- libs/login-api-2/build.gradle | 3 ++- .../pandarecycler/BaseRecyclerAdapter.kt | 2 +- 12 files changed, 27 insertions(+), 18 deletions(-) diff --git a/android-vault b/android-vault index ddd561a8ef..15225ede6c 160000 --- a/android-vault +++ b/android-vault @@ -1 +1 @@ -Subproject commit ddd561a8ef8289f2ed248925679827b2da5ab016 +Subproject commit 15225ede6c44da8265e5cdaea34d49dbc47cb8f5 diff --git a/apps/flutter_parent/android/app/build.gradle b/apps/flutter_parent/android/app/build.gradle index 823d43f00f..5fd86f30e5 100644 --- a/apps/flutter_parent/android/app/build.gradle +++ b/apps/flutter_parent/android/app/build.gradle @@ -104,7 +104,7 @@ dependencies { testImplementation 'junit:junit:4.12' androidTestImplementation 'com.android.support.test:runner:1.0.2' - androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' implementation "com.squareup.okhttp3:okhttp:4.9.1" implementation 'org.jsoup:jsoup:1.11.3' implementation 'com.google.gms:google-services:4.3.14' diff --git a/apps/student/build.gradle b/apps/student/build.gradle index eb671160b8..e0246c94c1 100644 --- a/apps/student/build.gradle +++ b/apps/student/build.gradle @@ -320,7 +320,8 @@ dependencies { implementation Libs.SQLDELIGHT /* Qr Code */ - implementation Libs.JOURNEY_ZXING + implementation (Libs.JOURNEY_ZXING) { transitive = false } + implementation Libs.JOURNEY_ZXING_CORE /* AAC */ implementation Libs.VIEW_MODEL @@ -349,6 +350,8 @@ dependencies { implementation Libs.ROOM kapt Libs.ROOM_COMPILER implementation Libs.ROOM_COROUTINES + + testImplementation Libs.HAMCREST } // Comment out this line if the reporting logic starts going wonky. diff --git a/apps/student/src/test/java/com/instructure/student/test/util/TestUtils.kt b/apps/student/src/test/java/com/instructure/student/test/util/TestUtils.kt index 5f57e39188..b3066f1c5c 100644 --- a/apps/student/src/test/java/com/instructure/student/test/util/TestUtils.kt +++ b/apps/student/src/test/java/com/instructure/student/test/util/TestUtils.kt @@ -28,7 +28,6 @@ import kotlinx.coroutines.runBlocking import okhttp3.Protocol import okhttp3.Request import okhttp3.Response -import okhttp3.ResponseBody import okhttp3.ResponseBody.Companion.toResponseBody import org.hamcrest.Matcher import org.hamcrest.Matchers diff --git a/apps/teacher/build.gradle b/apps/teacher/build.gradle index e0699ea43f..1b9fdb880c 100644 --- a/apps/teacher/build.gradle +++ b/apps/teacher/build.gradle @@ -310,6 +310,8 @@ dependencies { implementation Libs.ROOM kapt Libs.ROOM_COMPILER implementation Libs.ROOM_COROUTINES + + testImplementation Libs.HAMCREST } apply plugin: 'com.google.gms.google-services' diff --git a/apps/teacher/src/main/java/com/instructure/teacher/view/grade_slider/PossiblePointView.kt b/apps/teacher/src/main/java/com/instructure/teacher/view/grade_slider/PossiblePointView.kt index a915a867aa..5165bf6045 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/view/grade_slider/PossiblePointView.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/view/grade_slider/PossiblePointView.kt @@ -48,11 +48,11 @@ class PossiblePointView @JvmOverloads constructor( strokeWidth = 4.toPx.toFloat() } - override fun onDraw(canvas: Canvas?) { + override fun onDraw(canvas: Canvas) { super.onDraw(canvas) - canvas?.drawLine(anchorRect.left.toFloat(), anchorRect.bottom.toFloat() - 16.toPx, anchorRect.left.toFloat(), anchorRect.bottom.toFloat() - 8.toPx, linePaint) - canvas?.drawText(label, anchorRect.left.toFloat(), anchorRect.bottom.toFloat() + 16.toPx, textPaint) + canvas.drawLine(anchorRect.left.toFloat(), anchorRect.bottom.toFloat() - 16.toPx, anchorRect.left.toFloat(), anchorRect.bottom.toFloat() - 8.toPx, linePaint) + canvas.drawText(label, anchorRect.left.toFloat(), anchorRect.bottom.toFloat() + 16.toPx, textPaint) } fun showPossiblePoint(anchorRect: Rect, label: String) { diff --git a/apps/teacher/src/test/java/com/instructure/teacher/unit/utils/MobiusTestUtils.kt b/apps/teacher/src/test/java/com/instructure/teacher/unit/utils/MobiusTestUtils.kt index a50ba7e97b..18d223afb8 100644 --- a/apps/teacher/src/test/java/com/instructure/teacher/unit/utils/MobiusTestUtils.kt +++ b/apps/teacher/src/test/java/com/instructure/teacher/unit/utils/MobiusTestUtils.kt @@ -24,7 +24,6 @@ import com.spotify.mobius.test.NextMatchers import okhttp3.Protocol import okhttp3.Request import okhttp3.Response -import okhttp3.ResponseBody import okhttp3.ResponseBody.Companion.toResponseBody import org.hamcrest.Matcher import org.hamcrest.Matchers diff --git a/automation/espresso/build.gradle b/automation/espresso/build.gradle index 195d693576..05043f647a 100644 --- a/automation/espresso/build.gradle +++ b/automation/espresso/build.gradle @@ -45,7 +45,7 @@ android { buildToolsVersion '28.0.3' defaultConfig { - minSdkVersion 24 + minSdkVersion 26 targetSdkVersion 28 } diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts index daadb630e2..d761e661c8 100644 --- a/buildSrc/build.gradle.kts +++ b/buildSrc/build.gradle.kts @@ -26,6 +26,7 @@ dependencies { implementation("com.android.tools.build:gradle-api:$agpVersion") implementation("org.javassist:javassist:3.24.1-GA") implementation("com.google.code.gson:gson:2.8.8") + implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.20") } plugins { diff --git a/buildSrc/src/main/java/GlobalDependencies.kt b/buildSrc/src/main/java/GlobalDependencies.kt index 4370c14f67..8ad33f36dd 100644 --- a/buildSrc/src/main/java/GlobalDependencies.kt +++ b/buildSrc/src/main/java/GlobalDependencies.kt @@ -2,21 +2,21 @@ object Versions { /* SDK Versions */ - const val COMPILE_SDK = 33 + const val COMPILE_SDK = 34 const val MIN_SDK = 26 const val TARGET_SDK = 33 /* Build/tooling */ const val ANDROID_GRADLE_TOOLS = "7.1.3" - const val BUILD_TOOLS = "30.0.3" + const val BUILD_TOOLS = "34.0.0" /* Testing */ const val JUNIT = "4.13.2" - const val ROBOLECTRIC = "4.3.1" + const val ROBOLECTRIC = "4.11.1" const val JACOCO_ANDROID = "0.1.5" /* Kotlin */ - const val KOTLIN = "1.8.10" + const val KOTLIN = "1.9.20" const val KOTLIN_COROUTINES = "1.6.4" /* Google, Play Services */ @@ -24,11 +24,11 @@ object Versions { /* Others */ const val APOLLO = "2.5.14" // There is already a brand new version, Apollo 3, that requires lots of migration - const val PSPDFKIT = "8.7.1" + const val PSPDFKIT = "8.9.1" const val PHOTO_VIEW = "2.3.0" const val MOBIUS = "1.2.1" const val SQLDELIGHT = "1.5.4" - const val HILT = "2.45" + const val HILT = "2.48" const val HILT_ANDROIDX = "1.0.0" const val LIFECYCLE = "2.6.0" const val FRAGMENT = "1.5.5" @@ -37,7 +37,8 @@ object Versions { const val RETROFIT = "2.9.0" const val OKHTTP = "4.10.0" const val HEAP = "1.10.5" - const val ROOM = "2.5.0" + const val ROOM = "2.6.0" + const val HAMCREST = "2.2" } object Libs { @@ -101,6 +102,7 @@ object Libs { /* Qr Code (zxing) */ const val JOURNEY_ZXING = "com.journeyapps:zxing-android-embedded:4.3.0" + const val JOURNEY_ZXING_CORE = "com.google.zxing:core:3.5.2" /* Dependency Inejction */ const val HILT = "com.google.dagger:hilt-android:${Versions.HILT}" @@ -157,6 +159,8 @@ object Libs { const val ROOM_COMPILER = "androidx.room:room-compiler:${Versions.ROOM}" const val ROOM_COROUTINES = "androidx.room:room-ktx:${Versions.ROOM}" const val ROOM_TEST = "androidx.room:room-testing:${Versions.ROOM}" + + const val HAMCREST = "org.hamcrest:hamcrest:${Versions.HAMCREST}" } object Plugins { diff --git a/libs/login-api-2/build.gradle b/libs/login-api-2/build.gradle index 007c66956d..5df6ac4c54 100644 --- a/libs/login-api-2/build.gradle +++ b/libs/login-api-2/build.gradle @@ -116,7 +116,8 @@ dependencies { implementation Libs.ANDROIDX_RECYCLERVIEW implementation Libs.ANDROIDX_CONSTRAINT_LAYOUT - implementation Libs.JOURNEY_ZXING + implementation (Libs.JOURNEY_ZXING) { transitive = false } + implementation Libs.JOURNEY_ZXING_CORE implementation Libs.FRAGMENT_KTX implementation Libs.HILT diff --git a/libs/recyclerview/src/main/java/com/instructure/pandarecycler/BaseRecyclerAdapter.kt b/libs/recyclerview/src/main/java/com/instructure/pandarecycler/BaseRecyclerAdapter.kt index 6365e3f807..71cf71916f 100644 --- a/libs/recyclerview/src/main/java/com/instructure/pandarecycler/BaseRecyclerAdapter.kt +++ b/libs/recyclerview/src/main/java/com/instructure/pandarecycler/BaseRecyclerAdapter.kt @@ -22,7 +22,7 @@ import android.view.View import android.view.ViewGroup import androidx.recyclerview.widget.RecyclerView -abstract class BaseRecyclerAdapter(var context: Context) : RecyclerView.Adapter() { +abstract class BaseRecyclerAdapter(var context: Context) : RecyclerView.Adapter() { abstract fun createViewHolder(v: View, viewType: Int): T abstract fun itemLayoutResId(viewType: Int): Int abstract fun loadData() From 8f790f168eff0f3ff82668679cdd17a4ad05ebdd Mon Sep 17 00:00:00 2001 From: Kristof Deak <92309696+kdeakinstructure@users.noreply.github.com> Date: Thu, 7 Dec 2023 09:38:09 +0100 Subject: [PATCH 029/132] [MBL-17120][Student] - Create Offline All Courses E2E Test (#2268) --- .../student/ui/e2e/DashboardE2ETest.kt | 38 ++--- .../instructure/student/ui/e2e/TodoE2ETest.kt | 4 +- .../e2e/offline/OfflineAllCoursesE2ETest.kt | 141 ++++++++++++++++++ .../offline/OfflineCourseBrowserE2ETest.kt | 2 +- .../interaction/DashboardInteractionTest.kt | 28 ++-- ...EditDashboardPage.kt => AllCoursesPage.kt} | 85 ++++++++++- .../student/ui/pages/DashboardPage.kt | 4 +- .../pages/offline/ManageOfflineContentPage.kt | 4 +- .../student/ui/utils/StudentTest.kt | 4 +- .../espresso/CustomViewAssertions.kt | 9 ++ .../panda_annotations/TestMetaData.kt | 2 +- 11 files changed, 273 insertions(+), 48 deletions(-) create mode 100644 apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineAllCoursesE2ETest.kt rename apps/student/src/androidTest/java/com/instructure/student/ui/pages/{EditDashboardPage.kt => AllCoursesPage.kt} (62%) diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/DashboardE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/DashboardE2ETest.kt index e0fe3bcffa..1f50e7b9ac 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/DashboardE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/DashboardE2ETest.kt @@ -101,11 +101,11 @@ class DashboardE2ETest : StudentTest() { dashboardPage.assertDisplaysGroup(group2, course1) Log.d(STEP_TAG,"Click on 'All Courses' button. Assert that the All Courses Page is loaded.") - dashboardPage.clickEditDashboard() - editDashboardPage.assertPageObjects() + dashboardPage.openAllCoursesPage() + allCoursesPage.assertPageObjects() Log.d(STEP_TAG, "Favorite '${course1.name}' course and navigate back to Dashboard Page.") - editDashboardPage.favoriteCourse(course1.name) + allCoursesPage.favoriteCourse(course1.name) Espresso.pressBack() Log.d(STEP_TAG,"Assert that only the favoured course, '${course1.name}' is displayed." + @@ -124,16 +124,16 @@ class DashboardE2ETest : StudentTest() { Log.d(STEP_TAG,"Click on 'All Courses' button. Assert that the All Courses Page is loaded.") dashboardPage.assertPageObjects() - dashboardPage.clickEditDashboard() - editDashboardPage.assertPageObjects() + dashboardPage.openAllCoursesPage() + allCoursesPage.assertPageObjects() Log.d(STEP_TAG, "Assert that the mass select button's text is 'Unselect All', since one of the courses is selected.") - editDashboardPage.assertCourseMassSelectButtonIsDisplayed(true) + allCoursesPage.assertCourseMassSelectButtonIsDisplayed(true) Log.d(STEP_TAG, "Toggle off favourite star icon of '${course1.name}' course." + "Assert that the 'mass' select button's label is 'Select All'.") - editDashboardPage.unfavoriteCourse(course1.name) - editDashboardPage.assertCourseMassSelectButtonIsDisplayed(false) + allCoursesPage.unfavoriteCourse(course1.name) + allCoursesPage.assertCourseMassSelectButtonIsDisplayed(false) Log.d(STEP_TAG, "Navigate back to Dashboard Page.") Espresso.pressBack() @@ -186,15 +186,15 @@ class DashboardE2ETest : StudentTest() { dashboardPage.assertCourseGrade(course2.name, "N/A") Log.d(STEP_TAG,"Click on 'All Courses' button.") - dashboardPage.clickEditDashboard() - editDashboardPage.assertPageObjects() + dashboardPage.openAllCoursesPage() + allCoursesPage.assertPageObjects() Log.d(STEP_TAG, "Assert that the group 'mass' select button's label is 'Select All'.") - editDashboardPage.swipeUp() - editDashboardPage.assertGroupMassSelectButtonIsDisplayed(false) + allCoursesPage.swipeUp() + allCoursesPage.assertGroupMassSelectButtonIsDisplayed(false) Log.d(STEP_TAG, "Favorite '${group.name}' course and navigate back to Dashboard Page.") - editDashboardPage.favoriteGroup(group.name) + allCoursesPage.favoriteGroup(group.name) Espresso.pressBack() Log.d(STEP_TAG,"Assert that only the favoured group, '${group.name}' is displayed." + @@ -203,18 +203,18 @@ class DashboardE2ETest : StudentTest() { dashboardPage.assertGroupNotDisplayed(group2) Log.d(STEP_TAG,"Click on 'All Courses' button.") - dashboardPage.clickEditDashboard() - editDashboardPage.assertPageObjects() + dashboardPage.openAllCoursesPage() + allCoursesPage.assertPageObjects() Thread.sleep(2000) //It can be flaky without this 2 seconds - editDashboardPage.swipeUp() + allCoursesPage.swipeUp() Log.d(STEP_TAG, "Assert that the group 'mass' select button's label is 'Unselect All'.") - editDashboardPage.assertGroupMassSelectButtonIsDisplayed(true) + allCoursesPage.assertGroupMassSelectButtonIsDisplayed(true) Log.d(STEP_TAG, "Toggle off favourite star icon of '${group.name}' group." + "Assert that the 'mass' select button's label is 'Select All'.") - editDashboardPage.unfavoriteGroup(group.name) - editDashboardPage.assertGroupMassSelectButtonIsDisplayed(false) + allCoursesPage.unfavoriteGroup(group.name) + allCoursesPage.assertGroupMassSelectButtonIsDisplayed(false) Log.d(STEP_TAG, "Navigate back to Dashboard Page.") Espresso.pressBack() diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/TodoE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/TodoE2ETest.kt index 4968c7105f..103b61215f 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/TodoE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/TodoE2ETest.kt @@ -120,8 +120,8 @@ class TodoE2ETest: StudentTest() { Log.d(STEP_TAG, "Navigate back to the Dashboard Page. Open ${favoriteCourse.name} course. Mark it as favorite.") Espresso.pressBack() - dashboardPage.clickEditDashboard() - editDashboardPage.favoriteCourse(favoriteCourse.name) + dashboardPage.openAllCoursesPage() + allCoursesPage.favoriteCourse(favoriteCourse.name) Log.d(STEP_TAG, "Navigate back to the Dashboard Page and open the To Do Page again.") Espresso.pressBack() diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineAllCoursesE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineAllCoursesE2ETest.kt new file mode 100644 index 0000000000..8eaceb62d5 --- /dev/null +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineAllCoursesE2ETest.kt @@ -0,0 +1,141 @@ +/* + * Copyright (C) 2023 - 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.student.ui.e2e.offline + +import android.util.Log +import androidx.test.espresso.Espresso +import androidx.test.espresso.matcher.ViewMatchers +import com.google.android.material.checkbox.MaterialCheckBox +import com.instructure.canvas.espresso.OfflineE2E +import com.instructure.panda_annotations.FeatureCategory +import com.instructure.panda_annotations.Priority +import com.instructure.panda_annotations.SecondaryFeatureCategory +import com.instructure.panda_annotations.TestCategory +import com.instructure.panda_annotations.TestMetaData +import com.instructure.student.ui.utils.StudentTest +import com.instructure.student.ui.utils.seedData +import com.instructure.student.ui.utils.tokenLogin +import dagger.hilt.android.testing.HiltAndroidTest +import org.junit.After +import org.junit.Test +import java.lang.Thread.sleep + +@HiltAndroidTest +class OfflineAllCoursesE2ETest : StudentTest() { + override fun displaysPageObjects() = Unit + + override fun enableAndConfigureAccessibilityChecks() = Unit + + @OfflineE2E + @Test + @TestMetaData(Priority.MANDATORY, FeatureCategory.DASHBOARD, TestCategory.E2E, false, SecondaryFeatureCategory.ALL_COURSES) + fun testOfflineAllCoursesE2E() { + + Log.d(PREPARATION_TAG, "Seeding data.") + val data = seedData(students = 1, teachers = 1, courses = 3, announcements = 1) + val student = data.studentsList[0] + val course1 = data.coursesList[0] + val course2 = data.coursesList[1] + val course3 = data.coursesList[2] + + Log.d(STEP_TAG, "Login with user: ${student.name}, login id: ${student.loginId}.") + tokenLogin(student) + dashboardPage.waitForRender() + + Log.d(STEP_TAG, "Open the 'All Courses' page and wait for it to be rendered.") + dashboardPage.openAllCoursesPage() + allCoursesPage.assertPageObjects() + + Log.d(STEP_TAG, "Favourite '${course1.name}' course and assert if it became favourited. Then navigate back to Dashboard page.") + allCoursesPage.favoriteCourse(course1.name) + allCoursesPage.assertCourseFavorited(course1) + Espresso.pressBack() + + Log.d(STEP_TAG, "Open global 'Manage Offline Content' page via the more menu of the Dashboard Page.") + dashboardPage.openGlobalManageOfflineContentPage() + manageOfflineContentPage.assertPageObjects() + + Log.d(STEP_TAG, "Assert that the '${course1.name}' course's checkbox state is 'Unchecked'.") + manageOfflineContentPage.assertCheckedStateOfItem(course1.name, MaterialCheckBox.STATE_UNCHECKED) + manageOfflineContentPage.assertCheckedStateOfItem(course2.name, MaterialCheckBox.STATE_UNCHECKED) + + Log.d(STEP_TAG, "Select '${course1.name}' and '${course2.name}' courses' checkboxes and Sync them.") + manageOfflineContentPage.changeItemSelectionState(course1.name) + manageOfflineContentPage.changeItemSelectionState(course2.name) + manageOfflineContentPage.clickOnSyncButtonAndConfirm() + + Log.d(STEP_TAG, "Wait for the 'Download Started' dashboard notification to be displayed, and the to disappear.") + dashboardPage.waitForRender() + dashboardPage.waitForSyncProgressDownloadStartedNotification() + dashboardPage.waitForSyncProgressDownloadStartedNotificationToDisappear() + + Log.d(STEP_TAG, "Wait for the 'Syncing Offline Content' dashboard notification to be displayed, and the to disappear. (It should be displayed after the 'Download Started' notification immediately.)") + dashboardPage.waitForSyncProgressStartingNotification() + dashboardPage.waitForSyncProgressStartingNotificationToDisappear() + + Log.d(PREPARATION_TAG, "Turn off the Wi-Fi and Mobile Data on the device, so it will go offline.") + turnOffConnectionViaADB() + + Log.d(STEP_TAG, "Wait for the Dashboard Page to be rendered, and assert that '${course1.name}' is the only course which is displayed on the offline mode Dashboard Page.") + dashboardPage.waitForRender() + sleep(10000) + dashboardPage.assertDisplaysCourse(course1) + dashboardPage.assertCourseNotDisplayed(course2) + dashboardPage.assertCourseNotDisplayed(course3) + + Log.d(STEP_TAG, "Open the 'All Courses' page and wait for it to be rendered.") + dashboardPage.openAllCoursesPage() + allCoursesPage.assertPageObjects() + + Log.d(STEP_TAG, "Assert that the plus 'Note' box is displayed in which warns the user that favouring courses can only be done in online mode.") + allCoursesPage.assertOfflineNoteDisplayed() + + Log.d(STEP_TAG, "Dismiss the offline 'Note' box and assert if it's disappear.") + allCoursesPage.dismissOfflineNoteBox() + allCoursesPage.assertOfflineNoteNotDisplayed() + + Log.d(STEP_TAG, "Assert that the select/unselect all button is not clickable because offline mode does not supports it.") + allCoursesPage.assertSelectUnselectAllButtonNotClickable() + + Log.d(STEP_TAG, "Try to unfavorite '${course1.name}' course and assert it does not happened because favoring does not allowed in offline state.") + allCoursesPage.unfavoriteCourse(course1.name) + allCoursesPage.assertCourseFavorited(course1) + + Log.d(STEP_TAG, "Assert that '${course3.name}' course's details are faded (and they having 0.4 alpha value) and it's offline sync icon is not displayed since it's not synced.") + allCoursesPage.assertCourseDetailsAlpha(course3.name, 0.4f) + allCoursesPage.assertCourseOfflineSyncButton(course3.name, ViewMatchers.Visibility.GONE) + + Log.d(STEP_TAG, "Assert that '${course1.name}' course's favourite star is faded (and it's having 0.4 alpha value) because favoring is not possible in offline mode," + + "the course title and open button are not faded (1.0 alpha value) and the offline sync icon is displayed since the course is synced.") + allCoursesPage.assertCourseFavouriteStarAlpha(course1.name, 0.4f) + allCoursesPage.assertCourseTitleAlpha(course1.name, 1.0f) + allCoursesPage.assertCourseOpenButtonAlpha(course1.name, 1.0f) + allCoursesPage.assertCourseOfflineSyncButton(course1.name, ViewMatchers.Visibility.VISIBLE) + + Log.d(STEP_TAG, "Click on '${course1.name}' course and assert if it will navigate the user to the CourseBrowser Page.") + allCoursesPage.openCourse(course1.name) + courseBrowserPage.assertTitleCorrect(course1) + + } + + @After + fun tearDown() { + Log.d(PREPARATION_TAG, "Turn back on the Wi-Fi and Mobile Data on the device, so it will come back online.") + turnOnConnectionViaADB() + } + +} \ No newline at end of file diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineCourseBrowserE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineCourseBrowserE2ETest.kt index 04a6fc4337..0c36ed3721 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineCourseBrowserE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineCourseBrowserE2ETest.kt @@ -42,7 +42,7 @@ class OfflineCourseBrowserE2ETest : StudentTest() { @OfflineE2E @Test - @TestMetaData(Priority.MANDATORY, FeatureCategory.DASHBOARD, TestCategory.E2E) + @TestMetaData(Priority.MANDATORY, FeatureCategory.COURSE, TestCategory.E2E) fun testOfflineCourseBrowserPageUnavailableE2E() { Log.d(PREPARATION_TAG,"Seeding data.") diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/DashboardInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/DashboardInteractionTest.kt index f02805dfff..e9ec0dcbb3 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/DashboardInteractionTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/DashboardInteractionTest.kt @@ -93,10 +93,10 @@ class DashboardInteractionTest : StudentTest() { dashboardPage.assertCourseNotShown(nonFavorite) dashboardPage.editFavorites() - editDashboardPage.assertCourseDisplayed(nonFavorite) - editDashboardPage.assertCourseNotFavorited(nonFavorite) - editDashboardPage.favoriteCourse(nonFavorite) - editDashboardPage.assertCourseFavorited(nonFavorite) + allCoursesPage.assertCourseDisplayed(nonFavorite) + allCoursesPage.assertCourseNotFavorited(nonFavorite) + allCoursesPage.favoriteCourse(nonFavorite) + allCoursesPage.assertCourseFavorited(nonFavorite) Espresso.pressBack() @@ -115,10 +115,10 @@ class DashboardInteractionTest : StudentTest() { dashboardPage.assertDisplaysCourse(favorite) dashboardPage.editFavorites() - editDashboardPage.assertCourseDisplayed(favorite) - editDashboardPage.assertCourseFavorited(favorite) - editDashboardPage.unfavoriteCourse(favorite) - editDashboardPage.assertCourseNotFavorited(favorite) + allCoursesPage.assertCourseDisplayed(favorite) + allCoursesPage.assertCourseFavorited(favorite) + allCoursesPage.unfavoriteCourse(favorite) + allCoursesPage.assertCourseNotFavorited(favorite) Espresso.pressBack() @@ -137,9 +137,9 @@ class DashboardInteractionTest : StudentTest() { data.courses.values.forEach { dashboardPage.assertDisplaysCourse(it) } dashboardPage.editFavorites() - toFavorite.forEach { editDashboardPage.assertCourseNotFavorited(it) } - editDashboardPage.selectAllCourses() - toFavorite.forEach { editDashboardPage.assertCourseFavorited(it) } + toFavorite.forEach { allCoursesPage.assertCourseNotFavorited(it) } + allCoursesPage.selectAllCourses() + toFavorite.forEach { allCoursesPage.assertCourseFavorited(it) } Espresso.pressBack() @@ -156,9 +156,9 @@ class DashboardInteractionTest : StudentTest() { toRemove.forEach { dashboardPage.assertDisplaysCourse(it) } dashboardPage.editFavorites() - toRemove.forEach { editDashboardPage.assertCourseFavorited(it) } - editDashboardPage.unselectAllCourses() - toRemove.forEach { editDashboardPage.assertCourseNotFavorited(it) } + toRemove.forEach { allCoursesPage.assertCourseFavorited(it) } + allCoursesPage.unselectAllCourses() + toRemove.forEach { allCoursesPage.assertCourseNotFavorited(it) } Espresso.pressBack() diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/EditDashboardPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/AllCoursesPage.kt similarity index 62% rename from apps/student/src/androidTest/java/com/instructure/student/ui/pages/EditDashboardPage.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/pages/AllCoursesPage.kt index c4bd4d94cf..d14942bad9 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/EditDashboardPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/AllCoursesPage.kt @@ -16,25 +16,31 @@ package com.instructure.student.ui.pages -import androidx.test.espresso.matcher.ViewMatchers.hasDescendant -import androidx.test.espresso.matcher.ViewMatchers.withContentDescription -import androidx.test.espresso.matcher.ViewMatchers.withParent -import androidx.test.espresso.matcher.ViewMatchers.withText +import android.view.View +import androidx.appcompat.widget.AppCompatImageView +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.matcher.ViewMatchers.* import com.instructure.canvasapi2.models.Course +import com.instructure.dataseeding.model.CourseApiModel +import com.instructure.espresso.DoesNotExistAssertion +import com.instructure.espresso.ViewAlphaAssertion import com.instructure.espresso.assertDisplayed import com.instructure.espresso.click import com.instructure.espresso.page.BasePage import com.instructure.espresso.page.onView import com.instructure.espresso.page.plus +import com.instructure.espresso.page.waitForView import com.instructure.espresso.page.withId import com.instructure.espresso.page.withText import com.instructure.espresso.scrollTo import com.instructure.espresso.swipeUp import com.instructure.student.R +import org.hamcrest.Matcher import org.hamcrest.Matchers.allOf +import org.hamcrest.Matchers.anyOf import org.hamcrest.Matchers.containsString -class EditDashboardPage : BasePage(R.id.editDashboardPage) { +class AllCoursesPage : BasePage(R.id.editDashboardPage) { fun assertCourseDisplayed(course: Course) { val itemMatcher = allOf(withText(containsString(course.name)), withId(R.id.title)) @@ -102,6 +108,14 @@ class EditDashboardPage : BasePage(R.id.editDashboardPage) { onView(itemMatcher).assertDisplayed() } + fun assertCourseFavorited(course: CourseApiModel) { + val childMatcher = withContentDescription("Remove from dashboard") + val itemMatcher = allOf( + withContentDescription(containsString("Course ${course.name}, favorite")), + hasDescendant(childMatcher)) + onView(itemMatcher).assertDisplayed() + } + fun selectAllCourses() { val childMatcher = withContentDescription("Add all to dashboard") val itemMatcher = allOf(hasDescendant(withText(R.string.allCoursesCourseHeader)), hasDescendant(childMatcher)) @@ -116,6 +130,17 @@ class EditDashboardPage : BasePage(R.id.editDashboardPage) { onView(withParent(itemMatcher) + childMatcher).click() } + fun openCourse(courseName: String) { + onView(withId(R.id.title) + withText(courseName)).click() + } + + //OfflineMethod + fun assertSelectUnselectAllButtonNotClickable() { + val unselectAllMatcher = withContentDescription("Remove all from dashboard") + val selectAllMatcher = withContentDescription("Add all to dashboard") + onView(anyOf(unselectAllMatcher, selectAllMatcher)).check(matches(isNotClickable())) + } + fun assertCourseMassSelectButtonIsDisplayed(someSelected: Boolean) { if (someSelected) { @@ -149,4 +174,54 @@ class EditDashboardPage : BasePage(R.id.editDashboardPage) { onView(withId(R.id.swipeRefreshLayout) + withParent(withId(R.id.editDashboardPage))).swipeUp() } + //OfflineMethod + fun assertOfflineNoteDisplayed() { + waitForView(withId(R.id.noteTitle) + withText(R.string.allCoursesOfflineNoteTitle)).assertDisplayed() + onView(withId(R.id.note) + withText(R.string.allCoursesOfflineNote)) + } + + //OfflineMethod + fun assertOfflineNoteNotDisplayed() { + onView(withId(R.id.noteTitle) + withText(R.string.allCoursesOfflineNoteTitle)).check(DoesNotExistAssertion(10)) + } + + //OfflineMethod + fun dismissOfflineNoteBox() { + onView(allOf(isAssignableFrom(AppCompatImageView::class.java), hasSibling(withId(R.id.noteTitle)), hasSibling(withId(R.id.note)))).click() + onView(withId(R.id.noteTitle) + withText(R.string.allCoursesOfflineNoteTitle)).check(DoesNotExistAssertion(10)) + } + + //OfflineMethod + private fun assertViewAlpha(matcher: Matcher, expectedAlphaValue: Float) { + onView(matcher).check(ViewAlphaAssertion(expectedAlphaValue)) + } + + //OfflineMethod + fun assertCourseFavouriteStarAlpha(courseName: String, expectedAlphaValue: Float) { + assertViewAlpha(withId(R.id.favoriteButton) + hasSibling(withId(R.id.title) + withText(courseName)), expectedAlphaValue) + } + + //OfflineMethod + fun assertCourseTitleAlpha(courseName: String, expectedAlphaValue: Float) { + assertViewAlpha(withId(R.id.title) + withText(courseName), expectedAlphaValue) + } + + //OfflineMethod + fun assertCourseOpenButtonAlpha(courseName: String, expectedAlphaValue: Float) { + assertViewAlpha(withId(R.id.openButton) + hasSibling(withId(R.id.title) + withText(courseName)), expectedAlphaValue) + } + + //OfflineMethod + fun assertCourseDetailsAlpha(courseName: String, expectedAlphaValue: Float) { + assertCourseFavouriteStarAlpha(courseName, expectedAlphaValue) + assertCourseTitleAlpha(courseName, expectedAlphaValue) + assertCourseOpenButtonAlpha(courseName, expectedAlphaValue) + } + + //OfflineMethod + fun assertCourseOfflineSyncButton(courseName: String, visibility: Visibility) { + onView(withId(R.id.offlineSyncIcon) + hasSibling(withId(R.id.title) + withText(courseName))) + .check(matches(withEffectiveVisibility(visibility))) + } + } \ No newline at end of file diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/DashboardPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/DashboardPage.kt index c4d2b55143..a8b51842eb 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/DashboardPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/DashboardPage.kt @@ -272,8 +272,8 @@ class DashboardPage : BasePage(R.id.dashboardPage) { Espresso.openActionBarOverflowOrOptionsMenu(InstrumentationRegistry.getInstrumentation().targetContext) } - fun clickEditDashboard() { - onView(withId(R.id.editDashboardTextView)).scrollTo().click() + fun openAllCoursesPage() { + waitForView(withId(R.id.editDashboardTextView)).scrollTo().click() } fun assertCourseNotDisplayed(course: CourseApiModel) { diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/offline/ManageOfflineContentPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/offline/ManageOfflineContentPage.kt index 383f9fe2df..ad3815b554 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/offline/ManageOfflineContentPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/offline/ManageOfflineContentPage.kt @@ -116,11 +116,11 @@ class ManageOfflineContentPage : BasePage(R.id.manageOfflineContentPage) { } fun assertCheckedStateOfItem(itemName: String, state: Int) { - onView(withId(R.id.checkbox) + hasSibling(withId(R.id.title) + withText(itemName)) + hasCheckedState(state)).scrollTo().assertDisplayed() + waitForView(withId(R.id.checkbox) + hasSibling(withId(R.id.title) + withText(itemName)) + hasCheckedState(state)).scrollTo().assertDisplayed() } fun waitForItemDisappear(itemName: String) { - onView(withId(R.id.checkbox) + hasSibling(withId(R.id.title) + withText(itemName))).check(DoesNotExistAssertion(5)) + waitForView(withId(R.id.checkbox) + hasSibling(withId(R.id.title) + withText(itemName))).check(DoesNotExistAssertion(5)) } fun assertDisplaysNoCourses() { diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/utils/StudentTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/utils/StudentTest.kt index 88c4ab96d8..d11f313b76 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/utils/StudentTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/utils/StudentTest.kt @@ -42,6 +42,7 @@ import com.instructure.student.R import com.instructure.student.activity.LoginActivity import com.instructure.student.espresso.StudentHiltTestApplication_Application import com.instructure.student.ui.pages.AboutPage +import com.instructure.student.ui.pages.AllCoursesPage import com.instructure.student.ui.pages.AnnotationCommentListPage import com.instructure.student.ui.pages.AnnouncementListPage import com.instructure.student.ui.pages.AssignmentDetailsPage @@ -56,7 +57,6 @@ import com.instructure.student.ui.pages.CourseGradesPage import com.instructure.student.ui.pages.DashboardPage import com.instructure.student.ui.pages.DiscussionDetailsPage import com.instructure.student.ui.pages.DiscussionListPage -import com.instructure.student.ui.pages.EditDashboardPage import com.instructure.student.ui.pages.ElementaryCoursePage import com.instructure.student.ui.pages.ElementaryDashboardPage import com.instructure.student.ui.pages.FileListPage @@ -162,7 +162,7 @@ abstract class StudentTest : CanvasTest() { val leftSideNavigationDrawerPage = LeftSideNavigationDrawerPage() val discussionDetailsPage = DiscussionDetailsPage() val discussionListPage = DiscussionListPage(Searchable(R.id.search, R.id.search_src_text, R.id.search_close_btn)) - val editDashboardPage = EditDashboardPage() + val allCoursesPage = AllCoursesPage() val fileListPage = FileListPage(Searchable(R.id.search, R.id.queryInput, R.id.clearButton, R.id.backButton)) val fileUploadPage = FileUploadPage() val helpPage = HelpPage() diff --git a/automation/espresso/src/main/kotlin/com/instructure/espresso/CustomViewAssertions.kt b/automation/espresso/src/main/kotlin/com/instructure/espresso/CustomViewAssertions.kt index 0951e8236d..77a1c2d6ec 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/espresso/CustomViewAssertions.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/espresso/CustomViewAssertions.kt @@ -26,9 +26,11 @@ import androidx.test.espresso.NoMatchingViewException import androidx.test.espresso.ViewAssertion import androidx.test.espresso.assertion.ViewAssertions.doesNotExist import androidx.test.espresso.matcher.ViewMatchers +import androidx.test.espresso.matcher.ViewMatchers.assertThat import androidx.viewpager.widget.ViewPager import com.google.android.material.bottomnavigation.BottomNavigationView import junit.framework.AssertionFailedError +import org.hamcrest.CoreMatchers.`is` import org.hamcrest.Matcher import org.hamcrest.Matchers import org.junit.Assert.assertEquals @@ -120,3 +122,10 @@ class ConstraintLayoutItemCountAssertion(private val expectedCount: Int) : ViewA } } +class ViewAlphaAssertion(private val expectedAlpha: Float): ViewAssertion { + override fun check(view: View, noViewFoundException: NoMatchingViewException?) { + noViewFoundException?.let { throw it } + assertThat("View alpha should be $expectedAlpha", view.alpha, `is`(expectedAlpha)) + } +} + diff --git a/libs/panda_annotations/src/main/java/com/instructure/panda_annotations/TestMetaData.kt b/libs/panda_annotations/src/main/java/com/instructure/panda_annotations/TestMetaData.kt index e9f5f0ddb6..4400d2346b 100644 --- a/libs/panda_annotations/src/main/java/com/instructure/panda_annotations/TestMetaData.kt +++ b/libs/panda_annotations/src/main/java/com/instructure/panda_annotations/TestMetaData.kt @@ -43,7 +43,7 @@ enum class SecondaryFeatureCategory { ASSIGNMENT_COMMENTS, ASSIGNMENT_QUIZZES, ASSIGNMENT_DISCUSSIONS, GROUPS_DASHBOARD, GROUPS_FILES, GROUPS_ANNOUNCEMENTS, GROUPS_DISCUSSIONS, GROUPS_PAGES, GROUPS_PEOPLE, EVENTS_DISCUSSIONS, EVENTS_QUIZZES, EVENTS_ASSIGNMENTS, EVENTS_NOTIFICATIONS, - MODULES_ASSIGNMENTS, MODULES_DISCUSSIONS, MODULES_FILES, MODULES_PAGES, MODULES_QUIZZES, OFFLINE_MODE + MODULES_ASSIGNMENTS, MODULES_DISCUSSIONS, MODULES_FILES, MODULES_PAGES, MODULES_QUIZZES, OFFLINE_MODE, ALL_COURSES } enum class TestCategory { From 886a3068ef04e53b97eaf8ba6fdeb7fc074e595d Mon Sep 17 00:00:00 2001 From: Kristof Nemere <109959688+kristofnemere@users.noreply.github.com> Date: Thu, 7 Dec 2023 15:33:32 +0100 Subject: [PATCH 030/132] [MBL-17188][Student][Parent] Late penalty display for assignments refs: MBL-17188 affects: Student, Parent release note: Improved display of the late penalty. * Student late penalty changes * Parent late penalty changes * Fixed string * Fixed strings * Test fix * test fix --- .../lib/l10n/app_localizations.dart | 13 +++- .../lib/models/grade_cell_data.dart | 12 +++- .../lib/models/grade_cell_data.g.dart | 14 ++++ .../lib/screens/assignments/grade_cell.dart | 71 +++++++++++-------- .../assignments/grade_cell_data_test.dart | 7 +- .../assignments/grade_cell_widget_test.dart | 7 +- .../renderTests/views/GradeCellRenderTest.kt | 6 +- .../gradecellview/GradeCellViewData.kt | 10 ++- .../ui/gradeCell/GradeCellView.kt | 2 +- .../ui/gradeCell/GradeCellViewState.kt | 13 ++-- .../view_student_enhanced_grade_cell.xml | 24 +++++-- .../res/layout/view_student_grade_cell.xml | 24 +++++-- .../assignment/details/GradeCellStateTest.kt | 10 +-- libs/pandares/src/main/res/values/strings.xml | 2 + 14 files changed, 150 insertions(+), 65 deletions(-) diff --git a/apps/flutter_parent/lib/l10n/app_localizations.dart b/apps/flutter_parent/lib/l10n/app_localizations.dart index 53eda6d918..86768767b3 100644 --- a/apps/flutter_parent/lib/l10n/app_localizations.dart +++ b/apps/flutter_parent/lib/l10n/app_localizations.dart @@ -989,11 +989,18 @@ class AppLocalizations { desc: 'Screen reader-friendly replacement for the "-" character in letter grades like "A-"', ); - String latePenalty(String pointsLost) => Intl.message( - 'Late penalty (-$pointsLost)', + String yourGrade(String pointsAchieved) => Intl.message( + 'Your grade: $pointsAchieved', + desc: 'Text displayed when a late penalty has been applied to the assignment, this is the achieved score without the penalty', + args: [pointsAchieved], + name: 'yourGrade', + ); + + String latePenaltyUpdated(String pointsLost) => Intl.message( + 'Late Penalty: -$pointsLost pts', desc: 'Text displayed when a late penalty has been applied to the assignment', args: [pointsLost], - name: 'latePenalty', + name: 'latePenaltyUpdated', ); String finalGrade(String grade) => Intl.message( diff --git a/apps/flutter_parent/lib/models/grade_cell_data.dart b/apps/flutter_parent/lib/models/grade_cell_data.dart index 642c29700e..f30b60241f 100644 --- a/apps/flutter_parent/lib/models/grade_cell_data.dart +++ b/apps/flutter_parent/lib/models/grade_cell_data.dart @@ -40,6 +40,7 @@ abstract class GradeCellData implements Built 0.0) { grade = ''; // Grade will be shown in the 'final grade' text var pointsDeducted = NumberFormat.decimalPattern().format(submission.pointsDeducted ?? 0.0); - latePenalty = l10n.latePenalty(pointsDeducted); + var pointsAchieved = NumberFormat.decimalPattern().format(submission.enteredScore); + yourGrade = l10n.yourGrade(pointsAchieved); + latePenalty = l10n.latePenaltyUpdated(pointsDeducted); finalGrade = l10n.finalGrade(submission.grade ?? grade); } @@ -166,6 +171,7 @@ abstract class GradeCellData implements Built _$this._outOf; set outOf(String? outOf) => _$this._outOf = outOf; + String? _yourGrade; + String? get yourGrade => _$this._yourGrade; + set yourGrade(String? yourGrade) => _$this._yourGrade = yourGrade; + String? _latePenalty; String? get latePenalty => _$this._latePenalty; set latePenalty(String? latePenalty) => _$this._latePenalty = latePenalty; @@ -221,6 +233,7 @@ class GradeCellDataBuilder _grade = $v.grade; _gradeContentDescription = $v.gradeContentDescription; _outOf = $v.outOf; + _yourGrade = $v.yourGrade; _latePenalty = $v.latePenalty; _finalGrade = $v.finalGrade; _$v = null; @@ -264,6 +277,7 @@ class GradeCellDataBuilder grade: BuiltValueNullFieldError.checkNotNull(grade, r'GradeCellData', 'grade'), gradeContentDescription: BuiltValueNullFieldError.checkNotNull(gradeContentDescription, r'GradeCellData', 'gradeContentDescription'), outOf: BuiltValueNullFieldError.checkNotNull(outOf, r'GradeCellData', 'outOf'), + yourGrade: BuiltValueNullFieldError.checkNotNull(yourGrade, r'GradeCellData', 'yourGrade'), latePenalty: BuiltValueNullFieldError.checkNotNull(latePenalty, r'GradeCellData', 'latePenalty'), finalGrade: BuiltValueNullFieldError.checkNotNull(finalGrade, r'GradeCellData', 'finalGrade')); replace(_$result); diff --git a/apps/flutter_parent/lib/screens/assignments/grade_cell.dart b/apps/flutter_parent/lib/screens/assignments/grade_cell.dart index 9d8bb559bf..d8bac77e11 100644 --- a/apps/flutter_parent/lib/screens/assignments/grade_cell.dart +++ b/apps/flutter_parent/lib/screens/assignments/grade_cell.dart @@ -18,6 +18,7 @@ import 'package:flutter_parent/models/assignment.dart'; import 'package:flutter_parent/models/course.dart'; import 'package:flutter_parent/models/grade_cell_data.dart'; import 'package:flutter_parent/models/submission.dart'; +import 'package:flutter_parent/utils/design/parent_colors.dart'; import 'package:flutter_parent/utils/design/parent_theme.dart'; import 'package:percent_indicator/circular_percent_indicator.dart'; @@ -134,37 +135,49 @@ class GradeCell extends StatelessWidget { ], ), if (!_isGradedRestrictQuantitativeData) SizedBox(width: 16), - if (!_isGradedRestrictQuantitativeData) Expanded( - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (data.grade.isNotEmpty) - Text( - data.grade, - key: Key('grade-cell-grade'), - style: Theme.of(context).textTheme.headlineMedium, - semanticsLabel: data.gradeContentDescription, - ), - if (data.outOf.isNotEmpty) Text(data.outOf, key: Key('grade-cell-out-of')), - if (data.latePenalty.isNotEmpty) - Text( - data.latePenalty, - style: TextStyle(color: data.accentColor), - key: Key('grade-cell-late-penalty'), - ), - if (data.finalGrade.isNotEmpty) - Padding( - padding: const EdgeInsets.only(top: 8), - child: Text( - data.finalGrade, - key: Key('grade-cell-final-grade'), - style: Theme.of(context).textTheme.titleMedium, + if (!_isGradedRestrictQuantitativeData) + Expanded( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (data.grade.isNotEmpty) + Text( + data.grade, + key: Key('grade-cell-grade'), + style: Theme.of(context).textTheme.headlineMedium, + semanticsLabel: data.gradeContentDescription, + ), + if (data.outOf.isNotEmpty) Text(data.outOf, key: Key('grade-cell-out-of')), + if (data.yourGrade.isNotEmpty) + Padding( + padding: const EdgeInsets.only(top: 4), + child: Text( + data.yourGrade, + key: Key('grade-cell-your-grade'), + ), + ), + if (data.latePenalty.isNotEmpty) + Padding( + padding: const EdgeInsets.only(top: 4), + child: Text( + data.latePenalty, + style: TextStyle(color: ParentColors.failure), + key: Key('grade-cell-late-penalty'), + ), + ), + if (data.finalGrade.isNotEmpty) + Padding( + padding: const EdgeInsets.only(top: 4), + child: Text( + data.finalGrade, + key: Key('grade-cell-final-grade'), + style: Theme.of(context).textTheme.titleMedium, + ), ), - ), - ], + ], + ), ), - ), ], ); } diff --git a/apps/flutter_parent/test/screens/assignments/grade_cell_data_test.dart b/apps/flutter_parent/test/screens/assignments/grade_cell_data_test.dart index 134975c7df..2fe2782299 100644 --- a/apps/flutter_parent/test/screens/assignments/grade_cell_data_test.dart +++ b/apps/flutter_parent/test/screens/assignments/grade_cell_data_test.dart @@ -250,10 +250,11 @@ void main() { ..grade = '79' ..score = 79.0); var expected = baseGradedState.rebuild((b) => b - ..graphPercent = 0.85 - ..score = '85' + ..graphPercent = 0.79 + ..score = '79' ..showPointsLabel = true - ..latePenalty = 'Late penalty (-6)' + ..yourGrade = 'Your grade: 85' + ..latePenalty = 'Late Penalty: -6 pts' ..finalGrade = 'Final Grade: 79'); var actual = GradeCellData.forSubmission(baseCourse, baseAssignment, submission, theme, l10n); expect(actual, expected); diff --git a/apps/flutter_parent/test/screens/assignments/grade_cell_widget_test.dart b/apps/flutter_parent/test/screens/assignments/grade_cell_widget_test.dart index 37bd688ec3..3cc7c10885 100644 --- a/apps/flutter_parent/test/screens/assignments/grade_cell_widget_test.dart +++ b/apps/flutter_parent/test/screens/assignments/grade_cell_widget_test.dart @@ -16,6 +16,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_parent/l10n/app_localizations.dart'; import 'package:flutter_parent/models/grade_cell_data.dart'; import 'package:flutter_parent/screens/assignments/grade_cell.dart'; +import 'package:flutter_parent/utils/design/parent_colors.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:percent_indicator/circular_percent_indicator.dart'; @@ -158,11 +159,13 @@ void main() { GradeCellData data = GradeCellData((b) => b ..state = GradeCellState.graded ..accentColor = Colors.pinkAccent - ..latePenalty = 'Late penalty: (-25)'); + ..yourGrade = 'Your grade: 85' + ..latePenalty = 'Late penalty: -25'); await setupWithData(tester, data); expect(find.byKey(Key('grade-cell-late-penalty')).evaluate(), find.text(data.latePenalty).evaluate()); - expect(tester.widget(find.byKey(Key('grade-cell-late-penalty'))).style!.color, data.accentColor); + expect(find.byKey(Key('grade-cell-your-grade')).evaluate(), find.text(data.yourGrade).evaluate()); + expect(tester.widget(find.byKey(Key('grade-cell-late-penalty'))).style!.color, ParentColors.failure); }); testWidgetsWithAccessibilityChecks('Displays final grade text', (tester) async { diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/views/GradeCellRenderTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/views/GradeCellRenderTest.kt index 432f6a7c6c..ecb161ed32 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/views/GradeCellRenderTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/views/GradeCellRenderTest.kt @@ -37,6 +37,7 @@ class GradeCellRenderTest : StudentRenderTest() { val submittedTitle by OnViewWithId(R.id.submittedTitle) val submittedSubtitle by OnViewWithId(R.id.submittedSubtitle) val pointsLabel by OnViewWithId(R.id.pointsLabel) + val yourGrade by OnViewWithId(R.id.yourGrade) val latePenalty by OnViewWithId(R.id.latePenalty) val finalGrade by OnViewWithId(R.id.finalGrade) val grade by OnViewWithId(R.id.grade) @@ -122,7 +123,8 @@ class GradeCellRenderTest : StudentRenderTest() { score = "91", showPointsLabel = true, outOf = "Out of 100 pts", - latePenalty = "Late Penalty (-2 pts)", + yourGrade = "Your Grade: 91 pts", + latePenalty = "Late Penalty: -2 pts", finalGrade = "Final Grade: 89 pts" ) setupViewWithState(state) @@ -132,6 +134,7 @@ class GradeCellRenderTest : StudentRenderTest() { score.assertDisplayed() pointsLabel.assertDisplayed() outOf.assertDisplayed() + yourGrade.assertDisplayed() latePenalty.assertDisplayed() finalGrade.assertDisplayed() } @@ -140,6 +143,7 @@ class GradeCellRenderTest : StudentRenderTest() { with(gradeCell) { score.assertHasText(state.score) outOf.assertHasText(state.outOf) + yourGrade.assertHasText(state.yourGrade) latePenalty.assertHasText(state.latePenalty) finalGrade.assertHasText(state.finalGrade) } diff --git a/apps/student/src/main/java/com/instructure/student/features/assignments/details/gradecellview/GradeCellViewData.kt b/apps/student/src/main/java/com/instructure/student/features/assignments/details/gradecellview/GradeCellViewData.kt index 7fb3b6eae8..fe8c7ca580 100644 --- a/apps/student/src/main/java/com/instructure/student/features/assignments/details/gradecellview/GradeCellViewData.kt +++ b/apps/student/src/main/java/com/instructure/student/features/assignments/details/gradecellview/GradeCellViewData.kt @@ -24,6 +24,7 @@ data class GradeCellViewData( val grade: String = "", val gradeCellContentDescription: String = "", val outOf: String = "", + val yourGrade: String = "", val latePenalty: String = "", val finalGrade: String = "", val stats: GradeCellViewState.GradeStats? = null @@ -39,7 +40,6 @@ data class GradeCellViewData( } companion object { - @Suppress("DEPRECATION") fun fromSubmission( resources: Resources, courseColor: ThemedColor, @@ -145,7 +145,7 @@ data class GradeCellViewData( gradeCellContentDescription = contentDescription, ) } else { - val score = NumberHelper.formatDecimal(submission.enteredScore, 2, true) + val score = NumberHelper.formatDecimal(submission.score, 2, true) val chartPercent = (submission.enteredScore / assignment.pointsPossible).coerceIn(0.0, 1.0).toFloat() // If grading type is Points, don't show the grade since we're already showing it as the score var grade = if (assignment.gradingType != Assignment.POINTS_TYPE) submission.grade.orEmpty() else "" @@ -170,12 +170,15 @@ data class GradeCellViewData( var latePenalty = "" var finalGrade = "" + var yourGrade = "" // Adjust for late penalty, if any if (submission.pointsDeducted.orDefault() > 0.0) { grade = "" // Grade will be shown in the 'final grade' text val pointsDeducted = NumberHelper.formatDecimal(submission.pointsDeducted.orDefault(), 2, true) - latePenalty = resources.getString(R.string.latePenalty, pointsDeducted) + val achievedScore = NumberHelper.formatDecimal(submission.enteredScore, 2, true) + yourGrade = resources.getString(R.string.yourGrade, achievedScore) + latePenalty = resources.getString(R.string.latePenaltyUpdated, pointsDeducted) finalGrade = resources.getString(R.string.finalGradeFormatted, submission.grade) } @@ -210,6 +213,7 @@ data class GradeCellViewData( grade = grade, gradeCellContentDescription = gradeCellContentDescription, outOf = outOfText, + yourGrade = yourGrade, latePenalty = latePenalty, finalGrade = finalGrade, stats = stats diff --git a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/ui/gradeCell/GradeCellView.kt b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/ui/gradeCell/GradeCellView.kt index 49b796f7ad..b5a9df7835 100644 --- a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/ui/gradeCell/GradeCellView.kt +++ b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/ui/gradeCell/GradeCellView.kt @@ -67,6 +67,7 @@ class GradeCellView @JvmOverloads constructor( pointsLabel.setVisible(state.showPointsLabel) completeIcon.setVisible(state.showCompleteIcon) incompleteIcon.setVisible(state.showIncompleteIcon) + yourGrade.setTextForVisibility(state.yourGrade) latePenalty.setTextForVisibility(state.latePenalty) finalGrade.setTextForVisibility(state.finalGrade) grade.setTextForVisibility(state.grade) @@ -75,7 +76,6 @@ class GradeCellView @JvmOverloads constructor( outOf.contentDescription = state.outOfContentDescription // Accent color - latePenalty.setTextColor(state.accentColor) chart.setColor(state.accentColor) completeIcon.imageTintList = ColorStateList.valueOf(state.accentColor) incompleteIcon.imageTintList = ColorStateList.valueOf(state.accentColor) diff --git a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/ui/gradeCell/GradeCellViewState.kt b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/ui/gradeCell/GradeCellViewState.kt index 42cf48e75c..f5f368b06a 100644 --- a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/ui/gradeCell/GradeCellViewState.kt +++ b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/ui/gradeCell/GradeCellViewState.kt @@ -43,6 +43,7 @@ sealed class GradeCellViewState { val gradeCellContentDescription: String = "", val outOf: String = "", val outOfContentDescription: String = "", + val yourGrade: String = "", val latePenalty: String = "", val finalGrade: String = "", val stats: GradeStats? = null @@ -132,8 +133,8 @@ sealed class GradeCellViewState { ) } - val score = NumberHelper.formatDecimal(submission.enteredScore, 2, true) - val graphPercent = (submission.enteredScore / assignment.pointsPossible).coerceIn(0.0, 1.0).toFloat() + val score = NumberHelper.formatDecimal(submission.score, 2, true) + val graphPercent = (submission.score / assignment.pointsPossible).coerceIn(0.0, 1.0).toFloat() // If grading type is Points, don't show the grade since we're already showing it as the score var grade = if (assignment.gradingType != Assignment.POINTS_TYPE) submission.grade.orEmpty() else "" @@ -148,12 +149,15 @@ sealed class GradeCellViewState { var latePenalty = "" var finalGrade = "" + var yourGrade = "" // Adjust for late penalty, if any - if (submission.pointsDeducted ?: 0.0 > 0.0) { + if ((submission.pointsDeducted ?: 0.0) > 0.0) { grade = "" // Grade will be shown in the 'final grade' text val pointsDeducted = NumberHelper.formatDecimal(submission.pointsDeducted!!, 2, true) - latePenalty = context.getString(R.string.latePenalty, pointsDeducted) + val achievedScore = NumberHelper.formatDecimal(submission.enteredScore, 2, true) + yourGrade = context.getString(R.string.yourGrade, achievedScore) + latePenalty = context.getString(R.string.latePenaltyUpdated, pointsDeducted) finalGrade = context.getString(R.string.finalGradeFormatted, submission.grade) } @@ -190,6 +194,7 @@ sealed class GradeCellViewState { grade = grade, gradeContentDescription = accessibleGradeString, gradeCellContentDescription = gradeCellContentDescription, + yourGrade = yourGrade, latePenalty = latePenalty, finalGrade = finalGrade, stats = stats diff --git a/apps/student/src/main/res/layout/view_student_enhanced_grade_cell.xml b/apps/student/src/main/res/layout/view_student_enhanced_grade_cell.xml index fb89c22521..7eeb284f87 100644 --- a/apps/student/src/main/res/layout/view_student_enhanced_grade_cell.xml +++ b/apps/student/src/main/res/layout/view_student_enhanced_grade_cell.xml @@ -273,12 +273,27 @@ android:text="@{viewData.outOf}" android:textColor="@color/textDarkest" android:visibility="@{viewData.outOf.empty ? View.GONE : View.VISIBLE}" - app:layout_constraintBottom_toTopOf="@id/latePenalty" + app:layout_constraintBottom_toTopOf="@id/yourGrade" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toEndOf="@id/chart" app:layout_constraintTop_toBottomOf="@id/grade" tools:text="Out of 100 pts" /> + + + tools:text="89%" /> + tools:text="Out of 100 pts" /> + + + android:textColor="@color/textWarning" + tools:text="Late Penalty: -46 pts" /> + tools:text="Final Grade: 89 pts" /> diff --git a/apps/student/src/test/java/com/instructure/student/test/assignment/details/GradeCellStateTest.kt b/apps/student/src/test/java/com/instructure/student/test/assignment/details/GradeCellStateTest.kt index 461a326251..0f60757041 100644 --- a/apps/student/src/test/java/com/instructure/student/test/assignment/details/GradeCellStateTest.kt +++ b/apps/student/src/test/java/com/instructure/student/test/assignment/details/GradeCellStateTest.kt @@ -314,11 +314,13 @@ class GradeCellStateTest : Assert() { score = 79.0 ) val expected = baseGradedState.copy( - graphPercent = 0.85f, - score = "85", + graphPercent = 0.79f, + score = "79", showPointsLabel = true, - latePenalty = "Late penalty (-6)", - finalGrade = "Final Grade: 79" + yourGrade = "Your Grade: 85 pts", + latePenalty = "Late Penalty: -6 pts", + finalGrade = "Final Grade: 79", + gradeCellContentDescription = "79 Out of 100 points" ) val actual = GradeCellViewState.fromSubmission(context, baseAssignment, submission) assertEquals(expected, actual) diff --git a/libs/pandares/src/main/res/values/strings.xml b/libs/pandares/src/main/res/values/strings.xml index c301e10fc8..e10ab2c072 100644 --- a/libs/pandares/src/main/res/values/strings.xml +++ b/libs/pandares/src/main/res/values/strings.xml @@ -957,6 +957,8 @@ Invite accepted! Invite declined. Late penalty (-%s) + Late Penalty: -%s pts + Your Grade: %s pts Final Grade 99+ From dd1682a39951b92c0b0d5d2fff7730c2aa17bbf9 Mon Sep 17 00:00:00 2001 From: Kristof Deak <92309696+kdeakinstructure@users.noreply.github.com> Date: Thu, 7 Dec 2023 16:21:41 +0100 Subject: [PATCH 031/132] [MBL-17115][Student] - Offline LeftSideMenu Unavailable features E2E test (#2275) --- .../e2e/offline/OfflineLeftSideMenuE2ETest.kt | 101 ++++++++++++++++++ .../student/ui/pages/DashboardPage.kt | 4 + .../ui/pages/LeftSideNavigationDrawerPage.kt | 89 ++++++++++++++- .../espresso/CustomViewAssertions.kt | 1 - .../panda_annotations/TestMetaData.kt | 2 +- 5 files changed, 193 insertions(+), 4 deletions(-) create mode 100644 apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineLeftSideMenuE2ETest.kt diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineLeftSideMenuE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineLeftSideMenuE2ETest.kt new file mode 100644 index 0000000000..b3dedd84bb --- /dev/null +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineLeftSideMenuE2ETest.kt @@ -0,0 +1,101 @@ +/* + * Copyright (C) 2023 - 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.student.ui.e2e.offline + +import android.util.Log +import com.instructure.canvas.espresso.OfflineE2E +import com.instructure.panda_annotations.FeatureCategory +import com.instructure.panda_annotations.Priority +import com.instructure.panda_annotations.TestCategory +import com.instructure.panda_annotations.TestMetaData +import com.instructure.student.ui.e2e.offline.utils.OfflineTestUtils.assertNoInternetConnectionDialog +import com.instructure.student.ui.e2e.offline.utils.OfflineTestUtils.assertOfflineIndicator +import com.instructure.student.ui.e2e.offline.utils.OfflineTestUtils.dismissNoInternetConnectionDialog +import com.instructure.student.ui.utils.StudentTest +import com.instructure.student.ui.utils.seedData +import com.instructure.student.ui.utils.tokenLogin +import dagger.hilt.android.testing.HiltAndroidTest +import org.junit.After +import org.junit.Test + +@HiltAndroidTest +class OfflineLeftSideMenuE2ETest : StudentTest() { + override fun displaysPageObjects() = Unit + + override fun enableAndConfigureAccessibilityChecks() = Unit + + @OfflineE2E + @Test + @TestMetaData(Priority.MANDATORY, FeatureCategory.LEFT_SIDE_MENU, TestCategory.E2E) + fun testOfflineLeftSideMenuUnavailableFunctionsE2E() { + + Log.d(PREPARATION_TAG,"Seeding data.") + val data = seedData(students = 1, teachers = 1, courses = 2, announcements = 1) + val student = data.studentsList[0] + + Log.d(STEP_TAG,"Login with user: ${student.name}, login id: ${student.loginId}.") + tokenLogin(student) + dashboardPage.waitForRender() + + Log.d(PREPARATION_TAG, "Turn off the Wi-Fi and Mobile Data on the device, so it will go offline.") + turnOffConnectionViaADB() + dashboardPage.waitForRender() + + Log.d(STEP_TAG, "Assert that the Offline Indicator (bottom banner) is displayed on the Dashboard Page.") + assertOfflineIndicator() + + Log.d(STEP_TAG, "Open Left Side Menu by clicking on the 'hamburger icon' on the Dashboard Page.") + dashboardPage.openLeftSideMenu() + + Log.d(STEP_TAG, "Assert that the offline indicator is displayed below the user info within the header.") + leftSideNavigationDrawerPage.assertOfflineIndicatorDisplayed() + + Log.d(STEP_TAG, "Assert that the 'Files, Bookmarks, Studio, Color Overlay, Help' menus are disabled in offline mode.") + leftSideNavigationDrawerPage.assertOfflineDisabledMenus(0.5f) + + Log.d(STEP_TAG, "Assert that the 'Settings, Show Grades, Change User, Log Out' menus are enabled in offline mode because they are supported.") + leftSideNavigationDrawerPage.assertOfflineEnabledMenus(1.0f) + + Log.d(STEP_TAG, "Click on 'Files' menu and assert that the 'No Internet Connection' dialog is popping-up. Dismiss it.") + leftSideNavigationDrawerPage.clickFilesMenu() + assertNoInternetConnectionDialog() + dismissNoInternetConnectionDialog() + + Log.d(STEP_TAG, "Click on 'Bookmarks' menu and assert that the 'No Internet Connection' dialog is popping-up. Dismiss it.") + leftSideNavigationDrawerPage.clickBookmarksMenu() + assertNoInternetConnectionDialog() + dismissNoInternetConnectionDialog() + + Log.d(STEP_TAG, "Click on 'Studio' menu and assert that the 'No Internet Connection' dialog is popping-up. Dismiss it.") + leftSideNavigationDrawerPage.clickStudioMenu() + assertNoInternetConnectionDialog() + dismissNoInternetConnectionDialog() + + Log.d(STEP_TAG, "Click on 'Help' menu and assert that the 'No Internet Connection' dialog is popping-up. Dismiss it.") + leftSideNavigationDrawerPage.clickHelpMenu() + assertNoInternetConnectionDialog() + dismissNoInternetConnectionDialog() + + } + + @After + fun tearDown() { + Log.d(PREPARATION_TAG, "Turn back on the Wi-Fi and Mobile Data on the device via ADB, so it will come back online.") + turnOnConnectionViaADB() + } + +} \ No newline at end of file diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/DashboardPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/DashboardPage.kt index a8b51842eb..18971f74a5 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/DashboardPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/DashboardPage.kt @@ -147,6 +147,10 @@ class DashboardPage : BasePage(R.id.dashboardPage) { onView(hamburgerButtonMatcher).waitForCheck(matches(isDisplayed())) } + fun openLeftSideMenu() { + onView(hamburgerButtonMatcher).click() + } + private fun scrollAndAssertDisplayed(matcher: Matcher) { // Arggghhh... This scrolling logic on the recycler view is really unreliable and seems // to fail for nonsensical reasons. For now, "scrollAndAssertDisplayed"" is just going to diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/LeftSideNavigationDrawerPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/LeftSideNavigationDrawerPage.kt index da39b4cefe..0fa4f1c98e 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/LeftSideNavigationDrawerPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/LeftSideNavigationDrawerPage.kt @@ -12,8 +12,22 @@ import com.instructure.canvas.espresso.CanvasTest import com.instructure.canvas.espresso.waitForMatcherWithSleeps import com.instructure.canvasapi2.models.User import com.instructure.dataseeding.model.CanvasUserApiModel -import com.instructure.espresso.* -import com.instructure.espresso.page.* +import com.instructure.espresso.OnViewWithContentDescription +import com.instructure.espresso.OnViewWithId +import com.instructure.espresso.ViewAlphaAssertion +import com.instructure.espresso.assertDisplayed +import com.instructure.espresso.assertNotDisplayed +import com.instructure.espresso.click +import com.instructure.espresso.page.BasePage +import com.instructure.espresso.page.onView +import com.instructure.espresso.page.onViewWithId +import com.instructure.espresso.page.onViewWithText +import com.instructure.espresso.page.waitForView +import com.instructure.espresso.page.waitForViewWithId +import com.instructure.espresso.page.withId +import com.instructure.espresso.scrollTo +import com.instructure.espresso.swipeDown +import com.instructure.espresso.swipeUp import com.instructure.student.R import org.hamcrest.CoreMatchers import org.hamcrest.Matcher @@ -86,6 +100,10 @@ class LeftSideNavigationDrawerPage : BasePage() { clickMenu(R.id.navigationDrawerItem_bookmarks) } + fun clickStudioMenu() { + clickMenu(R.id.navigationDrawerItem_studio) + } + fun clickSettingsMenu() { clickMenu(R.id.navigationDrawerSettings) } @@ -181,6 +199,73 @@ class LeftSideNavigationDrawerPage : BasePage() { logoutButton.assertDisplayed() } + //OfflineMethod + private fun assertViewAlpha(matcher: Matcher, expectedAlphaValue: Float) { + onView(matcher).check(ViewAlphaAssertion(expectedAlphaValue)) + } + + //OfflineMethod + private fun assertFilesMenuAlphaValue(expectedAlphaValue: Float) { + assertViewAlpha(withId(R.id.navigationDrawerItem_files), expectedAlphaValue) + } + + //OfflineMethod + private fun assertBookmarksMenuAlphaValue(expectedAlphaValue: Float) { + assertViewAlpha(withId(R.id.navigationDrawerItem_bookmarks), expectedAlphaValue) + } + + //OfflineMethod + private fun assertStudioMenuAlphaValue(expectedAlphaValue: Float) { + assertViewAlpha(withId(R.id.navigationDrawerItem_studio), expectedAlphaValue) + } + + //OfflineMethod + private fun assertSettingsMenuAlphaValue(expectedAlphaValue: Float) { + assertViewAlpha(withId(R.id.navigationDrawerSettings), expectedAlphaValue) + } + + //OfflineMethod + private fun assertShowGradesMenuAlphaValue(expectedAlphaValue: Float) { + assertViewAlpha(withId(R.id.navigationDrawerItem_showGrades), expectedAlphaValue) + } + + //OfflineMethod + private fun assertColorOverlayMenuAlphaValue(expectedAlphaValue: Float) { + assertViewAlpha(withId(R.id.navigationDrawerItem_colorOverlay), expectedAlphaValue) + } + + //OfflineMethod + private fun assertHelpMenuAlphaValue(expectedAlphaValue: Float) { + assertViewAlpha(withId(R.id.navigationDrawerItem_help), expectedAlphaValue) + } + + //OfflineMethod + private fun assertChangeUserMenuAlphaValue(expectedAlphaValue: Float) { + assertViewAlpha(withId(R.id.navigationDrawerItem_changeUser), expectedAlphaValue) + } + + //OfflineMethod + private fun assertLogoutMenuAlphaValue(expectedAlphaValue: Float) { + assertViewAlpha(withId(R.id.navigationDrawerItem_logout), expectedAlphaValue) + } + + //OfflineMethod + fun assertOfflineDisabledMenus(expectedAlphaValue: Float) { + assertFilesMenuAlphaValue(expectedAlphaValue) + assertBookmarksMenuAlphaValue(expectedAlphaValue) + assertStudioMenuAlphaValue(expectedAlphaValue) + assertColorOverlayMenuAlphaValue(expectedAlphaValue) + assertHelpMenuAlphaValue(expectedAlphaValue) + } + + //OfflineMethod + fun assertOfflineEnabledMenus(expectedAlphaValue: Float) { + assertSettingsMenuAlphaValue(expectedAlphaValue) + assertShowGradesMenuAlphaValue(expectedAlphaValue) + assertChangeUserMenuAlphaValue(expectedAlphaValue) + assertLogoutMenuAlphaValue(expectedAlphaValue) + } + /** * Custom ViewAction to set a SwitchCompat to the desired on/off position * [position]: true -> "on", false -> "off" diff --git a/automation/espresso/src/main/kotlin/com/instructure/espresso/CustomViewAssertions.kt b/automation/espresso/src/main/kotlin/com/instructure/espresso/CustomViewAssertions.kt index 77a1c2d6ec..64eee4f838 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/espresso/CustomViewAssertions.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/espresso/CustomViewAssertions.kt @@ -128,4 +128,3 @@ class ViewAlphaAssertion(private val expectedAlpha: Float): ViewAssertion { assertThat("View alpha should be $expectedAlpha", view.alpha, `is`(expectedAlpha)) } } - diff --git a/libs/panda_annotations/src/main/java/com/instructure/panda_annotations/TestMetaData.kt b/libs/panda_annotations/src/main/java/com/instructure/panda_annotations/TestMetaData.kt index 4400d2346b..4dfec47796 100644 --- a/libs/panda_annotations/src/main/java/com/instructure/panda_annotations/TestMetaData.kt +++ b/libs/panda_annotations/src/main/java/com/instructure/panda_annotations/TestMetaData.kt @@ -34,7 +34,7 @@ enum class Priority { enum class FeatureCategory { ASSIGNMENTS, SUBMISSIONS, LOGIN, COURSE, DASHBOARD, GROUPS, SETTINGS, PAGES, DISCUSSIONS, MODULES, INBOX, GRADES, FILES, EVENTS, PEOPLE, CONFERENCES, COLLABORATIONS, SYLLABUS, TODOS, QUIZZES, NOTIFICATIONS, - ANNOTATIONS, ANNOUNCEMENTS, COMMENTS, BOOKMARKS, NONE, K5_DASHBOARD, SPEED_GRADER, SYNC_SETTINGS, SYNC_PROGRESS, OFFLINE_CONTENT + ANNOTATIONS, ANNOUNCEMENTS, COMMENTS, BOOKMARKS, NONE, K5_DASHBOARD, SPEED_GRADER, SYNC_SETTINGS, SYNC_PROGRESS, OFFLINE_CONTENT, LEFT_SIDE_MENU } enum class SecondaryFeatureCategory { From eae402259694d1e0cf829937adc07c0645342304 Mon Sep 17 00:00:00 2001 From: Kristof Deak <92309696+kdeakinstructure@users.noreply.github.com> Date: Fri, 8 Dec 2023 10:26:48 +0100 Subject: [PATCH 032/132] Stabilize Offline ManageOfflineContentE2ETest. (#2278) --- .../student/ui/e2e/offline/ManageOfflineContentE2ETest.kt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/ManageOfflineContentE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/ManageOfflineContentE2ETest.kt index 3d300a2e1a..431b8d1509 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/ManageOfflineContentE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/ManageOfflineContentE2ETest.kt @@ -73,6 +73,9 @@ class ManageOfflineContentE2ETest : StudentTest() { Log.d(STEP_TAG, "Assert that the tool bar texts are displayed properly, so the subtitle is '${course1.name}', because we are on the Manage Offline Content page of '${course1.name}' course.") manageOfflineContentPage.assertToolbarTexts(course1.name) + Log.d(STEP_TAG, "Assert that the '${course1.name}' course's checkbox state became 'Checked'.") + manageOfflineContentPage.assertCheckedStateOfItem(course1.name, MaterialCheckBox.STATE_CHECKED) + Log.d(STEP_TAG, "Deselect the 'Announcements' and 'Discussions' of the '${course1.name}' course.") manageOfflineContentPage.changeItemSelectionState("Announcements") manageOfflineContentPage.changeItemSelectionState("Discussions") From b2527335642cf573dda3771515a7e6d8e9aeaf88 Mon Sep 17 00:00:00 2001 From: Akos Hermann <72087159+hermannakos@users.noreply.github.com> Date: Fri, 8 Dec 2023 12:19:33 +0100 Subject: [PATCH 033/132] [MBL-17174][Student][Teacher] Dependency update (#2272) Test plan: Smoke test the apps. refs: MBL-17174 affects: Student, Teacher release note: none --- .../student/espresso/TestAppManager.kt | 5 +- .../student/fragment/ViewImageFragment.kt | 12 +++-- .../drawer/files/ui/SubmissionFilesAdapter.kt | 13 +++-- .../student/util/BaseAppManager.kt | 10 ++-- .../teacher/fragments/ViewImageFragment.kt | 12 +++-- .../instructure/teacher/utils/AppManager.kt | 7 +-- .../teacher/utils/BaseAppManager.kt | 10 ++-- buildSrc/src/main/java/GlobalDependencies.kt | 48 +++++++++---------- libs/DocumentScanner/build.gradle | 14 +++--- .../canvasapi2/utils/DataResultTest.kt | 21 +++----- libs/pandautils/build.gradle | 4 +- .../pandautils/utils/AvatarCropUtils.kt | 4 +- .../instructure/pandautils/utils/SvgUtils.kt | 15 +++++- .../pandautils/utils/ViewExtensions.kt | 8 ++-- .../pandautils/views/RecipientChipsInput.kt | 8 ++-- .../DashboardNotificationsViewModelTest.kt | 16 +++---- .../offline/sync/OfflineSyncHelperTest.kt | 10 ++-- .../ShareExtensionProgressViewModelTest.kt | 20 ++++---- 18 files changed, 124 insertions(+), 113 deletions(-) diff --git a/apps/student/src/androidTest/java/com/instructure/student/espresso/TestAppManager.kt b/apps/student/src/androidTest/java/com/instructure/student/espresso/TestAppManager.kt index c99b8cf92d..fc00cc637e 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/espresso/TestAppManager.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/espresso/TestAppManager.kt @@ -16,7 +16,6 @@ */ package com.instructure.student.espresso -import androidx.work.Configuration import androidx.work.WorkerFactory import com.instructure.student.util.BaseAppManager @@ -25,8 +24,6 @@ open class TestAppManager : BaseAppManager() { var workerFactory: WorkerFactory? = null override fun getWorkManagerFactory(): WorkerFactory { - return workerFactory?.let { - it - } ?: WorkerFactory.getDefaultWorkerFactory() + return workerFactory ?: WorkerFactory.getDefaultWorkerFactory() } } \ No newline at end of file diff --git a/apps/student/src/main/java/com/instructure/student/fragment/ViewImageFragment.kt b/apps/student/src/main/java/com/instructure/student/fragment/ViewImageFragment.kt index e5c586603f..bce47bc595 100644 --- a/apps/student/src/main/java/com/instructure/student/fragment/ViewImageFragment.kt +++ b/apps/student/src/main/java/com/instructure/student/fragment/ViewImageFragment.kt @@ -91,7 +91,7 @@ class ViewImageFragment : Fragment(), ShareableFile { private val requestListener = object : RequestListener { - override fun onLoadFailed(p0: GlideException?, p1: Any?, p2: Target?, p3: Boolean): Boolean { + override fun onLoadFailed(p0: GlideException?, p1: Any?, target: Target, p3: Boolean): Boolean { binding.photoView.setGone() binding.progressBar.setGone() binding.errorContainer.setVisible() @@ -100,11 +100,17 @@ class ViewImageFragment : Fragment(), ShareableFile { return false } - override fun onResourceReady(drawable: Drawable?, p1: Any?, p2: Target?, p3: DataSource?, p4: Boolean): Boolean { + override fun onResourceReady( + resource: Drawable, + model: Any, + p2: Target?, + dataSource: DataSource, + p4: Boolean + ): Boolean { binding.progressBar.setGone() // Try to set the background color using palette if we can - (drawable as? BitmapDrawable)?.bitmap?.let { colorBackground(it) } + (resource as? BitmapDrawable)?.bitmap?.let { colorBackground(it) } return false } } diff --git a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/drawer/files/ui/SubmissionFilesAdapter.kt b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/drawer/files/ui/SubmissionFilesAdapter.kt index f9018ce7f9..0c1c06eb89 100644 --- a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/drawer/files/ui/SubmissionFilesAdapter.kt +++ b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/drawer/files/ui/SubmissionFilesAdapter.kt @@ -69,17 +69,22 @@ internal class SubmissionFilesHolder(view: View) : RecyclerView.ViewHolder(view) thumbnail.setVisible(data.thumbnailUrl.isValid()) data.thumbnailUrl.validOrNull()?.let { Glide.with(root.context).load(it).listener(object : RequestListener { - override fun onLoadFailed(e: GlideException?, model: Any?, target: Target?, isFirstResource: Boolean): Boolean { + override fun onLoadFailed( + e: GlideException?, + model: Any?, + target: Target, + isFirstResource: Boolean + ): Boolean { fileIcon.setVisible(true) thumbnail.setVisible(false) return false } override fun onResourceReady( - resource: Drawable?, - model: Any?, + resource: Drawable, + model: Any, target: Target?, - dataSource: DataSource?, + dataSource: DataSource, isFirstResource: Boolean ): Boolean { return false diff --git a/apps/student/src/main/java/com/instructure/student/util/BaseAppManager.kt b/apps/student/src/main/java/com/instructure/student/util/BaseAppManager.kt index 51b573aaa4..09f8856ab6 100644 --- a/apps/student/src/main/java/com/instructure/student/util/BaseAppManager.kt +++ b/apps/student/src/main/java/com/instructure/student/util/BaseAppManager.kt @@ -49,6 +49,11 @@ import io.flutter.embedding.engine.dart.DartExecutor abstract class BaseAppManager : com.instructure.canvasapi2.AppManager(), AnalyticsEventHandling, Configuration.Provider { + override val workManagerConfiguration: Configuration + get() = Configuration.Builder() + .setWorkerFactory(getWorkManagerFactory()) + .build() + override fun onCreate() { super.onCreate() @@ -153,11 +158,6 @@ abstract class BaseAppManager : com.instructure.canvasapi2.AppManager(), Analyti override fun performLogoutOnAuthError() = Unit - override fun getWorkManagerConfiguration(): Configuration = - Configuration.Builder() - .setWorkerFactory(getWorkManagerFactory()) - .build() - abstract fun getWorkManagerFactory(): WorkerFactory companion object { diff --git a/apps/teacher/src/main/java/com/instructure/teacher/fragments/ViewImageFragment.kt b/apps/teacher/src/main/java/com/instructure/teacher/fragments/ViewImageFragment.kt index 1cd5cc518a..26488116a0 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/fragments/ViewImageFragment.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/fragments/ViewImageFragment.kt @@ -109,7 +109,7 @@ class ViewImageFragment : Fragment(), ShareableFile { private val requestListener = object : RequestListener { - override fun onLoadFailed(p0: GlideException?, p1: Any?, p2: Target?, p3: Boolean): Boolean = with(binding) { + override fun onLoadFailed(p0: GlideException?, p1: Any?, target: Target, p3: Boolean): Boolean = with(binding) { photoView.setGone() progressBar.setGone() errorContainer.setVisible() @@ -118,11 +118,17 @@ class ViewImageFragment : Fragment(), ShareableFile { return false } - override fun onResourceReady(bitmap: Bitmap?, p1: Any?, p2: Target?, p3: DataSource?, p4: Boolean): Boolean { + override fun onResourceReady( + resource: Bitmap, + model: Any, + p2: Target?, + dataSource: DataSource, + p4: Boolean + ): Boolean { binding.progressBar.setGone() // Try to set the background color using palette if we can - bitmap?.let { colorBackground(it) } + colorBackground(resource) return false } } diff --git a/apps/teacher/src/main/java/com/instructure/teacher/utils/AppManager.kt b/apps/teacher/src/main/java/com/instructure/teacher/utils/AppManager.kt index 34cb5dcc21..d87159491f 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/utils/AppManager.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/utils/AppManager.kt @@ -23,15 +23,10 @@ import dagger.hilt.android.HiltAndroidApp import javax.inject.Inject @HiltAndroidApp -class AppManager : BaseAppManager(), Configuration.Provider { +class AppManager : BaseAppManager() { @Inject lateinit var workerFactory: HiltWorkerFactory - override fun getWorkManagerConfiguration(): Configuration = - Configuration.Builder() - .setWorkerFactory(workerFactory) - .build() - override fun getWorkManagerFactory(): WorkerFactory = workerFactory } diff --git a/apps/teacher/src/main/java/com/instructure/teacher/utils/BaseAppManager.kt b/apps/teacher/src/main/java/com/instructure/teacher/utils/BaseAppManager.kt index f79b57adcc..5acca986bb 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/utils/BaseAppManager.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/utils/BaseAppManager.kt @@ -43,6 +43,11 @@ import com.pspdfkit.exceptions.PSPDFKitInitializationFailedException abstract class BaseAppManager : com.instructure.canvasapi2.AppManager(), Configuration.Provider { + override val workManagerConfiguration: Configuration + get() = Configuration.Builder() + .setWorkerFactory(getWorkManagerFactory()) + .build() + override fun onCreate() { super.onCreate() @@ -98,11 +103,6 @@ abstract class BaseAppManager : com.instructure.canvasapi2.AppManager(), Configu TeacherLogoutTask(LogoutTask.Type.LOGOUT).execute() } - override fun getWorkManagerConfiguration(): Configuration = - Configuration.Builder() - .setWorkerFactory(getWorkManagerFactory()) - .build() - abstract fun getWorkManagerFactory(): WorkerFactory companion object { diff --git a/buildSrc/src/main/java/GlobalDependencies.kt b/buildSrc/src/main/java/GlobalDependencies.kt index 8ad33f36dd..287dfe6e02 100644 --- a/buildSrc/src/main/java/GlobalDependencies.kt +++ b/buildSrc/src/main/java/GlobalDependencies.kt @@ -27,17 +27,17 @@ object Versions { const val PSPDFKIT = "8.9.1" const val PHOTO_VIEW = "2.3.0" const val MOBIUS = "1.2.1" - const val SQLDELIGHT = "1.5.4" - const val HILT = "2.48" - const val HILT_ANDROIDX = "1.0.0" - const val LIFECYCLE = "2.6.0" - const val FRAGMENT = "1.5.5" - const val WORK_MANAGER = "2.8.1" - const val GLIDE_VERSION = "4.15.1" + const val SQLDELIGHT = "1.5.4" // 2.0 is out but may break stuff. We should look into migrating to Room. + const val HILT = "2.49" + const val HILT_ANDROIDX = "1.1.0" + const val LIFECYCLE = "2.6.2" + const val FRAGMENT = "1.6.2" + const val WORK_MANAGER = "2.9.0" + const val GLIDE_VERSION = "4.16.0" const val RETROFIT = "2.9.0" - const val OKHTTP = "4.10.0" - const val HEAP = "1.10.5" - const val ROOM = "2.6.0" + const val OKHTTP = "4.12.0" + const val HEAP = "1.10.6" + const val ROOM = "2.6.1" const val HAMCREST = "2.2" } @@ -54,35 +54,35 @@ object Libs { const val APOLLO_HTTP_CACHE = "com.apollographql.apollo:apollo-http-cache:${Versions.APOLLO}" /* Support Libs */ - const val ANDROIDX_ANNOTATION = "androidx.annotation:annotation:1.6.0" + const val ANDROIDX_ANNOTATION = "androidx.annotation:annotation:1.7.0" const val ANDROIDX_APPCOMPAT = "androidx.appcompat:appcompat:1.6.1" - const val ANDROIDX_BROWSER = "androidx.browser:browser:1.5.0" + const val ANDROIDX_BROWSER = "androidx.browser:browser:1.7.0" const val ANDROIDX_CARDVIEW = "androidx.cardview:cardview:1.0.0" const val ANDROIDX_CONSTRAINT_LAYOUT = "androidx.constraintlayout:constraintlayout:2.1.4" - const val ANDROIDX_DESIGN = "com.google.android.material:material:1.8.0" + const val ANDROIDX_DESIGN = "com.google.android.material:material:1.10.0" const val ANDROIDX_EXIF = "androidx.exifinterface:exifinterface:1.3.6" const val ANDROIDX_FRAGMENT = "androidx.fragment:fragment:${Versions.FRAGMENT}" const val FRAGMENT_KTX = "androidx.fragment:fragment-ktx:${Versions.FRAGMENT}" const val ANDROIDX_PALETTE = "androidx.palette:palette:1.0.0" const val ANDROIDX_PERCENT = "androidx.percentlayout:percentlayout:1.0.0" - const val ANDROIDX_RECYCLERVIEW = "androidx.recyclerview:recyclerview:1.3.0" + const val ANDROIDX_RECYCLERVIEW = "androidx.recyclerview:recyclerview:1.3.2" const val ANDROIDX_VECTOR = "androidx.vectordrawable:vectordrawable:1.1.0" const val ANDROIDX_SWIPE_REFRESH_LAYOUT = "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0" const val ANDROIDX_CORE_TESTING = "androidx.arch.core:core-testing:2.2.0" const val ANDROIDX_WORK_MANAGER = "androidx.work:work-runtime:${Versions.WORK_MANAGER}" const val ANDROIDX_WORK_MANAGER_KTX = "androidx.work:work-runtime-ktx:${Versions.WORK_MANAGER}" - const val ANDROIDX_WEBKIT = "androidx.webkit:webkit:1.6.0" + const val ANDROIDX_WEBKIT = "androidx.webkit:webkit:1.9.0" const val ANDROIDX_DATABINDING_COMPILER = "androidx.databinding:databinding-compiler:${Versions.ANDROID_GRADLE_TOOLS}" // This is bundled with the gradle plugin so we use the same version /* Firebase */ - const val FIREBASE_BOM = "com.google.firebase:firebase-bom:31.2.3" + const val FIREBASE_BOM = "com.google.firebase:firebase-bom:32.6.0" const val FIREBASE_CRASHLYTICS = "com.google.firebase:firebase-crashlytics" const val FIREBASE_MESSAGING = "com.google.firebase:firebase-messaging" const val FIREBASE_CONFIG = "com.google.firebase:firebase-config" const val FIREBASE_CRASHLYTICS_NDK = "com.google.firebase:firebase-crashlytics-ndk" /* Play Services */ - const val PLAY_IN_APP_UPDATES = "com.google.android.play:app-update:2.0.1" + const val PLAY_IN_APP_UPDATES = "com.google.android.play:app-update:2.1.0" const val FLEXBOX_LAYOUT = "com.google.android.flexbox:flexbox:3.0.0" /* Mobius */ @@ -95,8 +95,8 @@ object Libs { const val JUNIT = "junit:junit:${Versions.JUNIT}" const val ROBOLECTRIC = "org.robolectric:robolectric:${Versions.ROBOLECTRIC}" const val ANDROIDX_TEST_JUNIT = "androidx.test.ext:junit:1.1.5" - const val MOCKK = "io.mockk:mockk:1.12.3" - const val THREETEN_BP = "org.threeten:threetenbp:1.6.5" + const val MOCKK = "io.mockk:mockk:1.12.8" + const val THREETEN_BP = "org.threeten:threetenbp:1.6.8" const val UI_AUTOMATOR = "com.android.support.test.uiautomator:uiautomator-v18:2.1.3" const val TEST_ORCHESTRATOR = "androidx.test:orchestrator:1.4.2" @@ -120,14 +120,14 @@ object Libs { /* Media and content handling */ const val PSPDFKIT = "com.pspdfkit:pspdfkit:${Versions.PSPDFKIT}" - const val EXOPLAYER = "com.google.android.exoplayer:exoplayer:2.18.5" + const val EXOPLAYER = "com.google.android.exoplayer:exoplayer:2.18.5" // This is deprecated, we should migrate to https://developer.android.com/guide/topics/media/media3/getting-started/migration-guide const val PHOTO_VIEW = "com.github.chrisbanes:PhotoView:${Versions.PHOTO_VIEW}" const val ANDROID_SVG = "com.caverock:androidsvg:1.4" const val RICH_EDITOR = "jp.wasabeef:richeditor-android:2.0.0" const val GLIDE = "com.github.bumptech.glide:glide:${Versions.GLIDE_VERSION}" const val GLIDE_OKHTTP = "com.github.bumptech.glide:okhttp3-integration:${Versions.GLIDE_VERSION}" const val GLIDE_COMPILER = "com.github.bumptech.glide:compiler:${Versions.GLIDE_VERSION}" - const val SCALE_IMAGE_VIEW = "com.davemorrissey.labs:subsampling-scale-image-view:3.9.0" + const val SCALE_IMAGE_VIEW = "com.davemorrissey.labs:subsampling-scale-image-view:3.10.0" /* Network */ const val RETROFIT = "com.squareup.retrofit2:retrofit:${Versions.RETROFIT}" @@ -138,15 +138,15 @@ object Libs { const val OKHTTP = "com.squareup.okhttp3:okhttp:${Versions.OKHTTP}" const val OKHTTP_LOGGING = "com.squareup.okhttp3:logging-interceptor:${Versions.OKHTTP}" const val OKHTTP_URL_CONNECTION = "com.squareup.okhttp3:okhttp-urlconnection:${Versions.OKHTTP}" - const val OKIO = "com.squareup.okio:okio:3.3.0" + const val OKIO = "com.squareup.okio:okio:3.6.0" /* Other */ - const val LOTTIE = "com.airbnb.android:lottie:6.0.0" + const val LOTTIE = "com.airbnb.android:lottie:6.2.0" const val SLIDING_UP_PANEL = "com.sothree.slidinguppanel:library:3.3.1" const val SQLDELIGHT = "com.squareup.sqldelight:android-driver:${Versions.SQLDELIGHT}" const val DISK_LRU_CACHE = "com.jakewharton:disklrucache:2.0.2" const val EVENTBUS = "org.greenrobot:eventbus:3.3.1" - const val JW_THREETEN_BP = "com.jakewharton.threetenabp:threetenabp:1.4.4" + const val JW_THREETEN_BP = "com.jakewharton.threetenabp:threetenabp:1.4.6" const val PROCESS_PHOENIX = "com.jakewharton:process-phoenix:2.1.2" const val PAPERDB = "io.github.pilgr:paperdb:2.7.2" const val KEYBOARD_VISIBILITY_LISTENER = "net.yslibrary.keyboardvisibilityevent:keyboardvisibilityevent:2.2.1" diff --git a/libs/DocumentScanner/build.gradle b/libs/DocumentScanner/build.gradle index 47145c3f4d..7c7bf810a8 100644 --- a/libs/DocumentScanner/build.gradle +++ b/libs/DocumentScanner/build.gradle @@ -63,20 +63,20 @@ dependencies { implementation fileTree(dir: "libs", include: ["*.jar"]) implementation Libs.KOTLIN_STD_LIB - implementation 'androidx.core:core-ktx:1.3.2' - implementation 'androidx.appcompat:appcompat:1.2.0' + implementation 'androidx.core:core-ktx:1.12.0' + implementation 'androidx.appcompat:appcompat:1.6.1' - implementation 'io.reactivex.rxjava3:rxandroid:3.0.0' + implementation 'io.reactivex.rxjava3:rxandroid:3.0.2' implementation 'com.github.zynkware:Tiny-OpenCV:4.4.0-4' - implementation "androidx.camera:camera-camera2:1.0.0" - implementation "androidx.camera:camera-lifecycle:1.0.0" - implementation "androidx.camera:camera-view:1.0.0-alpha24" + implementation "androidx.camera:camera-camera2:1.3.0" + implementation "androidx.camera:camera-lifecycle:1.3.0" + implementation "androidx.camera:camera-view:1.4.0-alpha02" implementation 'com.github.tbruyelle:rxpermissions:0.12' - implementation 'androidx.exifinterface:exifinterface:1.3.2' + implementation 'androidx.exifinterface:exifinterface:1.3.6' implementation Libs.KOTLIN_COROUTINES_ANDROID implementation 'id.zelory:compressor:3.0.1' } diff --git a/libs/canvas-api-2/src/test/java/com/instructure/canvasapi2/utils/DataResultTest.kt b/libs/canvas-api-2/src/test/java/com/instructure/canvasapi2/utils/DataResultTest.kt index df82ca9630..3eae2c1028 100644 --- a/libs/canvas-api-2/src/test/java/com/instructure/canvasapi2/utils/DataResultTest.kt +++ b/libs/canvas-api-2/src/test/java/com/instructure/canvasapi2/utils/DataResultTest.kt @@ -87,8 +87,7 @@ class DataResultTest : Assert() { val result: DataResult = DataResult.Success(expectedValue) val block = mockk<(String) -> Unit>(relaxed = true) result.onSuccess(block) - verify { block.invoke(expectedValue) } - confirmVerified() + verify(exactly = 1) { block.invoke(expectedValue) } } @Test @@ -97,7 +96,6 @@ class DataResultTest : Assert() { val block = mockk<(String) -> Unit>(relaxed = true) result.onSuccess(block) verify(exactly = 0) { block.invoke(capture(slot())) } - confirmVerified() } @Test @@ -106,8 +104,7 @@ class DataResultTest : Assert() { val result: DataResult = DataResult.Fail(expectedFailure) val block = mockk<(Failure) -> Unit>(relaxed = true) result.onFail(block) - verify { block.invoke(expectedFailure) } - confirmVerified() + verify(exactly = 1) { block.invoke(expectedFailure) } } @Test @@ -116,8 +113,7 @@ class DataResultTest : Assert() { val result: DataResult = DataResult.Fail(expectedFailure) val block = mockk<(Failure) -> Unit>(relaxed = true) result.onFail(block) - verify { block.invoke(expectedFailure) } - confirmVerified() + verify(exactly = 1) { block.invoke(expectedFailure) } } @Test @@ -126,8 +122,7 @@ class DataResultTest : Assert() { val result: DataResult = DataResult.Fail(expectedFailure) val block = mockk<(Failure) -> Unit>(relaxed = true) result.onFail(block) - verify { block.invoke(expectedFailure) } - confirmVerified() + verify(exactly = 1) { block.invoke(expectedFailure) } } @Test @@ -135,8 +130,7 @@ class DataResultTest : Assert() { val result: DataResult = DataResult.Fail() val block = mockk<(Failure?) -> Unit>(relaxed = true) result.onFailure(block) - verify { block.invoke(null) } - confirmVerified() + verify(exactly = 1) { block.invoke(null) } } @Test @@ -145,8 +139,7 @@ class DataResultTest : Assert() { val result: DataResult = DataResult.Fail(expectedFailure) val block = mockk<(Failure?) -> Unit>(relaxed = true) result.onFailure(block) - verify { block.invoke(expectedFailure) } - confirmVerified() + verify(exactly = 1) { block.invoke(expectedFailure) } } @Test @@ -155,7 +148,6 @@ class DataResultTest : Assert() { val block = mockk<(Failure?) -> Unit>(relaxed = true) result.onFailure(block) verify(exactly = 0) { block.invoke(capture(slot())) } - confirmVerified() } @Test @@ -164,6 +156,5 @@ class DataResultTest : Assert() { val block = mockk<(Failure) -> Unit>(relaxed = true) result.onFail(block) verify(exactly = 0) { block.invoke(capture(slot())) } - confirmVerified() } } diff --git a/libs/pandautils/build.gradle b/libs/pandautils/build.gradle index e0adde4efa..2353288f5c 100644 --- a/libs/pandautils/build.gradle +++ b/libs/pandautils/build.gradle @@ -125,8 +125,8 @@ dependencies { implementation Libs.KOTLIN_STD_LIB implementation Libs.ANDROIDX_BROWSER implementation 'androidx.legacy:legacy-support-v4:1.0.0' - implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.4.1' - implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.1' + implementation Libs.LIVE_DATA + implementation Libs.VIEW_MODEL /* Test Dependencies */ testImplementation Libs.JUNIT diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/utils/AvatarCropUtils.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/utils/AvatarCropUtils.kt index 2932710efc..2c9a8a6c93 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/utils/AvatarCropUtils.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/utils/AvatarCropUtils.kt @@ -199,8 +199,8 @@ class AvatarCropActivity : AppCompatActivity() { * crop boundary as a percentage (0f to 1f) of the source image dimensions. */ private fun getCropInfo(): RectF = with(binding) { - val origin = imageView.viewToSourceCoord(0f, 0f) - val dimen = imageView.viewToSourceCoord(imageView.width.toFloat(), imageView.height.toFloat()) + val origin = imageView.viewToSourceCoord(0f, 0f) ?: return RectF(0f, 0f, 1f, 1f) + val dimen = imageView.viewToSourceCoord(imageView.width.toFloat(), imageView.height.toFloat()) ?: return RectF(0f, 0f, 1f, 1f) val (appliedWidth, appliedHeight) = when (imageView.appliedOrientation) { 90, 270 -> imageView.sHeight to imageView.sWidth else -> imageView.sWidth to imageView.sHeight diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/utils/SvgUtils.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/utils/SvgUtils.kt index 99bb9cf272..e4eb8050de 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/utils/SvgUtils.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/utils/SvgUtils.kt @@ -91,12 +91,23 @@ internal class SvgDrawableTranscoder : ResourceTranscoder * can't render on a hardware backed [Canvas][android.graphics.Canvas]. */ internal class SvgSoftwareLayerSetter : RequestListener { - override fun onResourceReady(resource: PictureDrawable?, model: Any?, target: Target?, dataSource: DataSource?, isFirstResource: Boolean): Boolean { + override fun onResourceReady( + resource: PictureDrawable, + model: Any, + target: Target?, + dataSource: DataSource, + isFirstResource: Boolean + ): Boolean { (target as? ImageViewTarget)?.view?.setLayerType(ImageView.LAYER_TYPE_SOFTWARE, null) return false } - override fun onLoadFailed(p0: GlideException?, model: Any?, target: Target?, isFirstResource: Boolean): Boolean { + override fun onLoadFailed( + e: GlideException?, + model: Any?, + target: Target, + isFirstResource: Boolean + ): Boolean { (target as? ImageViewTarget)?.view?.setLayerType(ImageView.LAYER_TYPE_NONE, null) return false } diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/utils/ViewExtensions.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/utils/ViewExtensions.kt index 1980d35c8b..1189f32411 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/utils/ViewExtensions.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/utils/ViewExtensions.kt @@ -781,7 +781,7 @@ fun ImageView.loadCircularImage( override fun onLoadFailed( e: GlideException?, model: Any?, - target: Target?, + target: Target, isFirstResource: Boolean ): Boolean { onFailure?.invoke() @@ -789,10 +789,10 @@ fun ImageView.loadCircularImage( } override fun onResourceReady( - resource: Bitmap?, - model: Any?, + resource: Bitmap, + model: Any, target: Target?, - dataSource: DataSource?, + dataSource: DataSource, isFirstResource: Boolean ): Boolean { return false diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/views/RecipientChipsInput.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/views/RecipientChipsInput.kt index 5a91c11bcd..a09bed935f 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/views/RecipientChipsInput.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/views/RecipientChipsInput.kt @@ -196,7 +196,7 @@ class RecipientChip(context: Context, val recipient: Recipient, onRemoved: (Stri override fun onLoadFailed( e: GlideException?, model: Any?, - target: Target?, + target: Target, isFirstResource: Boolean ): Boolean { chipIcon = placeholder @@ -204,10 +204,10 @@ class RecipientChip(context: Context, val recipient: Recipient, onRemoved: (Stri } override fun onResourceReady( - resource: Drawable?, - model: Any?, + resource: Drawable, + model: Any, target: Target?, - source: DataSource?, + source: DataSource, isFirstResource: Boolean ): Boolean { chipIcon = resource diff --git a/libs/pandautils/src/test/java/com/instructure/pandautils/features/dashboard/notifications/DashboardNotificationsViewModelTest.kt b/libs/pandautils/src/test/java/com/instructure/pandautils/features/dashboard/notifications/DashboardNotificationsViewModelTest.kt index 7a917177c8..5343a8829a 100644 --- a/libs/pandautils/src/test/java/com/instructure/pandautils/features/dashboard/notifications/DashboardNotificationsViewModelTest.kt +++ b/libs/pandautils/src/test/java/com/instructure/pandautils/features/dashboard/notifications/DashboardNotificationsViewModelTest.kt @@ -505,8 +505,8 @@ class DashboardNotificationsViewModelTest { WorkInfo( workerId, WorkInfo.State.RUNNING, + emptySet(), Data.EMPTY, - emptyList(), Data.Builder() .putString(FileUploadWorker.PROGRESS_DATA_TITLE, title) .putString(FileUploadWorker.PROGRESS_DATA_ASSIGNMENT_NAME, subTitle) @@ -522,8 +522,8 @@ class DashboardNotificationsViewModelTest { WorkInfo( workerId2, WorkInfo.State.SUCCEEDED, + emptySet(), Data.EMPTY, - emptyList(), Data.Builder() .putString(FileUploadWorker.PROGRESS_DATA_TITLE, title2) .putString(FileUploadWorker.PROGRESS_DATA_ASSIGNMENT_NAME, subTitle2) @@ -539,8 +539,8 @@ class DashboardNotificationsViewModelTest { WorkInfo( workerId3, WorkInfo.State.FAILED, + emptySet(), Data.EMPTY, - emptyList(), Data.Builder() .putString(FileUploadWorker.PROGRESS_DATA_TITLE, title3) .putString(FileUploadWorker.PROGRESS_DATA_ASSIGNMENT_NAME, subTitle3) @@ -586,8 +586,8 @@ class DashboardNotificationsViewModelTest { WorkInfo( workerId, WorkInfo.State.RUNNING, + emptySet(), Data.EMPTY, - emptyList(), Data.EMPTY, 1, 1 @@ -603,8 +603,8 @@ class DashboardNotificationsViewModelTest { WorkInfo( workerId, WorkInfo.State.SUCCEEDED, + emptySet(), Data.EMPTY, - emptyList(), Data.EMPTY, 1, 1 @@ -631,8 +631,8 @@ class DashboardNotificationsViewModelTest { WorkInfo( workerId, WorkInfo.State.RUNNING, + emptySet(), Data.EMPTY, - emptyList(), Data.EMPTY, 1, 1 @@ -666,8 +666,8 @@ class DashboardNotificationsViewModelTest { WorkInfo( workerId, WorkInfo.State.SUCCEEDED, + emptySet(), Data.EMPTY, - emptyList(), Data.EMPTY, 1, 1 @@ -707,8 +707,8 @@ class DashboardNotificationsViewModelTest { WorkInfo( workerId, WorkInfo.State.SUCCEEDED, + emptySet(), Data.EMPTY, - emptyList(), Data.EMPTY, 1, 1 diff --git a/libs/pandautils/src/test/java/com/instructure/pandautils/features/offline/sync/OfflineSyncHelperTest.kt b/libs/pandautils/src/test/java/com/instructure/pandautils/features/offline/sync/OfflineSyncHelperTest.kt index d84a44897d..f4bd92e64e 100644 --- a/libs/pandautils/src/test/java/com/instructure/pandautils/features/offline/sync/OfflineSyncHelperTest.kt +++ b/libs/pandautils/src/test/java/com/instructure/pandautils/features/offline/sync/OfflineSyncHelperTest.kt @@ -262,7 +262,7 @@ class OfflineSyncHelperTest { every { workManager.getWorkInfosForUniqueWork(any()) } returns Futures.immediateFuture( listOf( - WorkInfo(originalRequest.id, WorkInfo.State.ENQUEUED, Data.EMPTY, emptyList(), Data.EMPTY, 1, 1) + WorkInfo(originalRequest.id, WorkInfo.State.ENQUEUED, emptySet(), Data.EMPTY, Data.EMPTY, 1, 1) ) ) @@ -281,7 +281,7 @@ class OfflineSyncHelperTest { every { workManager.getWorkInfosForUniqueWork(any()) } returns Futures.immediateFuture(emptyList()) every { workManager.getWorkInfosByTag(OfflineSyncWorker.ONE_TIME_TAG) } returns Futures.immediateFuture( listOf( - WorkInfo(uuid, WorkInfo.State.RUNNING, Data.EMPTY, emptyList(), Data.EMPTY, 1, 1) + WorkInfo(uuid, WorkInfo.State.RUNNING, emptySet(), Data.EMPTY, Data.EMPTY, 1, 1) ) ) @@ -297,7 +297,7 @@ class OfflineSyncHelperTest { val uuid = UUID.randomUUID() every { workManager.getWorkInfosForUniqueWork(any()) } returns Futures.immediateFuture( listOf( - WorkInfo(uuid, WorkInfo.State.RUNNING, Data.EMPTY, emptyList(), Data.EMPTY, 1, 1) + WorkInfo(uuid, WorkInfo.State.RUNNING, emptySet(), Data.EMPTY, Data.EMPTY, 1, 1) ) ) every { workManager.getWorkInfosByTag(OfflineSyncWorker.ONE_TIME_TAG) } returns Futures.immediateFuture( @@ -350,7 +350,7 @@ class OfflineSyncHelperTest { val uuid = UUID.randomUUID() every { workManager.getWorkInfosForUniqueWork(any()) } returns Futures.immediateFuture( listOf( - WorkInfo(uuid, WorkInfo.State.RUNNING, Data.EMPTY, emptyList(), Data.EMPTY, 1, 1) + WorkInfo(uuid, WorkInfo.State.RUNNING, emptySet(), Data.EMPTY, Data.EMPTY, 1, 1) ) ) every { workManager.getWorkInfosByTag(OfflineSyncWorker.ONE_TIME_TAG) } returns Futures.immediateFuture( @@ -377,7 +377,7 @@ class OfflineSyncHelperTest { ) every { workManager.getWorkInfosByTag(OfflineSyncWorker.ONE_TIME_TAG) } returns Futures.immediateFuture( listOf( - WorkInfo(uuid, WorkInfo.State.RUNNING, Data.EMPTY, emptyList(), Data.EMPTY, 1, 1) + WorkInfo(uuid, WorkInfo.State.RUNNING, emptySet(), Data.EMPTY, Data.EMPTY, 1, 1) ) ) diff --git a/libs/pandautils/src/test/java/com/instructure/pandautils/features/shareextension/progress/ShareExtensionProgressViewModelTest.kt b/libs/pandautils/src/test/java/com/instructure/pandautils/features/shareextension/progress/ShareExtensionProgressViewModelTest.kt index 6b1eb762ff..77a3757aca 100644 --- a/libs/pandautils/src/test/java/com/instructure/pandautils/features/shareextension/progress/ShareExtensionProgressViewModelTest.kt +++ b/libs/pandautils/src/test/java/com/instructure/pandautils/features/shareextension/progress/ShareExtensionProgressViewModelTest.kt @@ -71,7 +71,7 @@ class ShareExtensionProgressViewModelTest { @Test fun `Show success dialog after uploading`() { viewModel.setUUID(uuid) - mockLiveData.postValue(WorkInfo(uuid, WorkInfo.State.SUCCEEDED, Data.EMPTY, emptyList(), Data.EMPTY, 1, 1)) + mockLiveData.postValue(WorkInfo(uuid, WorkInfo.State.SUCCEEDED, emptySet(), Data.EMPTY, Data.EMPTY, 1, 1)) viewModel.events.observe(lifecycleOwner) {} assertEquals(ShareExtensionProgressAction.ShowSuccessDialog(FileUploadType.USER), viewModel.events.value?.getContentIfNotHandled()) @@ -86,7 +86,7 @@ class ShareExtensionProgressViewModelTest { .putStringArray(FileUploadWorker.PROGRESS_DATA_FILES_TO_UPLOAD, emptyArray()) .build() - mockLiveData.postValue(WorkInfo(uuid, WorkInfo.State.FAILED, outputData, emptyList(), Data.EMPTY, 1, 1)) + mockLiveData.postValue(WorkInfo(uuid, WorkInfo.State.FAILED, emptySet(), outputData, Data.EMPTY, 1, 1)) assertEquals("Error", viewModel.data.value?.subtitle) } @@ -124,8 +124,8 @@ class ShareExtensionProgressViewModelTest { WorkInfo( uuid, WorkInfo.State.RUNNING, + emptySet(), Data.EMPTY, - emptyList(), progressData, 1, 1 @@ -156,8 +156,8 @@ class ShareExtensionProgressViewModelTest { WorkInfo( uuid, WorkInfo.State.RUNNING, + emptySet(), Data.EMPTY, - emptyList(), progressData, 1, 1 @@ -213,8 +213,8 @@ class ShareExtensionProgressViewModelTest { WorkInfo( uuid, WorkInfo.State.RUNNING, + emptySet(), Data.EMPTY, - emptyList(), progressData.build(), 1, 1 @@ -248,8 +248,8 @@ class ShareExtensionProgressViewModelTest { WorkInfo( uuid, WorkInfo.State.RUNNING, + emptySet(), Data.EMPTY, - emptyList(), progressData.build(), 1, 1 @@ -290,8 +290,8 @@ class ShareExtensionProgressViewModelTest { WorkInfo( uuid, WorkInfo.State.FAILED, + emptySet(), progressData, - emptyList(), Data.EMPTY, 1, 1 @@ -342,8 +342,8 @@ class ShareExtensionProgressViewModelTest { WorkInfo( uuid, WorkInfo.State.FAILED, + emptySet(), failedOutputData, - emptyList(), Data.EMPTY, 1, 1 @@ -379,8 +379,8 @@ class ShareExtensionProgressViewModelTest { WorkInfo( uuid, WorkInfo.State.RUNNING, + emptySet(), Data.EMPTY, - emptyList(), successProgressData, 1, 1 @@ -395,8 +395,8 @@ class ShareExtensionProgressViewModelTest { WorkInfo( uuid, WorkInfo.State.SUCCEEDED, + emptySet(), Data.EMPTY, - emptyList(), successProgressData, 1, 1 From 6c1fce5feca02f97ee2791a30d999709285c5202 Mon Sep 17 00:00:00 2001 From: Kristof Nemere <109959688+kristofnemere@users.noreply.github.com> Date: Fri, 8 Dec 2023 12:31:02 +0100 Subject: [PATCH 034/132] [MBL-17237][Teacher] SpeedGraderCommentHolder crash refs: MBL-17237 affects: Teacher, Student release note: none --- .../mobius/common/ui/MobiusFragment.kt | 27 ++++++++++++------- .../holders/SpeedGraderCommentHolder.kt | 18 +++++++++---- .../mobius/common/ui/MobiusFragment.kt | 26 +++++++++++------- .../pandautils/utils/ActivityExtensions.kt | 10 ++++++- 4 files changed, 55 insertions(+), 26 deletions(-) diff --git a/apps/student/src/main/java/com/instructure/student/mobius/common/ui/MobiusFragment.kt b/apps/student/src/main/java/com/instructure/student/mobius/common/ui/MobiusFragment.kt index c1e5000be0..bd4b8b96b5 100644 --- a/apps/student/src/main/java/com/instructure/student/mobius/common/ui/MobiusFragment.kt +++ b/apps/student/src/main/java/com/instructure/student/mobius/common/ui/MobiusFragment.kt @@ -18,7 +18,6 @@ package com.instructure.student.mobius.common.ui import android.app.Activity import android.content.Context -import android.content.ContextWrapper import android.os.Bundle import android.view.LayoutInflater import android.view.View @@ -29,10 +28,24 @@ import com.google.firebase.crashlytics.FirebaseCrashlytics import com.instructure.canvasapi2.utils.Analytics import com.instructure.interactions.FragmentInteractions import com.instructure.interactions.Navigation +import com.instructure.pandautils.utils.getFragmentActivity import com.instructure.student.BuildConfig import com.instructure.student.fragment.ParentFragment -import com.instructure.student.mobius.common.* -import com.spotify.mobius.* +import com.instructure.student.mobius.common.ConsumerQueueWrapper +import com.instructure.student.mobius.common.CoroutineConnection +import com.instructure.student.mobius.common.GlobalEventMapper +import com.instructure.student.mobius.common.GlobalEventSource +import com.instructure.student.mobius.common.LateInit +import com.instructure.student.mobius.common.MobiusExceptionLogger +import com.instructure.student.mobius.common.contraMap +import com.spotify.mobius.Connectable +import com.spotify.mobius.Connection +import com.spotify.mobius.EventSource +import com.spotify.mobius.First +import com.spotify.mobius.Init +import com.spotify.mobius.Mobius +import com.spotify.mobius.MobiusLoop +import com.spotify.mobius.Update import com.spotify.mobius.android.MobiusAndroid import com.spotify.mobius.android.runners.MainThreadWorkRunner import com.spotify.mobius.functions.Consumer @@ -176,13 +189,7 @@ abstract class MobiusView(inflater: Lay get() = parent.context protected val activity: Activity - get() = getActivity(context) - - private fun getActivity(context: Context): Activity { - if (context is Activity) return context - if (context is ContextWrapper) return getActivity(context.baseContext) - else throw IllegalStateException("Not activity context") - } + get() = context.getFragmentActivity() abstract fun onConnect(output: Consumer) diff --git a/apps/teacher/src/main/java/com/instructure/teacher/holders/SpeedGraderCommentHolder.kt b/apps/teacher/src/main/java/com/instructure/teacher/holders/SpeedGraderCommentHolder.kt index f42843bd3a..46bff66066 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/holders/SpeedGraderCommentHolder.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/holders/SpeedGraderCommentHolder.kt @@ -17,13 +17,17 @@ package com.instructure.teacher.holders import android.widget.ImageView -import androidx.fragment.app.FragmentActivity import androidx.recyclerview.widget.RecyclerView -import com.instructure.canvasapi2.models.* +import com.instructure.canvasapi2.models.Assignee +import com.instructure.canvasapi2.models.Attachment +import com.instructure.canvasapi2.models.GroupAssignee +import com.instructure.canvasapi2.models.StudentAssignee +import com.instructure.canvasapi2.models.User import com.instructure.canvasapi2.models.postmodels.CommentSendStatus import com.instructure.canvasapi2.utils.Pronouns import com.instructure.canvasapi2.utils.isValid import com.instructure.interactions.router.Route +import com.instructure.pandautils.utils.getFragmentActivity import com.instructure.pandautils.utils.onClick import com.instructure.pandautils.utils.setGone import com.instructure.pandautils.utils.setVisible @@ -41,7 +45,11 @@ import com.instructure.teacher.utils.getColorCompat import com.instructure.teacher.utils.getSubmissionFormattedDate import com.instructure.teacher.utils.iconRes import com.instructure.teacher.utils.setAnonymousAvatar -import com.instructure.teacher.view.* +import com.instructure.teacher.view.CommentAttachmentsView +import com.instructure.teacher.view.CommentDirection +import com.instructure.teacher.view.CommentMediaAttachmentView +import com.instructure.teacher.view.CommentSubmissionView +import com.instructure.teacher.view.CommentView class SpeedGraderCommentHolder(private val binding: AdapterSubmissionCommentBinding) : RecyclerView.ViewHolder(binding.root) { fun bind( @@ -83,7 +91,7 @@ class SpeedGraderCommentHolder(private val binding: AdapterSubmissionCommentBind avatarView.setupAvatarA11y(comment.authorName) avatarView.onClick { val bundle = StudentContextFragment.makeBundle(comment.authorId, courseId) - RouteMatcher.route(context as FragmentActivity, Route(StudentContextFragment::class.java, null, bundle)) + RouteMatcher.route(context.getFragmentActivity(), Route(StudentContextFragment::class.java, null, bundle)) } } Triple( @@ -143,7 +151,7 @@ class SpeedGraderCommentHolder(private val binding: AdapterSubmissionCommentBind avatarView.setupAvatarA11y(assignee.name) avatarView.onClick { val bundle = StudentContextFragment.makeBundle(assignee.id, courseId) - RouteMatcher.route(context as FragmentActivity, Route(StudentContextFragment::class.java, null, bundle)) + RouteMatcher.route(context.getFragmentActivity(), Route(StudentContextFragment::class.java, null, bundle)) } Triple(null, Pronouns.span(assignee.name, assignee.pronouns), assignee.student.avatarUrl) } diff --git a/apps/teacher/src/main/java/com/instructure/teacher/mobius/common/ui/MobiusFragment.kt b/apps/teacher/src/main/java/com/instructure/teacher/mobius/common/ui/MobiusFragment.kt index d6267030a5..6149b5b7ee 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/mobius/common/ui/MobiusFragment.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/mobius/common/ui/MobiusFragment.kt @@ -18,15 +18,27 @@ package com.instructure.teacher.mobius.common.ui import android.app.Activity import android.content.Context -import android.content.ContextWrapper import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.fragment.app.Fragment import androidx.viewbinding.ViewBinding -import com.instructure.teacher.mobius.common.* -import com.spotify.mobius.* +import com.instructure.pandautils.utils.getFragmentActivity +import com.instructure.teacher.mobius.common.ConsumerQueueWrapper +import com.instructure.teacher.mobius.common.CoroutineConnection +import com.instructure.teacher.mobius.common.GlobalEventMapper +import com.instructure.teacher.mobius.common.GlobalEventSource +import com.instructure.teacher.mobius.common.LateInit +import com.instructure.teacher.mobius.common.contraMap +import com.spotify.mobius.Connectable +import com.spotify.mobius.Connection +import com.spotify.mobius.EventSource +import com.spotify.mobius.First +import com.spotify.mobius.Init +import com.spotify.mobius.Mobius +import com.spotify.mobius.MobiusLoop +import com.spotify.mobius.Update import com.spotify.mobius.android.MobiusAndroid import com.spotify.mobius.android.runners.MainThreadWorkRunner import com.spotify.mobius.functions.Consumer @@ -154,13 +166,7 @@ abstract class MobiusView( get() = parent.context protected val activity: Activity - get() = getActivity(context) - - private fun getActivity(context: Context): Activity { - if (context is Activity) return context - if (context is ContextWrapper) return getActivity(context.baseContext) - else throw IllegalStateException("Not activity context") - } + get() = context.getFragmentActivity() abstract fun onConnect(output: Consumer) diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/utils/ActivityExtensions.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/utils/ActivityExtensions.kt index 37f7385d6f..31e9928ca0 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/utils/ActivityExtensions.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/utils/ActivityExtensions.kt @@ -16,6 +16,8 @@ */ package com.instructure.pandautils.utils +import android.content.Context +import android.content.ContextWrapper import androidx.appcompat.app.AlertDialog import androidx.fragment.app.FragmentActivity import com.instructure.canvasapi2.utils.APIHelper @@ -32,4 +34,10 @@ fun FragmentActivity.withRequireNetwork(block: () -> Unit) { .setPositiveButton(android.R.string.ok, { dialog, _ -> dialog.dismiss() }) .showThemed() } -} \ No newline at end of file +} + +fun Context.getFragmentActivity(): FragmentActivity { + if (this is FragmentActivity) return this + if (this is ContextWrapper) return this.baseContext.getFragmentActivity() + else throw IllegalStateException("Not FragmentActivity context") +} From cb4fceac92ab532db8a1ee400af2b86b8fbf7ff2 Mon Sep 17 00:00:00 2001 From: Kristof Nemere <109959688+kristofnemere@users.noreply.github.com> Date: Fri, 8 Dec 2023 12:57:34 +0100 Subject: [PATCH 035/132] [MBL-14485][Teacher] Module items have no previous or next buttons refs: MBL-14485 affects: Teacher release note: Added previous and next buttons to module items. * teacher module progression * Fixed routing * Added file details fragment * Edit file details update fix * fixed caching * Tablet toolbar changes * Chevron changes * fixed tests * fixed some comments * PR findings * Test fix * Test fix * Test fix * Binding issue fix * Test fix * Test fix * Module item remove issue fix * Test fix --- .../CourseModuleProgressionFragment.kt | 4 +- .../res/layout/course_module_progression.xml | 18 +- .../teacher/di/FileDetailsModule.kt | 44 +++ .../teacher/di/ModuleProgressionModule.kt | 34 +++ .../discussion/DiscussionsDetailsFragment.kt | 9 +- .../files/details/FileDetailsFragment.kt | 130 ++++++++ .../files/details/FileDetailsRepository.kt | 44 +++ .../files/details/FileDetailsViewData.kt | 60 ++++ .../files/details/FileDetailsViewModel.kt | 144 +++++++++ .../modules/list/ModuleListEffectHandler.kt | 46 +-- .../features/modules/list/ModuleListModels.kt | 4 - .../features/modules/list/ModuleListUpdate.kt | 9 +- .../modules/list/ui/ModuleListView.kt | 73 +---- .../modules/progression/ModuleItemAsset.kt | 37 +++ .../progression/ModuleProgressionAdapter.kt | 31 ++ .../progression/ModuleProgressionFragment.kt | 194 ++++++++++++ .../ModuleProgressionRepository.kt | 59 ++++ .../progression/ModuleProgressionViewData.kt | 40 +++ .../progression/ModuleProgressionViewModel.kt | 117 ++++++++ .../commentlibrary/CommentLibraryViewModel.kt | 3 +- .../teacher/fragments/AddMessageFragment.kt | 40 ++- .../fragments/AssignmentDetailsFragment.kt | 34 ++- .../fragments/CourseBrowserEmptyFragment.kt | 14 +- .../fragments/CourseSettingsFragment.kt | 19 +- .../fragments/CreateDiscussionFragment.kt | 50 +++- .../CreateOrEditAnnouncementFragment.kt | 37 ++- .../CreateOrEditPageDetailsFragment.kt | 34 ++- .../fragments/DiscussionsReplyFragment.kt | 20 +- .../fragments/DiscussionsUpdateFragment.kt | 23 +- .../fragments/EditFileFolderFragment.kt | 39 ++- .../fragments/EditQuizDetailsFragment.kt | 36 ++- .../fragments/InternalWebViewFragment.kt | 66 ++++- .../teacher/fragments/PageDetailsFragment.kt | 27 +- .../teacher/fragments/ProfileEditFragment.kt | 27 +- .../teacher/fragments/QuizDetailsFragment.kt | 42 ++- .../teacher/fragments/SettingsFragment.kt | 22 +- .../fragments/SpeedGraderGradeFragment.kt | 30 +- .../teacher/fragments/ViewHtmlFragment.kt | 25 +- .../teacher/fragments/ViewImageFragment.kt | 31 +- .../teacher/fragments/ViewMediaFragment.kt | 78 ++++- .../teacher/fragments/ViewPdfFragment.kt | 32 +- .../fragments/ViewUnsupportedFileFragment.kt | 36 ++- .../teacher/router/RouteMatcher.kt | 62 +++- .../teacher/router/RouteResolver.kt | 3 + .../main/res/layout/fragment_file_details.xml | 30 ++ .../res/layout/fragment_internal_webview.xml | 1 + .../layout/fragment_module_progression.xml | 89 ++++++ .../layout/fragment_speed_grader_media.xml | 16 +- .../details/FileDetailsRepositoryTest.kt | 90 ++++++ .../files/details/FileDetailsViewModelTest.kt | 277 ++++++++++++++++++ .../{ => list}/ModuleListEffectHandlerTest.kt | 62 ++-- .../{ => list}/ModuleListPresenterTest.kt | 2 +- .../{ => list}/ModuleListUpdateTest.kt | 36 +-- .../ModuleProgressionRepositoryTest.kt | 119 ++++++++ .../ModuleProgressionViewModelTest.kt | 225 ++++++++++++++ .../canvasapi2/apis/FileFolderAPI.kt | 3 + .../instructure/canvasapi2/apis/ModuleAPI.kt | 3 + .../src/main/res/drawable/ic_chevron_left.xml | 11 +- .../main/res/drawable/ic_chevron_right.xml | 11 +- .../pandautils/binding/BindingAdapters.kt | 6 + .../fragments/BasePresenterFragment.kt | 12 +- .../instructure/pandautils/mvvm/ViewState.kt | 4 + 62 files changed, 2539 insertions(+), 385 deletions(-) create mode 100644 apps/teacher/src/main/java/com/instructure/teacher/di/FileDetailsModule.kt create mode 100644 apps/teacher/src/main/java/com/instructure/teacher/di/ModuleProgressionModule.kt create mode 100644 apps/teacher/src/main/java/com/instructure/teacher/features/files/details/FileDetailsFragment.kt create mode 100644 apps/teacher/src/main/java/com/instructure/teacher/features/files/details/FileDetailsRepository.kt create mode 100644 apps/teacher/src/main/java/com/instructure/teacher/features/files/details/FileDetailsViewData.kt create mode 100644 apps/teacher/src/main/java/com/instructure/teacher/features/files/details/FileDetailsViewModel.kt create mode 100644 apps/teacher/src/main/java/com/instructure/teacher/features/modules/progression/ModuleItemAsset.kt create mode 100644 apps/teacher/src/main/java/com/instructure/teacher/features/modules/progression/ModuleProgressionAdapter.kt create mode 100644 apps/teacher/src/main/java/com/instructure/teacher/features/modules/progression/ModuleProgressionFragment.kt create mode 100644 apps/teacher/src/main/java/com/instructure/teacher/features/modules/progression/ModuleProgressionRepository.kt create mode 100644 apps/teacher/src/main/java/com/instructure/teacher/features/modules/progression/ModuleProgressionViewData.kt create mode 100644 apps/teacher/src/main/java/com/instructure/teacher/features/modules/progression/ModuleProgressionViewModel.kt create mode 100644 apps/teacher/src/main/res/layout/fragment_file_details.xml create mode 100644 apps/teacher/src/main/res/layout/fragment_module_progression.xml create mode 100644 apps/teacher/src/test/java/com/instructure/teacher/features/files/details/FileDetailsRepositoryTest.kt create mode 100644 apps/teacher/src/test/java/com/instructure/teacher/features/files/details/FileDetailsViewModelTest.kt rename apps/teacher/src/test/java/com/instructure/teacher/unit/modules/{ => list}/ModuleListEffectHandlerTest.kt (86%) rename apps/teacher/src/test/java/com/instructure/teacher/unit/modules/{ => list}/ModuleListPresenterTest.kt (99%) rename apps/teacher/src/test/java/com/instructure/teacher/unit/modules/{ => list}/ModuleListUpdateTest.kt (95%) create mode 100644 apps/teacher/src/test/java/com/instructure/teacher/unit/modules/progression/ModuleProgressionRepositoryTest.kt create mode 100644 apps/teacher/src/test/java/com/instructure/teacher/unit/modules/progression/ModuleProgressionViewModelTest.kt diff --git a/apps/student/src/main/java/com/instructure/student/features/modules/progression/CourseModuleProgressionFragment.kt b/apps/student/src/main/java/com/instructure/student/features/modules/progression/CourseModuleProgressionFragment.kt index 9bf146ebba..7338f5d9a9 100644 --- a/apps/student/src/main/java/com/instructure/student/features/modules/progression/CourseModuleProgressionFragment.kt +++ b/apps/student/src/main/java/com/instructure/student/features/modules/progression/CourseModuleProgressionFragment.kt @@ -127,8 +127,8 @@ class CourseModuleProgressionFragment : ParentFragment(), Bookmarkable { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - binding.prevItem.background = ColorKeeper.getColoredDrawable(requireActivity(), R.drawable.ic_chevron_left, canvasContext.textAndIconColor) - binding.nextItem.background = ColorKeeper.getColoredDrawable(requireActivity(), R.drawable.ic_chevron_right, canvasContext.textAndIconColor) + binding.prevItem.setImageDrawable(ColorKeeper.getColoredDrawable(requireActivity(), R.drawable.ic_chevron_left, canvasContext.textAndIconColor)) + binding.nextItem.setImageDrawable(ColorKeeper.getColoredDrawable(requireActivity(), R.drawable.ic_chevron_right, canvasContext.textAndIconColor)) } override fun onActivityCreated(savedInstanceState: Bundle?) { diff --git a/apps/student/src/main/res/layout/course_module_progression.xml b/apps/student/src/main/res/layout/course_module_progression.xml index 4918489d1c..59ac4aa00f 100644 --- a/apps/student/src/main/res/layout/course_module_progression.xml +++ b/apps/student/src/main/res/layout/course_module_progression.xml @@ -147,23 +147,25 @@ -