From 4c7cdfe0dd62368c5f1149f097f20d68d4a0a9c7 Mon Sep 17 00:00:00 2001 From: Akos Hermann <72087159+hermannakos@users.noreply.github.com> Date: Wed, 12 Apr 2023 15:00:51 +0200 Subject: [PATCH 001/150] [MBL-16554][Student] Offline poc (#1925) refs: MBL-16554 affects: Student release note: none Test plan: quick smoke test --- .../instructure/student/di/OfflineModule.kt | 37 ++++++ .../coursebrowser/CourseBrowserRepository.kt | 49 ++++++++ .../student/fragment/CourseBrowserFragment.kt | 12 +- .../res/layout/fragment_course_browser.xml | 18 +-- .../instructure/canvasapi2/apis/CourseAPI.kt | 3 + .../com/instructure/canvasapi2/apis/TabAPI.kt | 7 +- .../instructure/canvasapi2/apis/UserAPI.kt | 1 - .../instructure/canvasapi2/di/ApiModule.kt | 13 +-- .../instructure/canvasapi2/models/Course.kt | 4 +- .../com/instructure/canvasapi2/models/Term.kt | 2 +- .../com/instructure/canvasapi2/models/User.kt | 2 +- libs/pandautils/src/main/AndroidManifest.xml | 1 + .../pandautils/di/OfflineModule.kt | 102 +++++++++++++++++ .../pandautils/room/AppDatabase.kt | 4 +- .../instructure/pandautils/room/Converters.kt | 8 +- .../pandautils/room/OfflineDatabase.kt | 62 +++++++++++ .../room/OfflineDatabaseProvider.kt | 34 ++++++ .../pandautils/room/daos/CourseDao.kt | 37 ++++++ .../room/daos/CourseGradingPeriodDao.kt | 37 ++++++ .../pandautils/room/daos/EnrollmentDao.kt | 37 ++++++ .../pandautils/room/daos/GradesDao.kt | 37 ++++++ .../pandautils/room/daos/GradingPeriodDao.kt | 37 ++++++ .../pandautils/room/daos/SectionDao.kt | 37 ++++++ .../pandautils/room/daos/TabDao.kt | 37 ++++++ .../pandautils/room/daos/TermDao.kt | 34 ++++++ .../pandautils/room/daos/UserCalendarDao.kt | 37 ++++++ .../pandautils/room/daos/UserDao.kt | 34 ++++++ .../pandautils/room/entities/CourseEntity.kt | 96 ++++++++++++++++ .../entities/CourseGradingPeriodEntity.kt | 43 +++++++ .../room/entities/EnrollmentEntity.kt | 105 ++++++++++++++++++ .../pandautils/room/entities/GradesEntity.kt | 52 +++++++++ .../room/entities/GradingPeriodEntity.kt | 40 +++++++ .../pandautils/room/entities/SectionEntity.kt | 54 +++++++++ .../pandautils/room/entities/TabEntity.kt | 63 +++++++++++ .../pandautils/room/entities/TermEntity.kt | 40 +++++++ .../room/entities/UserCalendarEntity.kt | 34 ++++++ .../pandautils/room/entities/UserEntity.kt | 64 +++++++++++ .../pandautils/utils/NetworkStateProvider.kt | 30 +++++ 38 files changed, 1315 insertions(+), 29 deletions(-) create mode 100644 apps/student/src/main/java/com/instructure/student/di/OfflineModule.kt create mode 100644 apps/student/src/main/java/com/instructure/student/features/offline/repository/coursebrowser/CourseBrowserRepository.kt create mode 100644 libs/pandautils/src/main/java/com/instructure/pandautils/di/OfflineModule.kt create mode 100644 libs/pandautils/src/main/java/com/instructure/pandautils/room/OfflineDatabase.kt create mode 100644 libs/pandautils/src/main/java/com/instructure/pandautils/room/OfflineDatabaseProvider.kt create mode 100644 libs/pandautils/src/main/java/com/instructure/pandautils/room/daos/CourseDao.kt create mode 100644 libs/pandautils/src/main/java/com/instructure/pandautils/room/daos/CourseGradingPeriodDao.kt create mode 100644 libs/pandautils/src/main/java/com/instructure/pandautils/room/daos/EnrollmentDao.kt create mode 100644 libs/pandautils/src/main/java/com/instructure/pandautils/room/daos/GradesDao.kt create mode 100644 libs/pandautils/src/main/java/com/instructure/pandautils/room/daos/GradingPeriodDao.kt create mode 100644 libs/pandautils/src/main/java/com/instructure/pandautils/room/daos/SectionDao.kt create mode 100644 libs/pandautils/src/main/java/com/instructure/pandautils/room/daos/TabDao.kt create mode 100644 libs/pandautils/src/main/java/com/instructure/pandautils/room/daos/TermDao.kt create mode 100644 libs/pandautils/src/main/java/com/instructure/pandautils/room/daos/UserCalendarDao.kt create mode 100644 libs/pandautils/src/main/java/com/instructure/pandautils/room/daos/UserDao.kt create mode 100644 libs/pandautils/src/main/java/com/instructure/pandautils/room/entities/CourseEntity.kt create mode 100644 libs/pandautils/src/main/java/com/instructure/pandautils/room/entities/CourseGradingPeriodEntity.kt create mode 100644 libs/pandautils/src/main/java/com/instructure/pandautils/room/entities/EnrollmentEntity.kt create mode 100644 libs/pandautils/src/main/java/com/instructure/pandautils/room/entities/GradesEntity.kt create mode 100644 libs/pandautils/src/main/java/com/instructure/pandautils/room/entities/GradingPeriodEntity.kt create mode 100644 libs/pandautils/src/main/java/com/instructure/pandautils/room/entities/SectionEntity.kt create mode 100644 libs/pandautils/src/main/java/com/instructure/pandautils/room/entities/TabEntity.kt create mode 100644 libs/pandautils/src/main/java/com/instructure/pandautils/room/entities/TermEntity.kt create mode 100644 libs/pandautils/src/main/java/com/instructure/pandautils/room/entities/UserCalendarEntity.kt create mode 100644 libs/pandautils/src/main/java/com/instructure/pandautils/room/entities/UserEntity.kt create mode 100644 libs/pandautils/src/main/java/com/instructure/pandautils/utils/NetworkStateProvider.kt diff --git a/apps/student/src/main/java/com/instructure/student/di/OfflineModule.kt b/apps/student/src/main/java/com/instructure/student/di/OfflineModule.kt new file mode 100644 index 0000000000..407300f4b8 --- /dev/null +++ b/apps/student/src/main/java/com/instructure/student/di/OfflineModule.kt @@ -0,0 +1,37 @@ +/* + * 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.canvasapi2.apis.TabAPI +import com.instructure.pandautils.room.daos.TabDao +import com.instructure.pandautils.utils.NetworkStateProvider +import com.instructure.student.features.offline.repository.coursebrowser.CourseBrowserRepository +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.components.FragmentComponent + +@Module +@InstallIn(FragmentComponent::class) +class OfflineModule { + + @Provides + fun provideCourseBrowserRepository(tabApi: TabAPI.TabsInterface, tabDao: TabDao, networkStateProvider: NetworkStateProvider): CourseBrowserRepository { + return CourseBrowserRepository(tabApi, tabDao, networkStateProvider) + } +} \ No newline at end of file diff --git a/apps/student/src/main/java/com/instructure/student/features/offline/repository/coursebrowser/CourseBrowserRepository.kt b/apps/student/src/main/java/com/instructure/student/features/offline/repository/coursebrowser/CourseBrowserRepository.kt new file mode 100644 index 0000000000..164ac96a59 --- /dev/null +++ b/apps/student/src/main/java/com/instructure/student/features/offline/repository/coursebrowser/CourseBrowserRepository.kt @@ -0,0 +1,49 @@ +/* + * 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.features.offline.repository.coursebrowser + +import com.instructure.canvasapi2.apis.TabAPI +import com.instructure.canvasapi2.builders.RestParams +import com.instructure.canvasapi2.models.CanvasContext +import com.instructure.canvasapi2.models.Tab +import com.instructure.pandautils.room.daos.TabDao +import com.instructure.pandautils.utils.NetworkStateProvider + + +class CourseBrowserRepository( + private val tabApi: TabAPI.TabsInterface, + private val tabDao: TabDao, + private val networkStateProvider: NetworkStateProvider +) { + + suspend fun getTabs(canvasContext: CanvasContext, forceNetwork: Boolean): List { + val tabs = if (networkStateProvider.isOnline()) { + fetchTabs(canvasContext, forceNetwork) + } else { + tabDao.findByCourseId(canvasContext.id).map { + it.toApiModel() + } + } + return tabs.filter { !(it.isExternal && it.isHidden) } + } + + private suspend fun fetchTabs(canvasContext: CanvasContext, forceNetwork: Boolean): List { + val params = RestParams(isForceReadFromNetwork = forceNetwork) + return tabApi.getTabs(canvasContext.id, canvasContext.type.apiString, params).dataOrThrow + } +} \ No newline at end of file diff --git a/apps/student/src/main/java/com/instructure/student/fragment/CourseBrowserFragment.kt b/apps/student/src/main/java/com/instructure/student/fragment/CourseBrowserFragment.kt index 4bd63fec45..56feed7da0 100644 --- a/apps/student/src/main/java/com/instructure/student/fragment/CourseBrowserFragment.kt +++ b/apps/student/src/main/java/com/instructure/student/fragment/CourseBrowserFragment.kt @@ -25,9 +25,9 @@ import android.view.View import android.view.ViewGroup import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.fragment.app.Fragment +import androidx.lifecycle.lifecycleScope import com.google.android.material.appbar.AppBarLayout import com.instructure.canvasapi2.managers.PageManager -import com.instructure.canvasapi2.managers.TabManager import com.instructure.canvasapi2.models.* import com.instructure.canvasapi2.utils.isValid import com.instructure.canvasapi2.utils.pageview.PageView @@ -46,19 +46,27 @@ import com.instructure.student.R import com.instructure.student.adapter.CourseBrowserAdapter import com.instructure.student.databinding.FragmentCourseBrowserBinding import com.instructure.student.events.CourseColorOverlayToggledEvent +import com.instructure.student.features.offline.repository.coursebrowser.CourseBrowserRepository import com.instructure.student.router.RouteMatcher import com.instructure.student.util.Const import com.instructure.student.util.DisableableAppBarLayoutBehavior import com.instructure.student.util.StudentPrefs import com.instructure.student.util.TabHelper +import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.Job +import kotlinx.coroutines.launch import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.Subscribe +import javax.inject.Inject @ScreenView(SCREEN_VIEW_COURSE_BROWSER) @PageView(url = "{canvasContext}") +@AndroidEntryPoint class CourseBrowserFragment : Fragment(), FragmentInteractions, AppBarLayout.OnOffsetChangedListener { + @Inject + lateinit var repository: CourseBrowserRepository + private val binding by viewBinding(FragmentCourseBrowserBinding::bind) private var apiCalls: Job? = null @@ -183,7 +191,7 @@ class CourseBrowserFragment : Fragment(), FragmentInteractions, AppBarLayout.OnO homePageTitle = homePage.title } - val tabs = awaitApi> { TabManager.getTabs(canvasContext, it, isRefresh) }.filter { !(it.isExternal && it.isHidden) } + val tabs = repository.getTabs(canvasContext, isRefresh) // Finds the home tab so we can reorder them if necessary val sortedTabs = tabs.toMutableList() diff --git a/apps/student/src/main/res/layout/fragment_course_browser.xml b/apps/student/src/main/res/layout/fragment_course_browser.xml index fa2e5bd992..3c97f591f6 100644 --- a/apps/student/src/main/res/layout/fragment_course_browser.xml +++ b/apps/student/src/main/res/layout/fragment_course_browser.xml @@ -16,18 +16,18 @@ + android:orientation="vertical"> + app:theme="@style/ToolBarStyle" /> + android:scaleType="centerCrop" /> + tools:text="Title of Course" /> + tools:text="Subtitle" /> @@ -116,7 +116,7 @@ android:id="@+id/courseBrowserHeader" layout="@layout/view_course_browser_header" android:layout_width="match_parent" - android:layout_height="wrap_content"/> + android:layout_height="wrap_content" /> @@ -141,7 +141,7 @@ android:layout_height="match_parent" android:cacheColorHint="@android:color/transparent" app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" - app:layout_behavior="@string/appbar_scrolling_view_behavior"/> + app:layout_behavior="@string/appbar_scrolling_view_behavior" /> @@ -149,7 +149,7 @@ android:id="@+id/emptyView" android:layout_width="match_parent" android:layout_height="match_parent" - android:layout_centerInParent="true"/> + android:layout_centerInParent="true" /> diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/CourseAPI.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/CourseAPI.kt index 8e0cacc940..f54a757e0a 100644 --- a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/CourseAPI.kt +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/CourseAPI.kt @@ -75,6 +75,9 @@ object CourseAPI { @GET("courses/{courseId}?include[]=term&include[]=permissions&include[]=license&include[]=is_public&include[]=needs_grading_count&include[]=total_scores&include[]=current_grading_period_scores&include[]=course_image") fun getCourseWithGrade(@Path("courseId") courseId: Long): Call + @GET("courses/{courseId}?include[]=term&include[]=syllabus_body&include[]=total_scores&include[]=license&include[]=is_public&include[]=needs_grading_count&include[]=permissions&include[]=favorites&include[]=current_grading_period_scores&include[]=course_image&include[]=sections&include[]=public_description&include[]=grading_periods&include[]=account&include[]=course_progress&include[]=storage_quota_used_mb&include[]=total_students&include[]=passback_status&include[]=teachers&include[]=tabs&include[]=banner_image&include[]=concluded&include[]=observed_users") + suspend fun getFullCourseContent(@Path("courseId") courseId: Long, @Tag restParams: RestParams): DataResult + @GET("courses?include[]=term&include[]=total_scores&include[]=license&include[]=is_public&include[]=needs_grading_count&include[]=permissions&include[]=favorites&include[]=current_grading_period_scores&include[]=course_image&include[]=sections&state[]=current_and_concluded") fun firstPageCoursesByEnrollmentState(@Query("enrollment_state") enrollmentState: String): Call> diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/TabAPI.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/TabAPI.kt index 0260f2e6c7..d7cc7b1ed9 100644 --- a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/TabAPI.kt +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/TabAPI.kt @@ -19,17 +19,22 @@ import com.instructure.canvasapi2.StatusCallback import com.instructure.canvasapi2.builders.RestBuilder import com.instructure.canvasapi2.builders.RestParams import com.instructure.canvasapi2.models.Tab +import com.instructure.canvasapi2.utils.DataResult import retrofit2.Call import retrofit2.http.GET import retrofit2.http.Path +import retrofit2.http.Tag object TabAPI { - internal interface TabsInterface { + interface TabsInterface { @GET("{contextId}/tabs") fun getTabs(@Path("contextId") contextId: Long): Call> + @GET("{contextType}/{contextId}/tabs") + suspend fun getTabs(@Path("contextId") contextId: Long, @Path("contextType") contextType: String, @Tag params: RestParams): DataResult> + @GET("{contextId}/tabs?include[]=course_subject_tabs") fun getTabsForElementary(@Path("contextId") contextId: Long): Call> } diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/UserAPI.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/UserAPI.kt index 8261410f91..75666ce007 100644 --- a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/UserAPI.kt +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/UserAPI.kt @@ -22,7 +22,6 @@ import com.instructure.canvasapi2.builders.RestBuilder import com.instructure.canvasapi2.builders.RestParams import com.instructure.canvasapi2.models.* import com.instructure.canvasapi2.utils.APIHelper -import com.instructure.canvasapi2.utils.ApiPrefs import retrofit2.Call import retrofit2.http.* diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/di/ApiModule.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/di/ApiModule.kt index 8ac553e2fe..9c53f5c9c3 100644 --- a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/di/ApiModule.kt +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/di/ApiModule.kt @@ -1,12 +1,6 @@ package com.instructure.canvasapi2.di -import com.instructure.canvasapi2.apis.CourseAPI -import com.instructure.canvasapi2.apis.GroupAPI -import com.instructure.canvasapi2.apis.HelpLinksAPI -import com.instructure.canvasapi2.apis.InboxApi -import com.instructure.canvasapi2.apis.NotificationPreferencesAPI -import com.instructure.canvasapi2.apis.PlannerAPI -import com.instructure.canvasapi2.apis.ProgressAPI +import com.instructure.canvasapi2.apis.* import com.instructure.canvasapi2.builders.RestBuilder import com.instructure.canvasapi2.builders.RestParams import com.instructure.canvasapi2.managers.* @@ -168,4 +162,9 @@ class ApiModule { fun provideProgressApi(): ProgressAPI.ProgressInterface { return RestBuilder().build(ProgressAPI.ProgressInterface::class.java, RestParams()) } + + @Provides + fun provideTabApi(): TabAPI.TabsInterface { + return RestBuilder().build(TabAPI.TabsInterface::class.java, RestParams()) + } } \ No newline at end of file diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/models/Course.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/models/Course.kt index 6aebf29621..63c40e5bdd 100644 --- a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/models/Course.kt +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/models/Course.kt @@ -78,7 +78,9 @@ data class Course( @SerializedName("course_color") val courseColor: String? = null, @SerializedName("grading_periods") - val gradingPeriods: List? = null + val gradingPeriods: List? = null, + @SerializedName("tabs") + val tabs: List? = null ) : CanvasContext(), Comparable { override val type: Type get() = Type.COURSE diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/models/Term.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/models/Term.kt index c755d5213e..a1333f4627 100644 --- a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/models/Term.kt +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/models/Term.kt @@ -26,7 +26,7 @@ data class Term( override val id: Long = 0, val name: String? = null, @SerializedName("start_at") - private val startAt: String? = null, + val startAt: String? = null, @SerializedName("end_at") val endAt: String? = null, diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/models/User.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/models/User.kt index a46677e4ff..c70ea627ce 100644 --- a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/models/User.kt +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/models/User.kt @@ -41,7 +41,7 @@ data class User( // Helper variable for the "specified" enrollment. val enrollmentIndex: Int = 0, @SerializedName("last_login") - private val lastLogin: String? = null, + val lastLogin: String? = null, val locale: String? = null, @SerializedName("effective_locale") val effective_locale: String? = null, diff --git a/libs/pandautils/src/main/AndroidManifest.xml b/libs/pandautils/src/main/AndroidManifest.xml index d07ec32c6d..33566d832d 100644 --- a/libs/pandautils/src/main/AndroidManifest.xml +++ b/libs/pandautils/src/main/AndroidManifest.xml @@ -19,6 +19,7 @@ + () + + fun getDatabase(userId: Long?): OfflineDatabase { + if (userId == null) throw IllegalStateException("You can't access the database while logged out") + + return dbMap.getOrPut(userId) { + Room.databaseBuilder(context, OfflineDatabase::class.java, "offline-db-$userId").build() + } + } +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/daos/CourseDao.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/daos/CourseDao.kt new file mode 100644 index 0000000000..9b5bedc718 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/daos/CourseDao.kt @@ -0,0 +1,37 @@ +/* + * 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.room.daos + +import androidx.room.Dao +import androidx.room.Delete +import androidx.room.Insert +import androidx.room.Update +import com.instructure.pandautils.room.entities.CourseEntity + +@Dao +interface CourseDao { + + @Insert + suspend fun insert(entity: CourseEntity) + + @Delete + suspend fun delete(entity: CourseEntity) + + @Update + suspend fun update(entity: CourseEntity) +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/daos/CourseGradingPeriodDao.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/daos/CourseGradingPeriodDao.kt new file mode 100644 index 0000000000..c9c089126f --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/daos/CourseGradingPeriodDao.kt @@ -0,0 +1,37 @@ +/* + * 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.room.daos + +import androidx.room.Dao +import androidx.room.Delete +import androidx.room.Insert +import androidx.room.Update +import com.instructure.pandautils.room.entities.CourseGradingPeriodEntity + +@Dao +interface CourseGradingPeriodDao { + + @Insert + suspend fun insert(entity: CourseGradingPeriodEntity) + + @Delete + suspend fun delete(entity: CourseGradingPeriodEntity) + + @Update + suspend fun update(entity: CourseGradingPeriodEntity) +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/daos/EnrollmentDao.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/daos/EnrollmentDao.kt new file mode 100644 index 0000000000..d1e04e71e7 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/daos/EnrollmentDao.kt @@ -0,0 +1,37 @@ +/* + * 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.room.daos + +import androidx.room.Dao +import androidx.room.Delete +import androidx.room.Insert +import androidx.room.Update +import com.instructure.pandautils.room.entities.EnrollmentEntity + +@Dao +interface EnrollmentDao { + + @Insert + suspend fun insert(entity: EnrollmentEntity): Long + + @Delete + suspend fun delete(entity: EnrollmentEntity) + + @Update + suspend fun update(entity: EnrollmentEntity) +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/daos/GradesDao.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/daos/GradesDao.kt new file mode 100644 index 0000000000..aebd583a27 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/daos/GradesDao.kt @@ -0,0 +1,37 @@ +/* + * 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.room.daos + +import androidx.room.Dao +import androidx.room.Delete +import androidx.room.Insert +import androidx.room.Update +import com.instructure.pandautils.room.entities.GradesEntity + +@Dao +interface GradesDao { + + @Insert + suspend fun insert(entity: GradesEntity) + + @Delete + suspend fun delete(entity: GradesEntity) + + @Update + suspend fun update(entity: GradesEntity) +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/daos/GradingPeriodDao.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/daos/GradingPeriodDao.kt new file mode 100644 index 0000000000..dff8dfc3e7 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/daos/GradingPeriodDao.kt @@ -0,0 +1,37 @@ +/* + * 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.room.daos + +import androidx.room.Dao +import androidx.room.Delete +import androidx.room.Insert +import androidx.room.Update +import com.instructure.pandautils.room.entities.GradingPeriodEntity + +@Dao +interface GradingPeriodDao { + + @Insert + suspend fun insert(entity: GradingPeriodEntity) + + @Delete + suspend fun delete(entity: GradingPeriodEntity) + + @Update + suspend fun update(entity: GradingPeriodEntity) +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/daos/SectionDao.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/daos/SectionDao.kt new file mode 100644 index 0000000000..cf2599e57a --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/daos/SectionDao.kt @@ -0,0 +1,37 @@ +/* + * 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.room.daos + +import androidx.room.Dao +import androidx.room.Delete +import androidx.room.Insert +import androidx.room.Update +import com.instructure.pandautils.room.entities.SectionEntity + +@Dao +interface SectionDao { + + @Insert + suspend fun insert(entity: SectionEntity) + + @Delete + suspend fun delete(entity: SectionEntity) + + @Update + suspend fun update(entity: SectionEntity) +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/daos/TabDao.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/daos/TabDao.kt new file mode 100644 index 0000000000..cf25bbc8ef --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/daos/TabDao.kt @@ -0,0 +1,37 @@ +/* + * 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.room.daos + +import androidx.room.* +import com.instructure.pandautils.room.entities.TabEntity + +@Dao +interface TabDao { + + @Insert + suspend fun insert(entity: TabEntity) + + @Delete + suspend fun delete(entity: TabEntity) + + @Update + suspend fun update(entity: TabEntity) + + @Query("SELECT * FROM TabEntity WHERE courseId=:courseId") + suspend fun findByCourseId(courseId: Long): List +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/daos/TermDao.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/daos/TermDao.kt new file mode 100644 index 0000000000..2410313cb5 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/daos/TermDao.kt @@ -0,0 +1,34 @@ +/* + * 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.room.daos + +import androidx.room.* +import com.instructure.pandautils.room.entities.TermEntity + +@Dao +interface TermDao { + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insert(entity: TermEntity) + + @Delete + suspend fun delete(entity: TermEntity) + + @Update + suspend fun update(entity: TermEntity) +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/daos/UserCalendarDao.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/daos/UserCalendarDao.kt new file mode 100644 index 0000000000..62587dd4b6 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/daos/UserCalendarDao.kt @@ -0,0 +1,37 @@ +/* + * 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.room.daos + +import androidx.room.Dao +import androidx.room.Delete +import androidx.room.Insert +import androidx.room.Update +import com.instructure.pandautils.room.entities.UserCalendarEntity + +@Dao +interface UserCalendarDao { + + @Insert + suspend fun insert(entity: UserCalendarEntity) + + @Delete + suspend fun delete(entity: UserCalendarEntity) + + @Update + suspend fun update(entity: UserCalendarEntity) +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/daos/UserDao.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/daos/UserDao.kt new file mode 100644 index 0000000000..eacb4a06e3 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/daos/UserDao.kt @@ -0,0 +1,34 @@ +/* + * 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.room.daos + +import androidx.room.* +import com.instructure.pandautils.room.entities.UserEntity + +@Dao +interface UserDao { + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insert(entity: UserEntity) + + @Delete + suspend fun delete(entity: UserEntity) + + @Update + suspend fun update(entity: UserEntity) +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/entities/CourseEntity.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/entities/CourseEntity.kt new file mode 100644 index 0000000000..7a07989c3b --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/entities/CourseEntity.kt @@ -0,0 +1,96 @@ +/* + * 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.room.entities + +import androidx.room.Entity +import androidx.room.ForeignKey +import androidx.room.PrimaryKey +import com.instructure.canvasapi2.models.Course + +@Entity( + foreignKeys = [ + ForeignKey( + entity = TermEntity::class, + parentColumns = ["id"], + childColumns = ["termId"], + onDelete = ForeignKey.SET_NULL + ) + ] +) +data class CourseEntity( + @PrimaryKey + val id: Long, + val name: String, + val originalName: String?, + val courseCode: String?, + val startAt: String?, + val endAt: String?, + var syllabusBody: String?, + val hideFinalGrades: Boolean, + val isPublic: Boolean, + val license: String, + val termId: Long?, + val needsGradingCount: Long, + val isApplyAssignmentGroupWeights: Boolean, + val currentScore: Double?, + val finalScore: Double?, + val currentGrade: String?, + val finalGrade: String?, + val isFavorite: Boolean, + val accessRestrictedByDate: Boolean, + val imageUrl: String?, + val bannerImageUrl: String?, + val isWeightedGradingPeriods: Boolean, + val hasGradingPeriods: Boolean, + val homePage: String?, + val restrictEnrollmentsToCourseDate: Boolean, + val workflowState: String?, + val homeroomCourse: Boolean, + val courseColor: String? +) { + constructor(course: Course) : this( + course.id, + course.name, + course.originalName, + course.courseCode, + course.startAt, + course.endAt, + course.syllabusBody, + course.hideFinalGrades, + course.isPublic, + course.license?.apiString ?: Course.License.PRIVATE_COPYRIGHTED.apiString, + course.term?.id, + course.needsGradingCount, + course.isApplyAssignmentGroupWeights, + course.currentScore, + course.finalScore, + course.currentGrade, + course.finalGrade, + course.isFavorite, + course.accessRestrictedByDate, + course.imageUrl, + course.bannerImageUrl, + course.isWeightedGradingPeriods, + course.hasGradingPeriods, + course.homePage?.apiString, + course.restrictEnrollmentsToCourseDate, + course.workflowState?.apiString, + course.homeroomCourse, + course.courseColor + ) +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/entities/CourseGradingPeriodEntity.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/entities/CourseGradingPeriodEntity.kt new file mode 100644 index 0000000000..17dd0ec092 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/entities/CourseGradingPeriodEntity.kt @@ -0,0 +1,43 @@ +/* + * 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.room.entities + +import androidx.room.Entity +import androidx.room.ForeignKey + +@Entity( + primaryKeys = ["courseId", "gradingPeriodId"], + foreignKeys = [ + ForeignKey( + entity = CourseEntity::class, + parentColumns = ["id"], + childColumns = ["courseId"], + onDelete = ForeignKey.CASCADE + ), + ForeignKey( + entity = GradingPeriodEntity::class, + parentColumns = ["id"], + childColumns = ["gradingPeriodId"], + onDelete = ForeignKey.CASCADE + ) + ] +) +data class CourseGradingPeriodEntity( + val courseId: Long, + val gradingPeriodId: Long +) \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/entities/EnrollmentEntity.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/entities/EnrollmentEntity.kt new file mode 100644 index 0000000000..bff9ff9345 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/entities/EnrollmentEntity.kt @@ -0,0 +1,105 @@ +/* + * 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.room.entities + +import androidx.room.Entity +import androidx.room.ForeignKey +import androidx.room.PrimaryKey +import com.instructure.canvasapi2.models.Enrollment +import java.util.* + +@Entity( + foreignKeys = [ + ForeignKey( + entity = UserEntity::class, + parentColumns = ["id"], + childColumns = ["observedUserId"], + onDelete = ForeignKey.NO_ACTION + ), + ForeignKey( + entity = UserEntity::class, + parentColumns = ["id"], + childColumns = ["userId"], + onDelete = ForeignKey.NO_ACTION + ), + ForeignKey( + entity = SectionEntity::class, + parentColumns = ["id"], + childColumns = ["courseSectionId"], + onDelete = ForeignKey.NO_ACTION + ), + ForeignKey( + entity = CourseEntity::class, + parentColumns = ["id"], + childColumns = ["courseId"], + onDelete = ForeignKey.CASCADE + ) + ] +) +data class EnrollmentEntity( + @PrimaryKey(autoGenerate = true) + val id: Long, + val role: String, + val type: String, + val courseId: Long?, + val courseSectionId: Long?, + val enrollmentState: String?, + val userId: Long, + val computedCurrentScore: Double?, + val computedFinalScore: Double?, + val computedCurrentGrade: String?, + val computedFinalGrade: String?, + val multipleGradingPeriodsEnabled: Boolean, + val totalsForAllGradingPeriodsOption: Boolean, + val currentPeriodComputedCurrentScore: Double?, + val currentPeriodComputedFinalScore: Double?, + val currentPeriodComputedCurrentGrade: String?, + val currentPeriodComputedFinalGrade: String?, + val currentGradingPeriodId: Long, + val currentGradingPeriodTitle: String?, + val associatedUserId: Long, + val lastActivityAt: Date?, + val limitPrivilegesToCourseSection: Boolean, + val observedUserId: Long? +) { + constructor(enrollment: Enrollment, courseId: Long? = null, courseSectionId: Long? = null, observedUserId: Long?) : this( + enrollment.id, + enrollment.role?.apiRoleString ?: Enrollment.EnrollmentType.NoEnrollment.apiRoleString, + enrollment.type?.apiTypeString ?: Enrollment.EnrollmentType.NoEnrollment.apiTypeString, + if (enrollment.courseId != 0L) enrollment.courseId else courseId, + if (enrollment.courseSectionId != 0L) enrollment.courseSectionId else courseSectionId, + enrollment.enrollmentState, + enrollment.userId, + enrollment.computedCurrentScore, + enrollment.computedFinalScore, + enrollment.computedCurrentGrade, + enrollment.computedFinalGrade, + enrollment.multipleGradingPeriodsEnabled, + enrollment.totalsForAllGradingPeriodsOption, + enrollment.currentPeriodComputedCurrentScore, + enrollment.currentPeriodComputedFinalScore, + enrollment.currentPeriodComputedCurrentGrade, + enrollment.currentPeriodComputedFinalGrade, + enrollment.currentGradingPeriodId, + enrollment.currentGradingPeriodTitle, + enrollment.associatedUserId, + enrollment.lastActivityAt, + enrollment.limitPrivilegesToCourseSection, + observedUserId + ) +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/entities/GradesEntity.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/entities/GradesEntity.kt new file mode 100644 index 0000000000..a567fbfe8c --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/entities/GradesEntity.kt @@ -0,0 +1,52 @@ +/* + * 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.room.entities + +import androidx.room.Entity +import androidx.room.ForeignKey +import androidx.room.PrimaryKey +import com.instructure.canvasapi2.models.Grades + +@Entity( + foreignKeys = [ForeignKey( + entity = EnrollmentEntity::class, + parentColumns = ["id"], + childColumns = ["enrollmentId"], + onDelete = ForeignKey.CASCADE + )] +) +class GradesEntity( + @PrimaryKey + val id: Long, + val enrollmentId: Long, + val htmlUrl: String?, + val currentScore: Double?, + val finalScore: Double?, + val currentGrade: String?, + val finalGrade: String? +) { + constructor(grades: Grades, enrollmentId: Long) : this( + grades.id, + enrollmentId, + grades.htmlUrl, + grades.currentScore, + grades.finalScore, + grades.currentGrade, + grades.finalGrade + ) +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/entities/GradingPeriodEntity.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/entities/GradingPeriodEntity.kt new file mode 100644 index 0000000000..3bc99adfdc --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/entities/GradingPeriodEntity.kt @@ -0,0 +1,40 @@ +/* + * 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.room.entities + +import androidx.room.Entity +import androidx.room.PrimaryKey +import com.instructure.canvasapi2.models.GradingPeriod + +@Entity +data class GradingPeriodEntity( + @PrimaryKey + val id: Long, + val title: String?, + val startDate: String?, + val endDate: String?, + val weight: Double, +) { + constructor(gradingPeriod: GradingPeriod): this( + gradingPeriod.id, + gradingPeriod.title, + gradingPeriod.startDate, + gradingPeriod.endDate, + gradingPeriod.weight, + ) +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/entities/SectionEntity.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/entities/SectionEntity.kt new file mode 100644 index 0000000000..6313f5b458 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/entities/SectionEntity.kt @@ -0,0 +1,54 @@ +/* + * 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.room.entities + +import androidx.room.Entity +import androidx.room.ForeignKey +import androidx.room.PrimaryKey +import com.instructure.canvasapi2.models.Section + +@Entity( + foreignKeys = [ + ForeignKey( + entity = CourseEntity::class, + parentColumns = ["id"], + childColumns = ["courseId"], + onDelete = ForeignKey.CASCADE + ) + ] +) +data class SectionEntity( + @PrimaryKey + val id: Long, + var name: String, + val courseId: Long?, + val startAt: String?, + val endAt: String?, + val totalStudents: Int, + val restrictEnrollmentsToSectionDates: Boolean +) { + constructor(section: Section, courseId: Long? = null) : this( + section.id, + section.name, + if (section.courseId != 0L) section.courseId else courseId, + section.startAt, + section.endAt, + section.totalStudents, + section.restrictEnrollmentsToSectionDates + ) +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/entities/TabEntity.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/entities/TabEntity.kt new file mode 100644 index 0000000000..bf913e7627 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/entities/TabEntity.kt @@ -0,0 +1,63 @@ +/* + * 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.room.entities + +import androidx.room.Entity +import androidx.room.ForeignKey +import com.instructure.canvasapi2.models.Tab + +@Entity( + primaryKeys = ["id", "courseId"], + foreignKeys = [ForeignKey( + entity = CourseEntity::class, + parentColumns = ["id"], + childColumns = ["courseId"], + onDelete = ForeignKey.CASCADE + )] +) +data class TabEntity( + val id: String, + val label: String?, + val type: String, + val htmlUrl: String?, + val externalUrl: String?, + val visibility: String, + val isHidden: Boolean, + val position: Int, + val ltiUrl: String, + val courseId: Long +) { + constructor(tab: Tab, courseId: Long) : this( + tab.tabId, + tab.label, + tab.type, + tab.htmlUrl, + tab.externalUrl, + tab.visibility, + tab.isHidden, + tab.position, + tab.ltiUrl, + courseId + ) + + fun toApiModel(): Tab { + return Tab( + id, label, type, htmlUrl, externalUrl, visibility, isHidden, position, ltiUrl + ) + } +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/entities/TermEntity.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/entities/TermEntity.kt new file mode 100644 index 0000000000..2a3deada8e --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/entities/TermEntity.kt @@ -0,0 +1,40 @@ +/* + * 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.room.entities + +import androidx.room.Entity +import androidx.room.PrimaryKey +import com.instructure.canvasapi2.models.Term + +@Entity +data class TermEntity( + @PrimaryKey + val id: Long, + val name: String?, + val startAt: String?, + val endAt: String?, + val isGroupTerm: Boolean +) { + constructor(term: Term): this( + term.id, + term.name, + term.startAt, + term.endAt, + term.isGroupTerm + ) +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/entities/UserCalendarEntity.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/entities/UserCalendarEntity.kt new file mode 100644 index 0000000000..3f7221c860 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/entities/UserCalendarEntity.kt @@ -0,0 +1,34 @@ +/* + * 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.room.entities + +import androidx.room.Entity +import androidx.room.PrimaryKey +import com.instructure.canvasapi2.models.UserCalendar + +@Entity +data class UserCalendarEntity( + @PrimaryKey(autoGenerate = true) + val id: Long, + val ics: String, +) { + constructor(userCalendar: UserCalendar): this( + 0, + userCalendar.ics + ) +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/entities/UserEntity.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/entities/UserEntity.kt new file mode 100644 index 0000000000..6afe1ee439 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/entities/UserEntity.kt @@ -0,0 +1,64 @@ +/* + * 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.room.entities + +import androidx.room.Entity +import androidx.room.PrimaryKey +import com.instructure.canvasapi2.models.User + +@Entity +data class UserEntity( + @PrimaryKey + val id: Long, + val name: String, + val shortName: String?, + val loginId: String?, + val avatarUrl: String?, + val primaryEmail: String?, + val email: String?, + val sortableName: String?, + val bio: String?, + val enrollmentIndex: Int, + val lastLogin: String?, + val locale: String?, + val effective_locale: String?, + val pronouns: String?, + val k5User: Boolean, + val rootAccount: String?, + val isFakeStudent: Boolean +) { + constructor(user: User): this( + user.id, + user.name, + user.shortName, + user.loginId, + user.avatarUrl, + user.primaryEmail, + user.email, + user.sortableName, + user.bio, + user.enrollmentIndex, + user.lastLogin, + user.locale, + user.effective_locale, + user.pronouns, + user.k5User, + user.rootAccount, + user.isFakeStudent + ) +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/utils/NetworkStateProvider.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/utils/NetworkStateProvider.kt new file mode 100644 index 0000000000..911d0f4361 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/utils/NetworkStateProvider.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.pandautils.utils + +import android.content.Context +import android.net.ConnectivityManager + +class NetworkStateProvider(private val context: Context) { + + fun isOnline(): Boolean { + val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager + val networkInfo = connectivityManager.activeNetworkInfo + return networkInfo?.isConnected == true + } +} \ No newline at end of file From 2ef9024c9da943ea9e08fed09b007d4253427ce2 Mon Sep 17 00:00:00 2001 From: Akos Hermann <72087159+hermannakos@users.noreply.github.com> Date: Tue, 25 Apr 2023 11:11:52 +0200 Subject: [PATCH 002/150] merge master into feature/offline (#1953) --- .../assets/svg/ic_instructure_logo.svg | 34 ++ .../lib/l10n/app_localizations.dart | 18 + apps/flutter_parent/lib/l10n/res/intl_ar.arb | 32 +- apps/flutter_parent/lib/l10n/res/intl_ca.arb | 32 +- apps/flutter_parent/lib/l10n/res/intl_cy.arb | 32 +- apps/flutter_parent/lib/l10n/res/intl_da.arb | 32 +- .../lib/l10n/res/intl_da_instk12.arb | 32 +- apps/flutter_parent/lib/l10n/res/intl_de.arb | 32 +- apps/flutter_parent/lib/l10n/res/intl_en.arb | 37 +- .../lib/l10n/res/intl_en_AU.arb | 32 +- .../lib/l10n/res/intl_en_AU_unimelb.arb | 32 +- .../lib/l10n/res/intl_en_CY.arb | 32 +- .../lib/l10n/res/intl_en_GB.arb | 32 +- .../lib/l10n/res/intl_en_GB_instukhe.arb | 32 +- apps/flutter_parent/lib/l10n/res/intl_es.arb | 32 +- .../lib/l10n/res/intl_es_ES.arb | 32 +- apps/flutter_parent/lib/l10n/res/intl_fi.arb | 32 +- apps/flutter_parent/lib/l10n/res/intl_fr.arb | 32 +- .../lib/l10n/res/intl_fr_CA.arb | 32 +- apps/flutter_parent/lib/l10n/res/intl_ht.arb | 32 +- apps/flutter_parent/lib/l10n/res/intl_is.arb | 32 +- apps/flutter_parent/lib/l10n/res/intl_it.arb | 32 +- apps/flutter_parent/lib/l10n/res/intl_ja.arb | 32 +- .../lib/l10n/res/intl_messages.arb | 37 +- apps/flutter_parent/lib/l10n/res/intl_mi.arb | 32 +- apps/flutter_parent/lib/l10n/res/intl_ms.arb | 30 +- apps/flutter_parent/lib/l10n/res/intl_nb.arb | 32 +- .../lib/l10n/res/intl_nb_instk12.arb | 32 +- apps/flutter_parent/lib/l10n/res/intl_nl.arb | 32 +- apps/flutter_parent/lib/l10n/res/intl_pl.arb | 32 +- .../lib/l10n/res/intl_pt_BR.arb | 32 +- .../lib/l10n/res/intl_pt_PT.arb | 32 +- apps/flutter_parent/lib/l10n/res/intl_ru.arb | 32 +- apps/flutter_parent/lib/l10n/res/intl_sl.arb | 32 +- apps/flutter_parent/lib/l10n/res/intl_sv.arb | 32 +- .../lib/l10n/res/intl_sv_instk12.arb | 32 +- apps/flutter_parent/lib/l10n/res/intl_th.arb | 32 +- apps/flutter_parent/lib/l10n/res/intl_vi.arb | 32 +- apps/flutter_parent/lib/l10n/res/intl_zh.arb | 32 +- .../lib/l10n/res/intl_zh_HK.arb | 32 +- .../lib/l10n/res/intl_zh_Hans.arb | 32 +- .../lib/l10n/res/intl_zh_Hant.arb | 32 +- apps/flutter_parent/lib/models/user.dart | 4 + apps/flutter_parent/lib/models/user.g.dart | 250 +++++------ .../screens/settings/settings_interactor.dart | 54 ++- .../lib/screens/settings/settings_screen.dart | 30 +- apps/student/build.gradle | 4 +- .../student/ui/e2e/AnnouncementsE2ETest.kt | 4 +- .../student/ui/e2e/AssignmentsE2ETest.kt | 390 +++++++----------- .../student/ui/e2e/BookmarksE2ETest.kt | 37 +- .../student/ui/e2e/ConferencesE2ETest.kt | 8 +- .../student/ui/e2e/DiscussionsE2ETest.kt | 46 ++- .../student/ui/e2e/FilesE2ETest.kt | 82 ++-- .../student/ui/e2e/GradesE2ETest.kt | 134 +++--- .../student/ui/e2e/InboxE2ETest.kt | 42 +- .../student/ui/e2e/LoginE2ETest.kt | 36 +- .../student/ui/e2e/ModulesE2ETest.kt | 207 +++++----- .../student/ui/e2e/NotificationsE2ETest.kt | 116 ++++-- .../student/ui/e2e/PagesE2ETest.kt | 56 ++- .../student/ui/e2e/PeopleE2ETest.kt | 11 +- .../student/ui/e2e/QuizzesE2ETest.kt | 98 ++--- .../student/ui/e2e/SettingsE2ETest.kt | 6 +- .../student/ui/e2e/ShareExtensionE2ETest.kt | 44 +- .../student/ui/e2e/SyllabusE2ETest.kt | 52 ++- .../instructure/student/ui/e2e/TodoE2ETest.kt | 70 ++-- .../ui/e2e/k5/GradesElementaryE2ETest.kt | 86 ++-- .../student/ui/e2e/k5/HomeroomE2ETest.kt | 54 +-- .../ui/e2e/k5/ImportantDatesE2ETest.kt | 72 ++-- .../student/ui/e2e/k5/ScheduleE2ETest.kt | 54 ++- .../interaction/InAppUpdateInteractionTest.kt | 2 + .../ui/interaction/TodoInteractionTest.kt | 1 - .../instructure/student/ui/pages/AboutPage.kt | 10 +- .../student/ui/pages/DashboardPage.kt | 2 +- .../instructure/student/ui/pages/InboxPage.kt | 23 +- .../student/ui/pages/SettingsPage.kt | 8 +- .../instructure/student/ui/pages/TodoPage.kt | 10 +- apps/student/src/main/AndroidManifest.xml | 1 + .../student/activity/NavigationActivity.kt | 12 + .../student/activity/VideoViewActivity.kt | 2 +- .../adapter/DashboardRecyclerAdapter.kt | 6 +- .../com/instructure/student/di/AboutModule.kt | 41 ++ .../instructure/student/di/DatabaseModule.kt | 5 +- .../features/about/StudentAboutRepository.kt | 30 ++ .../AssignmentDetailsViewModel.kt | 2 +- .../course/ElementaryCoursePagerAdapter.kt | 4 +- .../fragment/ApplicationSettingsFragment.kt | 14 +- .../fragment/AssignmentListFragment.kt | 2 + .../student/fragment/BasicQuizViewFragment.kt | 20 +- .../CourseModuleProgressionFragment.kt | 142 ++----- .../fragment/InboxConversationFragment.kt | 2 +- .../fragment/InternalWebviewFragment.kt | 5 +- .../fragment/LockedModuleItemFragment.kt | 2 - .../student/fragment/PageDetailsFragment.kt | 24 +- .../student/fragment/StudioWebViewFragment.kt | 20 +- .../DiscussionSubmissionViewFragment.kt | 4 +- .../content/QuizSubmissionViewFragment.kt | 8 +- .../student/router/RouteMatcher.kt | 7 +- .../student/view/AttachmentDogEarLayout.kt | 120 ------ .../res/layout/course_module_progression.xml | 8 +- .../src/main/res/layout/dialog_about.xml | 76 ---- .../src/main/res/layout/view_attachment.xml | 4 +- .../instructure/teacher/ui/InboxPageTest.kt | 7 +- .../teacher/ui/e2e/DashboardE2ETest.kt | 2 +- .../teacher/ui/e2e/FilesE2ETest.kt | 16 +- .../teacher/ui/e2e/LoginE2ETest.kt | 25 +- .../teacher/ui/e2e/SettingsE2ETest.kt | 48 ++- .../instructure/teacher/ui/pages/AboutPage.kt | 51 +++ .../teacher/ui/pages/DashboardPage.kt | 72 +--- .../instructure/teacher/ui/pages/InboxPage.kt | 22 +- .../ui/pages/LeftSideNavigationDrawerPage.kt | 105 +++++ .../teacher/ui/pages/SettingsPage.kt | 11 +- .../teacher/ui/utils/TeacherTest.kt | 60 ++- apps/teacher/src/main/AndroidManifest.xml | 5 + .../teacher/activities/FeedbackActivity.kt | 2 - .../teacher/activities/InitActivity.kt | 13 + .../com/instructure/teacher/di/AboutModule.kt | 41 ++ .../instructure/teacher/di/DatabaseModule.kt | 5 +- .../features/about/TeacherAboutRepository.kt | 29 ++ .../TeacherShareExtensionActivity.kt | 30 ++ .../TeacherShareExtensionRouter.kt | 5 +- .../fragments/AttendanceListFragment.kt | 2 +- .../fragments/InternalWebViewFragment.kt | 1 - .../fragments/QuizPreviewWebviewFragment.kt | 4 +- .../teacher/fragments/SettingsFragment.kt | 2 + .../fragments/SimpleWebViewFragment.kt | 4 +- .../SpeedGraderQuizWebViewFragment.kt | 2 +- .../src/main/res/layout/fragment_settings.xml | 14 + .../res/values-b+da+DK+instk12/strings.xml | 2 +- .../res/values-b+nb+NO+instk12/strings.xml | 2 +- .../res/values-b+sv+SE+instk12/strings.xml | 2 +- .../dataseeding/api/SubmissionsApi.kt | 8 +- .../canvas/espresso/mockCanvas/MockCanvas.kt | 3 +- buildSrc/src/main/java/GlobalDependencies.kt | 4 +- libs/DocumentScanner/build.gradle | 6 +- .../src/main/AndroidManifest.xml | 2 +- .../ui/camerascreen/CameraScreenFragment.kt | 2 +- .../ui/imagecrop/ImageCropFragment.kt | 2 +- .../res/values-b+da+DK+instk12/strings.xml | 2 +- .../res/values-b+nb+NO+instk12/strings.xml | 2 +- .../res/values-b+sv+SE+instk12/strings.xml | 2 +- .../res/values-b+da+DK+instk12/strings.xml | 2 +- .../res/values-b+nb+NO+instk12/strings.xml | 2 +- .../res/values-b+sv+SE+instk12/strings.xml | 2 +- .../lib/l10n/res/intl_en.arb | 2 +- .../lib/l10n/res/intl_messages.arb | 2 +- .../activities/BaseLoginSignInActivity.kt | 5 - .../res/values-b+da+DK+instk12/strings.xml | 2 +- .../res/values-b+nb+NO+instk12/strings.xml | 2 +- .../res/values-b+sv+SE+instk12/strings.xml | 2 +- .../main/res/drawable/ic_instructure_logo.xml | 68 +++ .../src/main/res/values-ar/strings.xml | 4 + .../res/values-b+da+DK+instk12/strings.xml | 6 +- .../res/values-b+en+AU+unimelb/strings.xml | 4 + .../res/values-b+en+GB+instukhe/strings.xml | 4 + .../res/values-b+nb+NO+instk12/strings.xml | 6 +- .../res/values-b+sv+SE+instk12/strings.xml | 6 +- .../src/main/res/values-b+zh+HK/strings.xml | 4 + .../src/main/res/values-b+zh+Hans/strings.xml | 4 + .../src/main/res/values-b+zh+Hant/strings.xml | 4 + .../src/main/res/values-ca/strings.xml | 4 + .../src/main/res/values-cy/strings.xml | 4 + .../src/main/res/values-da/strings.xml | 4 + .../src/main/res/values-de/strings.xml | 4 + .../src/main/res/values-en-rAU/strings.xml | 4 + .../src/main/res/values-en-rCY/strings.xml | 4 + .../src/main/res/values-en-rGB/strings.xml | 4 + .../src/main/res/values-es-rES/strings.xml | 4 + .../src/main/res/values-es/strings.xml | 4 + .../src/main/res/values-fi/strings.xml | 4 + .../src/main/res/values-fr-rCA/strings.xml | 4 + .../src/main/res/values-fr/strings.xml | 4 + .../src/main/res/values-ht/strings.xml | 4 + .../src/main/res/values-is/strings.xml | 4 + .../src/main/res/values-it/strings.xml | 4 + .../src/main/res/values-ja/strings.xml | 4 + .../src/main/res/values-mi/strings.xml | 4 + .../src/main/res/values-ms/strings.xml | 4 + .../src/main/res/values-nb/strings.xml | 4 + .../src/main/res/values-nl/strings.xml | 4 + .../src/main/res/values-pl/strings.xml | 4 + .../src/main/res/values-pt-rBR/strings.xml | 4 + .../src/main/res/values-pt-rPT/strings.xml | 4 + .../src/main/res/values-ru/strings.xml | 4 + .../src/main/res/values-sl/strings.xml | 4 + .../src/main/res/values-sv/strings.xml | 4 + .../src/main/res/values-th/strings.xml | 4 + .../src/main/res/values-vi/strings.xml | 4 + .../src/main/res/values-zh/strings.xml | 4 + libs/pandares/src/main/res/values/strings.xml | 6 + .../features/about/AboutFragment.kt | 65 +++ .../features/about/AboutRepository.kt | 37 ++ .../features/about/AboutViewData.kt | 26 ++ .../features/about/AboutViewModel.kt | 45 ++ .../DashboardNotificationsViewData.kt | 2 +- .../DashboardNotificationsViewModel.kt | 23 +- .../DiscussionDetailsWebViewFragment.kt | 2 +- .../file/upload/FileUploadDialogViewData.kt | 8 +- .../file/upload/FileUploadDialogViewModel.kt | 45 +- .../file/upload/FileUploadUtilsHelper.kt | 21 +- .../file/upload/worker/FileUploadWorker.kt | 50 +-- .../ShareExtensionProgressDialogFragment.kt | 22 +- .../ShareExtensionProgressDialogViewModel.kt | 188 ++++++--- .../ShareExtensionProgressViewData.kt | 29 +- .../fragments/HtmlContentFragment.kt | 2 - .../pandautils/room/AppDatabase.kt | 2 +- ...Migrations.kt => AppDatabaseMigrations.kt} | 29 +- .../room/daos/DashboardFileUploadDao.kt | 3 + .../room/entities/FileUploadInputEntity.kt | 4 +- .../pandautils/utils/FileUploadUtils.kt | 21 + .../pandautils/utils/WebViewExtensions.kt | 24 +- .../src/main/res/layout/fragment_about.xml | 150 +++++++ ...agment_share_extension_progress_dialog.xml | 76 +++- ...fragment_share_extension_status_dialog.xml | 1 + .../main/res/layout/item_dashboard_upload.xml | 1 + .../main/res/layout/item_file_progress.xml | 13 +- .../viewholder_edit_dashboard_group.xml | 4 + .../res/values-b+da+DK+instk12/strings.xml | 2 +- .../res/values-b+nb+NO+instk12/strings.xml | 2 +- .../res/values-b+sv+SE+instk12/strings.xml | 2 +- .../DashboardNotificationsViewModelTest.kt | 8 +- .../file/upload/FileUploadViewModelTest.kt | 7 + .../ShareExtensionProgressViewModelTest.kt | 174 +++++++- .../res/values-b+da+DK+instk12/strings.xml | 2 +- .../res/values-b+nb+NO+instk12/strings.xml | 2 +- .../res/values-b+sv+SE+instk12/strings.xml | 2 +- 225 files changed, 4158 insertions(+), 1810 deletions(-) create mode 100644 apps/flutter_parent/assets/svg/ic_instructure_logo.svg create mode 100644 apps/student/src/main/java/com/instructure/student/di/AboutModule.kt create mode 100644 apps/student/src/main/java/com/instructure/student/features/about/StudentAboutRepository.kt delete mode 100644 apps/student/src/main/java/com/instructure/student/view/AttachmentDogEarLayout.kt delete mode 100644 apps/student/src/main/res/layout/dialog_about.xml create mode 100644 apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/AboutPage.kt create mode 100644 apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/LeftSideNavigationDrawerPage.kt create mode 100644 apps/teacher/src/main/java/com/instructure/teacher/di/AboutModule.kt create mode 100644 apps/teacher/src/main/java/com/instructure/teacher/features/about/TeacherAboutRepository.kt create mode 100644 apps/teacher/src/main/java/com/instructure/teacher/features/shareextension/TeacherShareExtensionActivity.kt create mode 100644 libs/pandares/src/main/res/drawable/ic_instructure_logo.xml create mode 100644 libs/pandautils/src/main/java/com/instructure/pandautils/features/about/AboutFragment.kt create mode 100644 libs/pandautils/src/main/java/com/instructure/pandautils/features/about/AboutRepository.kt create mode 100644 libs/pandautils/src/main/java/com/instructure/pandautils/features/about/AboutViewData.kt create mode 100644 libs/pandautils/src/main/java/com/instructure/pandautils/features/about/AboutViewModel.kt rename libs/pandautils/src/main/java/com/instructure/pandautils/room/{Migrations.kt => AppDatabaseMigrations.kt} (68%) create mode 100644 libs/pandautils/src/main/res/layout/fragment_about.xml diff --git a/apps/flutter_parent/assets/svg/ic_instructure_logo.svg b/apps/flutter_parent/assets/svg/ic_instructure_logo.svg new file mode 100644 index 0000000000..a1d7c3a8c1 --- /dev/null +++ b/apps/flutter_parent/assets/svg/ic_instructure_logo.svg @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + diff --git a/apps/flutter_parent/lib/l10n/app_localizations.dart b/apps/flutter_parent/lib/l10n/app_localizations.dart index e1a768471c..26fa996e0b 100644 --- a/apps/flutter_parent/lib/l10n/app_localizations.dart +++ b/apps/flutter_parent/lib/l10n/app_localizations.dart @@ -1687,4 +1687,22 @@ class AppLocalizations { String get acceptableUsePolicyAgree => Intl.message('I agree to the Acceptable Use Policy.', desc: 'acceptable use policy switch title'); + + String get about => + Intl.message('About', desc: 'Title for about menu item in settings'); + + String get aboutAppTitle => + Intl.message('App', desc: 'Title for App field on about page'); + + String get aboutDomainTitle => + Intl.message('Domain', desc: 'Title for Domain field on about page'); + + String get aboutLoginIdTitle => + Intl.message('Login ID', desc: 'Title for Login ID field on about page'); + + String get aboutEmailTitle => + Intl.message('Email', desc: 'Title for Email field on about page'); + + String get aboutVersionTitle => + Intl.message('Version', desc: 'Title for Version field on about page'); } diff --git a/apps/flutter_parent/lib/l10n/res/intl_ar.arb b/apps/flutter_parent/lib/l10n/res/intl_ar.arb index 039702f760..7dfec02c20 100644 --- a/apps/flutter_parent/lib/l10n/res/intl_ar.arb +++ b/apps/flutter_parent/lib/l10n/res/intl_ar.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2022-01-28T12:37:40.360857", + "@@last_modified": "2023-02-17T11:03:20.619429", "alertsLabel": "التنبيهات", "@alertsLabel": { "description": "The label for the Alerts tab", @@ -2666,5 +2666,33 @@ "type": "text", "placeholders_order": [], "placeholders": {} + }, + "Acceptable Use Policy": "سياسة الاستخدام المقبول", + "@Acceptable Use Policy": { + "description": "title for the acceptable use policy screen", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Submit": "إرسال", + "@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.": "إما أنك مستخدم جديد أو أن سياسة الاستخدام المقبول قد تغيرت منذ أن وافقت عليها آخر مرة. الرجاء الموافقة على سياسة الاستخدام المقبول قبل المتابعة.", + "@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.": "أوافق على سياسة الاستخدام المقبول", + "@I agree to the Acceptable Use Policy.": { + "description": "acceptable use policy switch title", + "type": "text", + "placeholders_order": [], + "placeholders": {} } -} +} \ No newline at end of file diff --git a/apps/flutter_parent/lib/l10n/res/intl_ca.arb b/apps/flutter_parent/lib/l10n/res/intl_ca.arb index 017d105f71..16aea4a2ce 100644 --- a/apps/flutter_parent/lib/l10n/res/intl_ca.arb +++ b/apps/flutter_parent/lib/l10n/res/intl_ca.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2022-01-28T12:37:40.360857", + "@@last_modified": "2023-02-17T11:03:20.619429", "alertsLabel": "Avisos", "@alertsLabel": { "description": "The label for the Alerts tab", @@ -2666,5 +2666,33 @@ "type": "text", "placeholders_order": [], "placeholders": {} + }, + "Acceptable Use Policy": "Política d’ús acceptable", + "@Acceptable Use Policy": { + "description": "title for the acceptable use policy screen", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Submit": "Entrega", + "@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.": "O bé sou un usuari nou o la política d’ús acceptable ha canviat des de la darrera vegada que la vau acceptar. Accepteu la política d’ús acceptable abans de continuar.", + "@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.": "Accepto la política d’ús acceptable.", + "@I agree to the Acceptable Use Policy.": { + "description": "acceptable use policy switch title", + "type": "text", + "placeholders_order": [], + "placeholders": {} } -} +} \ No newline at end of file diff --git a/apps/flutter_parent/lib/l10n/res/intl_cy.arb b/apps/flutter_parent/lib/l10n/res/intl_cy.arb index 17f17ec03b..4a08e4990b 100644 --- a/apps/flutter_parent/lib/l10n/res/intl_cy.arb +++ b/apps/flutter_parent/lib/l10n/res/intl_cy.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2022-01-28T12:37:40.360857", + "@@last_modified": "2023-02-17T11:03:20.619429", "alertsLabel": "Negeseuon Hysbysu", "@alertsLabel": { "description": "The label for the Alerts tab", @@ -2666,5 +2666,33 @@ "type": "text", "placeholders_order": [], "placeholders": {} + }, + "Acceptable Use Policy": "Polisi Defnydd Derbyniol", + "@Acceptable Use Policy": { + "description": "title for the acceptable use policy screen", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Submit": "Cyflwyno", + "@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.": "Naill ai rydych chi’n ddefnyddiwr newydd, neu mae’r Polisi Defnydd Derbyniol wedi newid ers i chi gytuno i’r Polisi ddiwethaf. Mae angen i chi gytuno i’r Polisi Defnydd Derbyniol cyn bwrw ymlaen.", + "@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.": "Mae’n rhaid i chi gytuno i’r Polisi Defnydd Derbyniol", + "@I agree to the Acceptable Use Policy.": { + "description": "acceptable use policy switch title", + "type": "text", + "placeholders_order": [], + "placeholders": {} } -} +} \ No newline at end of file diff --git a/apps/flutter_parent/lib/l10n/res/intl_da.arb b/apps/flutter_parent/lib/l10n/res/intl_da.arb index 09f9cc06b4..a9ce16b6d5 100644 --- a/apps/flutter_parent/lib/l10n/res/intl_da.arb +++ b/apps/flutter_parent/lib/l10n/res/intl_da.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2022-01-28T12:37:40.360857", + "@@last_modified": "2023-02-17T11:03:20.619429", "alertsLabel": "Varslinger", "@alertsLabel": { "description": "The label for the Alerts tab", @@ -2666,5 +2666,33 @@ "type": "text", "placeholders_order": [], "placeholders": {} + }, + "Acceptable Use Policy": "Betingelser for acceptabel brug", + "@Acceptable Use Policy": { + "description": "title for the acceptable use policy screen", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Submit": "Indsend", + "@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.": "Enten er du en ny bruger, eller Betingelser for acceptabel brug er blevet ændret, siden du accepterede dem. Du bedes acceptere Betingelser for acceptabel brug, før du fortsætter.", + "@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.": "Jeg accepterer betingelserne for acceptabel brug.", + "@I agree to the Acceptable Use Policy.": { + "description": "acceptable use policy switch title", + "type": "text", + "placeholders_order": [], + "placeholders": {} } -} +} \ No newline at end of file diff --git a/apps/flutter_parent/lib/l10n/res/intl_da_instk12.arb b/apps/flutter_parent/lib/l10n/res/intl_da_instk12.arb index 5da8bde216..39dcc14673 100644 --- a/apps/flutter_parent/lib/l10n/res/intl_da_instk12.arb +++ b/apps/flutter_parent/lib/l10n/res/intl_da_instk12.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2022-01-28T12:37:40.360857", + "@@last_modified": "2023-02-17T11:03:20.619429", "alertsLabel": "Varslinger", "@alertsLabel": { "description": "The label for the Alerts tab", @@ -2666,5 +2666,33 @@ "type": "text", "placeholders_order": [], "placeholders": {} + }, + "Acceptable Use Policy": "Acceptabel brugspolitik", + "@Acceptable Use Policy": { + "description": "title for the acceptable use policy screen", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Submit": "Aflever", + "@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.": "Enten er du en ny bruger, eller Betingelser for acceptabel brug er blevet ændret, siden du accepterede dem. Du bedes acceptere Betingelser for acceptabel brug, før du fortsætter.", + "@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.": "Jeg accepterer betingelserne for acceptabel brug.", + "@I agree to the Acceptable Use Policy.": { + "description": "acceptable use policy switch title", + "type": "text", + "placeholders_order": [], + "placeholders": {} } -} +} \ No newline at end of file diff --git a/apps/flutter_parent/lib/l10n/res/intl_de.arb b/apps/flutter_parent/lib/l10n/res/intl_de.arb index 2e51636988..14657d29ee 100644 --- a/apps/flutter_parent/lib/l10n/res/intl_de.arb +++ b/apps/flutter_parent/lib/l10n/res/intl_de.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2022-01-28T12:37:40.360857", + "@@last_modified": "2023-02-17T11:03:20.619429", "alertsLabel": "Benachrichtigungen", "@alertsLabel": { "description": "The label for the Alerts tab", @@ -2666,5 +2666,33 @@ "type": "text", "placeholders_order": [], "placeholders": {} + }, + "Acceptable Use Policy": "Richtlinie zur akzeptablen Nutzung", + "@Acceptable Use Policy": { + "description": "title for the acceptable use policy screen", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Submit": "Absenden", + "@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.": "Entweder sind Sie ein(e) neue(r) Benutzer*in oder die Richtlinie zur akzeptablen Nutzung hat sich geändert, seit Sie ihr zuletzt zugestimmt haben. Bitte stimmen Sie der Richtlinie zur akzeptablen Nutzung zu, bevor Sie fortfahren.", + "@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.": "Ich stimme der Richtlinie zur akzeptablen Nutzung zu", + "@I agree to the Acceptable Use Policy.": { + "description": "acceptable use policy switch title", + "type": "text", + "placeholders_order": [], + "placeholders": {} } -} +} \ No newline at end of file diff --git a/apps/flutter_parent/lib/l10n/res/intl_en.arb b/apps/flutter_parent/lib/l10n/res/intl_en.arb index 7ce29fc9e8..9d2ca6f103 100644 --- a/apps/flutter_parent/lib/l10n/res/intl_en.arb +++ b/apps/flutter_parent/lib/l10n/res/intl_en.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2023-03-31T11:03:36.613144", + "@@last_modified": "2023-04-14T11:04:46.988317", "alertsLabel": "Alerts", "@alertsLabel": { "description": "The label for the Alerts tab", @@ -2707,5 +2707,40 @@ "type": "text", "placeholders_order": [], "placeholders": {} + }, + "About": "About", + "@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": "Login ID", + "@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": "Version", + "@Version": { + "description": "Title for Version field on about page", + "type": "text", + "placeholders_order": [], + "placeholders": {} } } \ No newline at end of file diff --git a/apps/flutter_parent/lib/l10n/res/intl_en_AU.arb b/apps/flutter_parent/lib/l10n/res/intl_en_AU.arb index c2124cf1f3..38e2f80715 100644 --- a/apps/flutter_parent/lib/l10n/res/intl_en_AU.arb +++ b/apps/flutter_parent/lib/l10n/res/intl_en_AU.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2022-01-28T12:37:40.360857", + "@@last_modified": "2023-02-17T11:03:20.619429", "alertsLabel": "Alerts", "@alertsLabel": { "description": "The label for the Alerts tab", @@ -2666,5 +2666,33 @@ "type": "text", "placeholders_order": [], "placeholders": {} + }, + "Acceptable Use Policy": "Acceptable Use Policy", + "@Acceptable Use Policy": { + "description": "title for the acceptable use policy screen", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Submit": "Submit", + "@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.": "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.", + "@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.": "I agree to the Acceptable Use Policy.", + "@I agree to the Acceptable Use Policy.": { + "description": "acceptable use policy switch title", + "type": "text", + "placeholders_order": [], + "placeholders": {} } -} +} \ No newline at end of file diff --git a/apps/flutter_parent/lib/l10n/res/intl_en_AU_unimelb.arb b/apps/flutter_parent/lib/l10n/res/intl_en_AU_unimelb.arb index a3b26ba091..b657013ab7 100644 --- a/apps/flutter_parent/lib/l10n/res/intl_en_AU_unimelb.arb +++ b/apps/flutter_parent/lib/l10n/res/intl_en_AU_unimelb.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2022-01-28T12:37:40.360857", + "@@last_modified": "2023-02-17T11:03:20.619429", "alertsLabel": "Alerts", "@alertsLabel": { "description": "The label for the Alerts tab", @@ -2666,5 +2666,33 @@ "type": "text", "placeholders_order": [], "placeholders": {} + }, + "Acceptable Use Policy": "Acceptable Use Policy", + "@Acceptable Use Policy": { + "description": "title for the acceptable use policy screen", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Submit": "Submit", + "@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.": "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.", + "@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.": "I agree to the Acceptable Use Policy.", + "@I agree to the Acceptable Use Policy.": { + "description": "acceptable use policy switch title", + "type": "text", + "placeholders_order": [], + "placeholders": {} } -} +} \ No newline at end of file diff --git a/apps/flutter_parent/lib/l10n/res/intl_en_CY.arb b/apps/flutter_parent/lib/l10n/res/intl_en_CY.arb index 61b30fc682..be1386ee8a 100644 --- a/apps/flutter_parent/lib/l10n/res/intl_en_CY.arb +++ b/apps/flutter_parent/lib/l10n/res/intl_en_CY.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2022-01-28T12:37:40.360857", + "@@last_modified": "2023-02-17T11:03:20.619429", "alertsLabel": "Alerts", "@alertsLabel": { "description": "The label for the Alerts tab", @@ -2666,5 +2666,33 @@ "type": "text", "placeholders_order": [], "placeholders": {} + }, + "Acceptable Use Policy": "Acceptable Use Policy", + "@Acceptable Use Policy": { + "description": "title for the acceptable use policy screen", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Submit": "Submit", + "@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.": "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.", + "@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.": "I agree to the Acceptable Use Policy.", + "@I agree to the Acceptable Use Policy.": { + "description": "acceptable use policy switch title", + "type": "text", + "placeholders_order": [], + "placeholders": {} } -} +} \ No newline at end of file diff --git a/apps/flutter_parent/lib/l10n/res/intl_en_GB.arb b/apps/flutter_parent/lib/l10n/res/intl_en_GB.arb index c0180e8045..20c2335f72 100644 --- a/apps/flutter_parent/lib/l10n/res/intl_en_GB.arb +++ b/apps/flutter_parent/lib/l10n/res/intl_en_GB.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2022-01-28T12:37:40.360857", + "@@last_modified": "2023-02-17T11:03:20.619429", "alertsLabel": "Alerts", "@alertsLabel": { "description": "The label for the Alerts tab", @@ -2666,5 +2666,33 @@ "type": "text", "placeholders_order": [], "placeholders": {} + }, + "Acceptable Use Policy": "Acceptable Use Policy", + "@Acceptable Use Policy": { + "description": "title for the acceptable use policy screen", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Submit": "Submit", + "@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.": "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.", + "@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.": "I agree to the Acceptable Use Policy.", + "@I agree to the Acceptable Use Policy.": { + "description": "acceptable use policy switch title", + "type": "text", + "placeholders_order": [], + "placeholders": {} } -} +} \ No newline at end of file diff --git a/apps/flutter_parent/lib/l10n/res/intl_en_GB_instukhe.arb b/apps/flutter_parent/lib/l10n/res/intl_en_GB_instukhe.arb index 61b30fc682..be1386ee8a 100644 --- a/apps/flutter_parent/lib/l10n/res/intl_en_GB_instukhe.arb +++ b/apps/flutter_parent/lib/l10n/res/intl_en_GB_instukhe.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2022-01-28T12:37:40.360857", + "@@last_modified": "2023-02-17T11:03:20.619429", "alertsLabel": "Alerts", "@alertsLabel": { "description": "The label for the Alerts tab", @@ -2666,5 +2666,33 @@ "type": "text", "placeholders_order": [], "placeholders": {} + }, + "Acceptable Use Policy": "Acceptable Use Policy", + "@Acceptable Use Policy": { + "description": "title for the acceptable use policy screen", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Submit": "Submit", + "@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.": "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.", + "@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.": "I agree to the Acceptable Use Policy.", + "@I agree to the Acceptable Use Policy.": { + "description": "acceptable use policy switch title", + "type": "text", + "placeholders_order": [], + "placeholders": {} } -} +} \ No newline at end of file diff --git a/apps/flutter_parent/lib/l10n/res/intl_es.arb b/apps/flutter_parent/lib/l10n/res/intl_es.arb index 4c8cef0e26..2e5a216b07 100644 --- a/apps/flutter_parent/lib/l10n/res/intl_es.arb +++ b/apps/flutter_parent/lib/l10n/res/intl_es.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2022-01-28T12:37:40.360857", + "@@last_modified": "2023-02-17T11:03:20.619429", "alertsLabel": "Alertas", "@alertsLabel": { "description": "The label for the Alerts tab", @@ -2666,5 +2666,33 @@ "type": "text", "placeholders_order": [], "placeholders": {} + }, + "Acceptable Use Policy": "Política de uso aceptable", + "@Acceptable Use Policy": { + "description": "title for the acceptable use policy screen", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Submit": "Entregar", + "@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.": "Tanto si es un usuario nuevo como si la Política de uso aceptable cambió desde la última vez que la aceptó. Indique que está de acuerdo con la Política de uso aceptable antes de continuar.", + "@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.": "Estoy de acuerdo con la Política de uso aceptable.", + "@I agree to the Acceptable Use Policy.": { + "description": "acceptable use policy switch title", + "type": "text", + "placeholders_order": [], + "placeholders": {} } -} +} \ No newline at end of file diff --git a/apps/flutter_parent/lib/l10n/res/intl_es_ES.arb b/apps/flutter_parent/lib/l10n/res/intl_es_ES.arb index 9946c1ccf0..2a1ba17e2f 100644 --- a/apps/flutter_parent/lib/l10n/res/intl_es_ES.arb +++ b/apps/flutter_parent/lib/l10n/res/intl_es_ES.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2022-01-28T12:37:40.360857", + "@@last_modified": "2023-02-17T11:03:20.619429", "alertsLabel": "Alertas", "@alertsLabel": { "description": "The label for the Alerts tab", @@ -2666,5 +2666,33 @@ "type": "text", "placeholders_order": [], "placeholders": {} + }, + "Acceptable Use Policy": "Política de uso aceptable", + "@Acceptable Use Policy": { + "description": "title for the acceptable use policy screen", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Submit": "Entregar", + "@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.": "Es un usuario nuevo o la Política de uso aceptable ha cambiado desde la última vez que la aceptó. Aceptar la política de uso aceptable antes de continuar.", + "@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.": "Acepto la política de uso aceptable.", + "@I agree to the Acceptable Use Policy.": { + "description": "acceptable use policy switch title", + "type": "text", + "placeholders_order": [], + "placeholders": {} } -} +} \ No newline at end of file diff --git a/apps/flutter_parent/lib/l10n/res/intl_fi.arb b/apps/flutter_parent/lib/l10n/res/intl_fi.arb index 472e70b24f..b9730c06fa 100644 --- a/apps/flutter_parent/lib/l10n/res/intl_fi.arb +++ b/apps/flutter_parent/lib/l10n/res/intl_fi.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2022-01-28T12:37:40.360857", + "@@last_modified": "2023-02-17T11:03:20.619429", "alertsLabel": "Hälytykset", "@alertsLabel": { "description": "The label for the Alerts tab", @@ -2666,5 +2666,33 @@ "type": "text", "placeholders_order": [], "placeholders": {} + }, + "Acceptable Use Policy": "Hyväksyttävää käyttöä koskeva käytäntö", + "@Acceptable Use Policy": { + "description": "title for the acceptable use policy screen", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Submit": "Lähetä", + "@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.": "Olet joko uusi käyttäjä tai hyväksyttävää käyttöä koskeva käytäntö on muuttunut sen jälkeen, kun viimeksi hyväksyit sen. Hyväksy hyväksyttävää käyttöä koskeva käytäntö ennen kuin jatkat.", + "@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.": "Sinun täytyy hyväksyä hyväksyttävää käyttöä koskeva käytäntö", + "@I agree to the Acceptable Use Policy.": { + "description": "acceptable use policy switch title", + "type": "text", + "placeholders_order": [], + "placeholders": {} } -} +} \ No newline at end of file diff --git a/apps/flutter_parent/lib/l10n/res/intl_fr.arb b/apps/flutter_parent/lib/l10n/res/intl_fr.arb index d3f5ccc8b8..28463a7420 100644 --- a/apps/flutter_parent/lib/l10n/res/intl_fr.arb +++ b/apps/flutter_parent/lib/l10n/res/intl_fr.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2022-01-28T12:37:40.360857", + "@@last_modified": "2023-02-17T11:03:20.619429", "alertsLabel": "Alertes", "@alertsLabel": { "description": "The label for the Alerts tab", @@ -2666,5 +2666,33 @@ "type": "text", "placeholders_order": [], "placeholders": {} + }, + "Acceptable Use Policy": "Règlement d'utilisation acceptable", + "@Acceptable Use Policy": { + "description": "title for the acceptable use policy screen", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Submit": "Soumettre", + "@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.": "Vous êtes un nouvel utilisateur, ou bien le Règlement d'utilisation acceptable a changé depuis la dernière fois que vous l'avez accepté. Veuillez accepter le Règlement d'utilisation acceptable avant de continuer", + "@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.": "J'accepte le Règlement d'utilisation acceptable", + "@I agree to the Acceptable Use Policy.": { + "description": "acceptable use policy switch title", + "type": "text", + "placeholders_order": [], + "placeholders": {} } -} +} \ No newline at end of file diff --git a/apps/flutter_parent/lib/l10n/res/intl_fr_CA.arb b/apps/flutter_parent/lib/l10n/res/intl_fr_CA.arb index 5fe08ea3c5..b266dba62e 100644 --- a/apps/flutter_parent/lib/l10n/res/intl_fr_CA.arb +++ b/apps/flutter_parent/lib/l10n/res/intl_fr_CA.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2022-01-28T12:37:40.360857", + "@@last_modified": "2023-02-17T11:03:20.619429", "alertsLabel": "Alertes", "@alertsLabel": { "description": "The label for the Alerts tab", @@ -2666,5 +2666,33 @@ "type": "text", "placeholders_order": [], "placeholders": {} + }, + "Acceptable Use Policy": "Politique d’utilisation acceptable", + "@Acceptable Use Policy": { + "description": "title for the acceptable use policy screen", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Submit": "Envoyer", + "@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.": "Soit vous êtes un nouvel utilisateur, soit la politique d’utilisation acceptable a changé depuis que vous l’avez acceptée pour la dernière fois. Veuillez accepter la Politique d’utilisation acceptable avant de continuer.", + "@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.": "J’accepte la Politique d’utilisation acceptable.", + "@I agree to the Acceptable Use Policy.": { + "description": "acceptable use policy switch title", + "type": "text", + "placeholders_order": [], + "placeholders": {} } -} +} \ No newline at end of file diff --git a/apps/flutter_parent/lib/l10n/res/intl_ht.arb b/apps/flutter_parent/lib/l10n/res/intl_ht.arb index d49878b118..588f9e714a 100644 --- a/apps/flutter_parent/lib/l10n/res/intl_ht.arb +++ b/apps/flutter_parent/lib/l10n/res/intl_ht.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2022-01-28T12:37:40.360857", + "@@last_modified": "2023-02-17T11:03:20.619429", "alertsLabel": "Alèt", "@alertsLabel": { "description": "The label for the Alerts tab", @@ -2666,5 +2666,33 @@ "type": "text", "placeholders_order": [], "placeholders": {} + }, + "Acceptable Use Policy": "Politik Itilizasyon Akseptab", + "@Acceptable Use Policy": { + "description": "title for the acceptable use policy screen", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Submit": "Soumèt", + "@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.": "Swa ou se yon nouvo itilizatè, swa Politik Itilizasyon Akseptab la chanje depi dènye fwa ou te aksepte li a. Aksepte politik Itilizasyon Akseptab la anvan w kontinye.", + "@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.": "Mwen dakò ak Politik Itilizasyon Akseptab la.", + "@I agree to the Acceptable Use Policy.": { + "description": "acceptable use policy switch title", + "type": "text", + "placeholders_order": [], + "placeholders": {} } -} +} \ No newline at end of file diff --git a/apps/flutter_parent/lib/l10n/res/intl_is.arb b/apps/flutter_parent/lib/l10n/res/intl_is.arb index 92d22594e2..45d6d480a1 100644 --- a/apps/flutter_parent/lib/l10n/res/intl_is.arb +++ b/apps/flutter_parent/lib/l10n/res/intl_is.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2022-01-28T12:37:40.360857", + "@@last_modified": "2023-02-17T11:03:20.619429", "alertsLabel": "Viðvaranir", "@alertsLabel": { "description": "The label for the Alerts tab", @@ -2666,5 +2666,33 @@ "type": "text", "placeholders_order": [], "placeholders": {} + }, + "Acceptable Use Policy": "Stefna um ásættanlega notkun", + "@Acceptable Use Policy": { + "description": "title for the acceptable use policy screen", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Submit": "Skila", + "@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.": "Þú ert annað hvort nýr notandi eða Stefna um ásættanlega notkun hefur breyst síðan þú samþykktir hana síðast. Vinsamlegast samþykktu Stefnu um ásættanlega notkun áður en þú heldur áfram.", + "@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.": "Ég samþykki Stefnu um ásættanlega notkun.", + "@I agree to the Acceptable Use Policy.": { + "description": "acceptable use policy switch title", + "type": "text", + "placeholders_order": [], + "placeholders": {} } -} +} \ No newline at end of file diff --git a/apps/flutter_parent/lib/l10n/res/intl_it.arb b/apps/flutter_parent/lib/l10n/res/intl_it.arb index 9b1bc810bd..d099672b31 100644 --- a/apps/flutter_parent/lib/l10n/res/intl_it.arb +++ b/apps/flutter_parent/lib/l10n/res/intl_it.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2022-01-28T12:37:40.360857", + "@@last_modified": "2023-02-17T11:03:20.619429", "alertsLabel": "Avvisi", "@alertsLabel": { "description": "The label for the Alerts tab", @@ -2666,5 +2666,33 @@ "type": "text", "placeholders_order": [], "placeholders": {} + }, + "Acceptable Use Policy": "Norme di utilizzo accettabile", + "@Acceptable Use Policy": { + "description": "title for the acceptable use policy screen", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Submit": "Invia", + "@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.": "Sei un nuovo utente oppure le Norme di utilizzo accettabile sono cambiate dall'ultima volta in cui le hai accettate. Accetta le Norme di utilizzo accettabile prima di continuare.", + "@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.": "Accetto le Norme di utilizzo accettabile.", + "@I agree to the Acceptable Use Policy.": { + "description": "acceptable use policy switch title", + "type": "text", + "placeholders_order": [], + "placeholders": {} } -} +} \ No newline at end of file diff --git a/apps/flutter_parent/lib/l10n/res/intl_ja.arb b/apps/flutter_parent/lib/l10n/res/intl_ja.arb index 36bde9ca05..a32d862c4c 100644 --- a/apps/flutter_parent/lib/l10n/res/intl_ja.arb +++ b/apps/flutter_parent/lib/l10n/res/intl_ja.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2022-01-28T12:37:40.360857", + "@@last_modified": "2023-02-17T11:03:20.619429", "alertsLabel": "アラート", "@alertsLabel": { "description": "The label for the Alerts tab", @@ -2666,5 +2666,33 @@ "type": "text", "placeholders_order": [], "placeholders": {} + }, + "Acceptable Use Policy": "AUP (Acceptable Use Policy)", + "@Acceptable Use Policy": { + "description": "title for the acceptable use policy screen", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Submit": "提出", + "@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.": "あなたが新しいユーザーであるか、最後に同意して以降、許容される使用ポリシーが変更されています。続行する前に許可される使用ポリシーに同意してください。", + "@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.": "許可される使用ポリシーに合意します", + "@I agree to the Acceptable Use Policy.": { + "description": "acceptable use policy switch title", + "type": "text", + "placeholders_order": [], + "placeholders": {} } -} +} \ No newline at end of file diff --git a/apps/flutter_parent/lib/l10n/res/intl_messages.arb b/apps/flutter_parent/lib/l10n/res/intl_messages.arb index 7ce29fc9e8..9d2ca6f103 100644 --- a/apps/flutter_parent/lib/l10n/res/intl_messages.arb +++ b/apps/flutter_parent/lib/l10n/res/intl_messages.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2023-03-31T11:03:36.613144", + "@@last_modified": "2023-04-14T11:04:46.988317", "alertsLabel": "Alerts", "@alertsLabel": { "description": "The label for the Alerts tab", @@ -2707,5 +2707,40 @@ "type": "text", "placeholders_order": [], "placeholders": {} + }, + "About": "About", + "@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": "Login ID", + "@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": "Version", + "@Version": { + "description": "Title for Version field on about page", + "type": "text", + "placeholders_order": [], + "placeholders": {} } } \ No newline at end of file diff --git a/apps/flutter_parent/lib/l10n/res/intl_mi.arb b/apps/flutter_parent/lib/l10n/res/intl_mi.arb index 6a755a8785..ffddad5267 100644 --- a/apps/flutter_parent/lib/l10n/res/intl_mi.arb +++ b/apps/flutter_parent/lib/l10n/res/intl_mi.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2022-01-28T12:37:40.360857", + "@@last_modified": "2023-02-17T11:03:20.619429", "alertsLabel": "He whakamataara", "@alertsLabel": { "description": "The label for the Alerts tab", @@ -2666,5 +2666,33 @@ "type": "text", "placeholders_order": [], "placeholders": {} + }, + "Acceptable Use Policy": "Kaupapa Whakamahi e Whakāetia ana", + "@Acceptable Use Policy": { + "description": "title for the acceptable use policy screen", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Submit": "Tuku", + "@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.": "Ahakoa he kaiwhakamahi hou koe te Kaupapa Whakamahi Whakaae ranei kua rereke mai i to whakaaetanga whakamutunga. I mua i te haere whakaae mai ki te Kaupapa Whakamahi Whakaae.", + "@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.": "Ko te Kaupapa Whakamahi Whakaae he pai ki ahau.", + "@I agree to the Acceptable Use Policy.": { + "description": "acceptable use policy switch title", + "type": "text", + "placeholders_order": [], + "placeholders": {} } -} +} \ No newline at end of file diff --git a/apps/flutter_parent/lib/l10n/res/intl_ms.arb b/apps/flutter_parent/lib/l10n/res/intl_ms.arb index a5ffc75094..06d80ecc18 100644 --- a/apps/flutter_parent/lib/l10n/res/intl_ms.arb +++ b/apps/flutter_parent/lib/l10n/res/intl_ms.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2022-10-28T11:03:07.232972", + "@@last_modified": "2023-02-17T11:03:20.619429", "alertsLabel": "Isyarat", "@alertsLabel": { "description": "The label for the Alerts tab", @@ -2666,5 +2666,33 @@ "type": "text", "placeholders_order": [], "placeholders": {} + }, + "Acceptable Use Policy": "Dasar Penggunaan Boleh 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.": "Sama ada anda pengguna baharu atau Dasar Penggunaan Boleh Diterima telah berubah sejak anda kali terakhir menyatakan persetujuan berkenaannya. Sila nyatakan persetujuan anda dengan Dasar Penggunaan Boleh Diterima sebelum anda meneruskan.", + "@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.": "Anda mesti memberikan persetujuan untuk Dasar Penggunaan Boleh Diterima.", + "@I agree to the Acceptable Use Policy.": { + "description": "acceptable use policy switch title", + "type": "text", + "placeholders_order": [], + "placeholders": {} } } \ No newline at end of file diff --git a/apps/flutter_parent/lib/l10n/res/intl_nb.arb b/apps/flutter_parent/lib/l10n/res/intl_nb.arb index 097ccf0e02..50d81d9447 100644 --- a/apps/flutter_parent/lib/l10n/res/intl_nb.arb +++ b/apps/flutter_parent/lib/l10n/res/intl_nb.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2022-01-28T12:37:40.360857", + "@@last_modified": "2023-02-17T11:03:20.619429", "alertsLabel": "Varsler", "@alertsLabel": { "description": "The label for the Alerts tab", @@ -2666,5 +2666,33 @@ "type": "text", "placeholders_order": [], "placeholders": {} + }, + "Acceptable Use Policy": "Retningslinjer for retningslinjer for akseptabel bruk bruk", + "@Acceptable Use Policy": { + "description": "title for the acceptable use policy screen", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Submit": "Send inn", + "@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.": "Enten du er ny bruker eller retningslinjer for akseptabel bruk er endret siden sist du godkjente dem. Du må godta retningslinjer for akseptabel bruk før du fortsetter.", + "@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.": "Jeg godtar retningslinjer for akseptabel bruk", + "@I agree to the Acceptable Use Policy.": { + "description": "acceptable use policy switch title", + "type": "text", + "placeholders_order": [], + "placeholders": {} } -} +} \ No newline at end of file diff --git a/apps/flutter_parent/lib/l10n/res/intl_nb_instk12.arb b/apps/flutter_parent/lib/l10n/res/intl_nb_instk12.arb index cce7ac47b4..af302f925c 100644 --- a/apps/flutter_parent/lib/l10n/res/intl_nb_instk12.arb +++ b/apps/flutter_parent/lib/l10n/res/intl_nb_instk12.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2022-01-28T12:37:40.360857", + "@@last_modified": "2023-02-17T11:03:20.619429", "alertsLabel": "Varsler", "@alertsLabel": { "description": "The label for the Alerts tab", @@ -2666,5 +2666,33 @@ "type": "text", "placeholders_order": [], "placeholders": {} + }, + "Acceptable Use Policy": "Retningslinjer for akseptabel bruk", + "@Acceptable Use Policy": { + "description": "title for the acceptable use policy screen", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Submit": "Send inn", + "@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.": "Enten du er ny bruker eller retningslinjer for akseptabel bruk er endret siden sist du godkjente dem. Du må godta retningslinjer for akseptabel bruk før du fortsetter.", + "@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.": "Jeg godtar retningslinjer for akseptabel bruk", + "@I agree to the Acceptable Use Policy.": { + "description": "acceptable use policy switch title", + "type": "text", + "placeholders_order": [], + "placeholders": {} } -} +} \ No newline at end of file diff --git a/apps/flutter_parent/lib/l10n/res/intl_nl.arb b/apps/flutter_parent/lib/l10n/res/intl_nl.arb index b16713534d..22ea3b08b4 100644 --- a/apps/flutter_parent/lib/l10n/res/intl_nl.arb +++ b/apps/flutter_parent/lib/l10n/res/intl_nl.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2022-01-28T12:37:40.360857", + "@@last_modified": "2023-02-17T11:03:20.619429", "alertsLabel": "Waarschuwingen", "@alertsLabel": { "description": "The label for the Alerts tab", @@ -2666,5 +2666,33 @@ "type": "text", "placeholders_order": [], "placeholders": {} + }, + "Acceptable Use Policy": "Beleid voor acceptabel gebruik", + "@Acceptable Use Policy": { + "description": "title for the acceptable use policy screen", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Submit": "Indienen", + "@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.": "Je bent een nieuwe gebruiker of het Beleid voor acceptabel gebruik is gewijzigd sinds je er de laatste keer mee akkoord bent gegaan. Ga akkoord met het Beleid voor acceptabel gebruik voordat je verdergaat.", + "@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.": "Ik ga akkoord met het Beleid voor acceptabel gebruik.", + "@I agree to the Acceptable Use Policy.": { + "description": "acceptable use policy switch title", + "type": "text", + "placeholders_order": [], + "placeholders": {} } -} +} \ No newline at end of file diff --git a/apps/flutter_parent/lib/l10n/res/intl_pl.arb b/apps/flutter_parent/lib/l10n/res/intl_pl.arb index 6f509db156..98a06c5d5d 100644 --- a/apps/flutter_parent/lib/l10n/res/intl_pl.arb +++ b/apps/flutter_parent/lib/l10n/res/intl_pl.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2022-01-28T12:37:40.360857", + "@@last_modified": "2023-02-17T11:03:20.619429", "alertsLabel": "Alerty", "@alertsLabel": { "description": "The label for the Alerts tab", @@ -2666,5 +2666,33 @@ "type": "text", "placeholders_order": [], "placeholders": {} + }, + "Acceptable Use Policy": "Zasady dozwolonego użytku", + "@Acceptable Use Policy": { + "description": "title for the acceptable use policy screen", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Submit": "Prześlij", + "@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.": "Albo jesteś nowym użytkownikiem, albo Zasady dozwolonego użytku uległy zmianie od momentu, gdy wyraziłeś na nie zgodę. Zaakceptuj Zasady dozwolonego użytku, aby kontynuować.", + "@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.": "Wyrażam zgodę na Zasady dozwolonego użytku.", + "@I agree to the Acceptable Use Policy.": { + "description": "acceptable use policy switch title", + "type": "text", + "placeholders_order": [], + "placeholders": {} } -} +} \ No newline at end of file diff --git a/apps/flutter_parent/lib/l10n/res/intl_pt_BR.arb b/apps/flutter_parent/lib/l10n/res/intl_pt_BR.arb index fa598a44d4..c557a68c79 100644 --- a/apps/flutter_parent/lib/l10n/res/intl_pt_BR.arb +++ b/apps/flutter_parent/lib/l10n/res/intl_pt_BR.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2022-01-28T12:37:40.360857", + "@@last_modified": "2023-02-17T11:03:20.619429", "alertsLabel": "Alertas", "@alertsLabel": { "description": "The label for the Alerts tab", @@ -2666,5 +2666,33 @@ "type": "text", "placeholders_order": [], "placeholders": {} + }, + "Acceptable Use Policy": "Política de uso aceitável", + "@Acceptable Use Policy": { + "description": "title for the acceptable use policy screen", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Submit": "Enviar", + "@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.": "Ou você é um novo usuário ou a Política de Uso Aceitável mudou desde a última vez que você concordou com ela. Por favor, concorde com a Política de Uso Aceitável antes de continuar.", + "@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.": "Eu concordo com a Política de Uso Aceitável.", + "@I agree to the Acceptable Use Policy.": { + "description": "acceptable use policy switch title", + "type": "text", + "placeholders_order": [], + "placeholders": {} } -} +} \ No newline at end of file diff --git a/apps/flutter_parent/lib/l10n/res/intl_pt_PT.arb b/apps/flutter_parent/lib/l10n/res/intl_pt_PT.arb index 80097ce0d2..d33eb2b717 100644 --- a/apps/flutter_parent/lib/l10n/res/intl_pt_PT.arb +++ b/apps/flutter_parent/lib/l10n/res/intl_pt_PT.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2022-01-28T12:37:40.360857", + "@@last_modified": "2023-02-17T11:03:20.619429", "alertsLabel": "Alertas", "@alertsLabel": { "description": "The label for the Alerts tab", @@ -2666,5 +2666,33 @@ "type": "text", "placeholders_order": [], "placeholders": {} + }, + "Acceptable Use Policy": "Política de Utilização Aceitável", + "@Acceptable Use Policy": { + "description": "title for the acceptable use policy screen", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Submit": "Submeter", + "@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.": "Ou é um novo utilizador ou a Política de Utilização Aceitável mudou desde a última vez que concordou com ela. Por favor, aceite a Política de Utilização Aceitável antes de continuar.", + "@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.": "Concordo com a Política de Utilização Aceitável.", + "@I agree to the Acceptable Use Policy.": { + "description": "acceptable use policy switch title", + "type": "text", + "placeholders_order": [], + "placeholders": {} } -} +} \ No newline at end of file diff --git a/apps/flutter_parent/lib/l10n/res/intl_ru.arb b/apps/flutter_parent/lib/l10n/res/intl_ru.arb index 4c89c1973e..63e0bb16e1 100644 --- a/apps/flutter_parent/lib/l10n/res/intl_ru.arb +++ b/apps/flutter_parent/lib/l10n/res/intl_ru.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2022-01-28T12:37:40.360857", + "@@last_modified": "2023-02-17T11:03:20.619429", "alertsLabel": "Предупреждения", "@alertsLabel": { "description": "The label for the Alerts tab", @@ -2666,5 +2666,33 @@ "type": "text", "placeholders_order": [], "placeholders": {} + }, + "Acceptable Use Policy": "Приемлемая политика использования", + "@Acceptable Use Policy": { + "description": "title for the acceptable use policy screen", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Submit": "Отправить", + "@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.": "Вы либо новый пользователь, либо Политика допустимого использования изменились после того, как вы в последний раз приняли их. Согласитесь с Политикой допустимого использования, прежде чем продолжить.", + "@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.": "Я соглашаюсь с Политикой допустимого использования.", + "@I agree to the Acceptable Use Policy.": { + "description": "acceptable use policy switch title", + "type": "text", + "placeholders_order": [], + "placeholders": {} } -} +} \ No newline at end of file diff --git a/apps/flutter_parent/lib/l10n/res/intl_sl.arb b/apps/flutter_parent/lib/l10n/res/intl_sl.arb index 97b30b1fcb..8b1e905614 100644 --- a/apps/flutter_parent/lib/l10n/res/intl_sl.arb +++ b/apps/flutter_parent/lib/l10n/res/intl_sl.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2022-01-28T12:37:40.360857", + "@@last_modified": "2023-02-17T11:03:20.619429", "alertsLabel": "Opozorila", "@alertsLabel": { "description": "The label for the Alerts tab", @@ -2666,5 +2666,33 @@ "type": "text", "placeholders_order": [], "placeholders": {} + }, + "Acceptable Use Policy": "Pravilnik o sprejemljivi rabi", + "@Acceptable Use Policy": { + "description": "title for the acceptable use policy screen", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Submit": "Oddaj", + "@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.": "Ste nov uporabnik ali pa so Pravilnik o sprejemljivi rabi spremenili, odkar ste se nazadnje strinjali z njim. Pred nadaljevanjem potrdite, da se strinjate s Pravilnikom o sprejemljivi rabi.", + "@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.": "Strinjam se s Pravilnikom o sprejemljivi rabi.", + "@I agree to the Acceptable Use Policy.": { + "description": "acceptable use policy switch title", + "type": "text", + "placeholders_order": [], + "placeholders": {} } -} +} \ No newline at end of file diff --git a/apps/flutter_parent/lib/l10n/res/intl_sv.arb b/apps/flutter_parent/lib/l10n/res/intl_sv.arb index dfcff9cfbf..56928c1f25 100644 --- a/apps/flutter_parent/lib/l10n/res/intl_sv.arb +++ b/apps/flutter_parent/lib/l10n/res/intl_sv.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2022-01-28T12:37:40.360857", + "@@last_modified": "2023-02-17T11:03:20.619429", "alertsLabel": "Notiser", "@alertsLabel": { "description": "The label for the Alerts tab", @@ -2666,5 +2666,33 @@ "type": "text", "placeholders_order": [], "placeholders": {} + }, + "Acceptable Use Policy": "Policy för godkänd användning", + "@Acceptable Use Policy": { + "description": "title for the acceptable use policy screen", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Submit": "Lämna in", + "@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.": "Antingen är du en ny användare eller så har policyn för godkänd användning ändrats sedan du senast godkände den. Godkänn policyn för godkänd användning innan du fortsätter.", + "@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.": "Jag godkänner policyn för godkänd användning.", + "@I agree to the Acceptable Use Policy.": { + "description": "acceptable use policy switch title", + "type": "text", + "placeholders_order": [], + "placeholders": {} } -} +} \ No newline at end of file diff --git a/apps/flutter_parent/lib/l10n/res/intl_sv_instk12.arb b/apps/flutter_parent/lib/l10n/res/intl_sv_instk12.arb index ce86d6fde6..39df926726 100644 --- a/apps/flutter_parent/lib/l10n/res/intl_sv_instk12.arb +++ b/apps/flutter_parent/lib/l10n/res/intl_sv_instk12.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2022-01-28T12:37:40.360857", + "@@last_modified": "2023-02-17T11:03:20.619429", "alertsLabel": "Notiser", "@alertsLabel": { "description": "The label for the Alerts tab", @@ -2666,5 +2666,33 @@ "type": "text", "placeholders_order": [], "placeholders": {} + }, + "Acceptable Use Policy": "Acceptable Use Policy (Policy för godkänd användning)", + "@Acceptable Use Policy": { + "description": "title for the acceptable use policy screen", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Submit": "Spara", + "@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.": "Antingen är du en ny användare eller så har policyn för godkänd användning ändrats sedan du senast godkände den. Godkänn policyn för godkänd användning innan du fortsätter.", + "@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.": "Jag godkänner policyn för godkänd användning.", + "@I agree to the Acceptable Use Policy.": { + "description": "acceptable use policy switch title", + "type": "text", + "placeholders_order": [], + "placeholders": {} } -} +} \ No newline at end of file diff --git a/apps/flutter_parent/lib/l10n/res/intl_th.arb b/apps/flutter_parent/lib/l10n/res/intl_th.arb index 6970aec00b..7200a746b4 100644 --- a/apps/flutter_parent/lib/l10n/res/intl_th.arb +++ b/apps/flutter_parent/lib/l10n/res/intl_th.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2022-01-28T12:37:40.360857", + "@@last_modified": "2023-02-17T11:03:20.619429", "alertsLabel": "แจ้งเตือน", "@alertsLabel": { "description": "The label for the Alerts tab", @@ -2666,5 +2666,33 @@ "type": "text", "placeholders_order": [], "placeholders": {} + }, + "Acceptable Use Policy": "นโยบายการใช้งานที่ยอมรับได้", + "@Acceptable Use Policy": { + "description": "title for the acceptable use policy screen", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Submit": "ส่ง", + "@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.": "คุณเป็นผู้ใช้ใหม่หรือนโยบายการใช้งานที่ยอมรับได้ที่คุณตอบรับไว้ล่าสุดมีการเปลี่ยนแปลง กรุณาตอบรับนโยบายการใช้งานที่ยอมรับได้ก่อนดำเนินการต่อ", + "@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.": "ฉันยอมรับนโยบายการใช้งานที่ยอมรับได้", + "@I agree to the Acceptable Use Policy.": { + "description": "acceptable use policy switch title", + "type": "text", + "placeholders_order": [], + "placeholders": {} } -} +} \ No newline at end of file diff --git a/apps/flutter_parent/lib/l10n/res/intl_vi.arb b/apps/flutter_parent/lib/l10n/res/intl_vi.arb index a7dca35625..b1178043b0 100644 --- a/apps/flutter_parent/lib/l10n/res/intl_vi.arb +++ b/apps/flutter_parent/lib/l10n/res/intl_vi.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2022-01-28T12:37:40.360857", + "@@last_modified": "2023-02-17T11:03:20.619429", "alertsLabel": "Cảnh Báo", "@alertsLabel": { "description": "The label for the Alerts tab", @@ -2666,5 +2666,33 @@ "type": "text", "placeholders_order": [], "placeholders": {} + }, + "Acceptable Use Policy": "Chính Sách Sử Dụng Có Thể Chấp Nhận Được", + "@Acceptable Use Policy": { + "description": "title for the acceptable use policy screen", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Submit": "Nộp", + "@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.": "Bạn là người dùng mới hoặc Chính Sách Sử Dụng Có Thể Chấp Nhận Được đã thay đổi kể từ lần gần nhất bạn đồng ý. Vui lòng đồng ý với Chính Sách Sử Dụng Có Thể Chấp Nhận Được trước khi bạn tiếp tục.", + "@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.": "Tôi đồng ý với Chính Sách Sử Dụng Có Thể Chấp Nhận Được.", + "@I agree to the Acceptable Use Policy.": { + "description": "acceptable use policy switch title", + "type": "text", + "placeholders_order": [], + "placeholders": {} } -} +} \ No newline at end of file diff --git a/apps/flutter_parent/lib/l10n/res/intl_zh.arb b/apps/flutter_parent/lib/l10n/res/intl_zh.arb index f1fea8d23d..e2c3d89c68 100644 --- a/apps/flutter_parent/lib/l10n/res/intl_zh.arb +++ b/apps/flutter_parent/lib/l10n/res/intl_zh.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2022-01-28T12:37:40.360857", + "@@last_modified": "2023-02-17T11:03:20.619429", "alertsLabel": "警告", "@alertsLabel": { "description": "The label for the Alerts tab", @@ -2666,5 +2666,33 @@ "type": "text", "placeholders_order": [], "placeholders": {} + }, + "Acceptable Use Policy": "可接受的使用政策", + "@Acceptable Use Policy": { + "description": "title for the acceptable use policy screen", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Submit": "提交", + "@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.": "您是新用户,或者自您上次同意可接受的使用政策后,该政策已更改。请同意“可接受的使用政策”后再继续。", + "@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.": "我同意“可接受的使用政策”。", + "@I agree to the Acceptable Use Policy.": { + "description": "acceptable use policy switch title", + "type": "text", + "placeholders_order": [], + "placeholders": {} } -} +} \ No newline at end of file diff --git a/apps/flutter_parent/lib/l10n/res/intl_zh_HK.arb b/apps/flutter_parent/lib/l10n/res/intl_zh_HK.arb index 1b777ea114..b2d07344b8 100644 --- a/apps/flutter_parent/lib/l10n/res/intl_zh_HK.arb +++ b/apps/flutter_parent/lib/l10n/res/intl_zh_HK.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2022-01-28T12:37:40.360857", + "@@last_modified": "2023-02-17T11:03:20.619429", "alertsLabel": "提醒", "@alertsLabel": { "description": "The label for the Alerts tab", @@ -2666,5 +2666,33 @@ "type": "text", "placeholders_order": [], "placeholders": {} + }, + "Acceptable Use Policy": "可接受使用政策", + "@Acceptable Use Policy": { + "description": "title for the acceptable use policy screen", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Submit": "提交", + "@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.": "無論您是新的使用者,或是自上次同意以來,「可接受使用政策」都已經變更。請在您繼續使用前同意「可接受使用政策」。", + "@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.": "您必須同意「可接受使用政策」。", + "@I agree to the Acceptable Use Policy.": { + "description": "acceptable use policy switch title", + "type": "text", + "placeholders_order": [], + "placeholders": {} } -} +} \ No newline at end of file diff --git a/apps/flutter_parent/lib/l10n/res/intl_zh_Hans.arb b/apps/flutter_parent/lib/l10n/res/intl_zh_Hans.arb index f1fea8d23d..e2c3d89c68 100644 --- a/apps/flutter_parent/lib/l10n/res/intl_zh_Hans.arb +++ b/apps/flutter_parent/lib/l10n/res/intl_zh_Hans.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2022-01-28T12:37:40.360857", + "@@last_modified": "2023-02-17T11:03:20.619429", "alertsLabel": "警告", "@alertsLabel": { "description": "The label for the Alerts tab", @@ -2666,5 +2666,33 @@ "type": "text", "placeholders_order": [], "placeholders": {} + }, + "Acceptable Use Policy": "可接受的使用政策", + "@Acceptable Use Policy": { + "description": "title for the acceptable use policy screen", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Submit": "提交", + "@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.": "您是新用户,或者自您上次同意可接受的使用政策后,该政策已更改。请同意“可接受的使用政策”后再继续。", + "@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.": "我同意“可接受的使用政策”。", + "@I agree to the Acceptable Use Policy.": { + "description": "acceptable use policy switch title", + "type": "text", + "placeholders_order": [], + "placeholders": {} } -} +} \ No newline at end of file diff --git a/apps/flutter_parent/lib/l10n/res/intl_zh_Hant.arb b/apps/flutter_parent/lib/l10n/res/intl_zh_Hant.arb index 1b777ea114..b2d07344b8 100644 --- a/apps/flutter_parent/lib/l10n/res/intl_zh_Hant.arb +++ b/apps/flutter_parent/lib/l10n/res/intl_zh_Hant.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2022-01-28T12:37:40.360857", + "@@last_modified": "2023-02-17T11:03:20.619429", "alertsLabel": "提醒", "@alertsLabel": { "description": "The label for the Alerts tab", @@ -2666,5 +2666,33 @@ "type": "text", "placeholders_order": [], "placeholders": {} + }, + "Acceptable Use Policy": "可接受使用政策", + "@Acceptable Use Policy": { + "description": "title for the acceptable use policy screen", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Submit": "提交", + "@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.": "無論您是新的使用者,或是自上次同意以來,「可接受使用政策」都已經變更。請在您繼續使用前同意「可接受使用政策」。", + "@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.": "您必須同意「可接受使用政策」。", + "@I agree to the Acceptable Use Policy.": { + "description": "acceptable use policy switch title", + "type": "text", + "placeholders_order": [], + "placeholders": {} } -} +} \ No newline at end of file diff --git a/apps/flutter_parent/lib/models/user.dart b/apps/flutter_parent/lib/models/user.dart index 81f25d1621..d55ab108f1 100644 --- a/apps/flutter_parent/lib/models/user.dart +++ b/apps/flutter_parent/lib/models/user.dart @@ -60,6 +60,10 @@ abstract class User implements Built { @nullable UserPermission get permissions; + @nullable + @BuiltValueField(wireName: 'login_id') + String get loginId; + static void _initializeBuilder(UserBuilder b) => b ..id = '' ..name = ''; diff --git a/apps/flutter_parent/lib/models/user.g.dart b/apps/flutter_parent/lib/models/user.g.dart index 6b6960237d..258aefd8f8 100644 --- a/apps/flutter_parent/lib/models/user.g.dart +++ b/apps/flutter_parent/lib/models/user.g.dart @@ -25,62 +25,62 @@ class _$UserSerializer implements StructuredSerializer { 'name', serializers.serialize(object.name, specifiedType: const FullType(String)), ]; - result.add('sortable_name'); - if (object.sortableName == null) { - result.add(null); - } else { - result.add(serializers.serialize(object.sortableName, - specifiedType: const FullType(String))); - } - result.add('short_name'); - if (object.shortName == null) { - result.add(null); - } else { - result.add(serializers.serialize(object.shortName, - specifiedType: const FullType(String))); - } - result.add('pronouns'); - if (object.pronouns == null) { - result.add(null); - } else { - result.add(serializers.serialize(object.pronouns, - specifiedType: const FullType(String))); - } - result.add('avatar_url'); - if (object.avatarUrl == null) { - result.add(null); - } else { - result.add(serializers.serialize(object.avatarUrl, - specifiedType: const FullType(String))); - } - result.add('primary_email'); - if (object.primaryEmail == null) { - result.add(null); - } else { - result.add(serializers.serialize(object.primaryEmail, - specifiedType: const FullType(String))); - } - result.add('locale'); - if (object.locale == null) { - result.add(null); - } else { - result.add(serializers.serialize(object.locale, - specifiedType: const FullType(String))); - } - result.add('effective_locale'); - if (object.effectiveLocale == null) { - result.add(null); - } else { - result.add(serializers.serialize(object.effectiveLocale, - specifiedType: const FullType(String))); - } - result.add('permissions'); - if (object.permissions == null) { - result.add(null); - } else { - result.add(serializers.serialize(object.permissions, + Object value; + value = object.sortableName; + + result + ..add('sortable_name') + ..add( + serializers.serialize(value, specifiedType: const FullType(String))); + value = object.shortName; + + result + ..add('short_name') + ..add( + serializers.serialize(value, specifiedType: const FullType(String))); + value = object.pronouns; + + result + ..add('pronouns') + ..add( + serializers.serialize(value, specifiedType: const FullType(String))); + value = object.avatarUrl; + + result + ..add('avatar_url') + ..add( + serializers.serialize(value, specifiedType: const FullType(String))); + value = object.primaryEmail; + + result + ..add('primary_email') + ..add( + serializers.serialize(value, specifiedType: const FullType(String))); + value = object.locale; + + result + ..add('locale') + ..add( + serializers.serialize(value, specifiedType: const FullType(String))); + value = object.effectiveLocale; + + result + ..add('effective_locale') + ..add( + serializers.serialize(value, specifiedType: const FullType(String))); + value = object.permissions; + + result + ..add('permissions') + ..add(serializers.serialize(value, specifiedType: const FullType(UserPermission))); - } + value = object.loginId; + + result + ..add('login_id') + ..add( + serializers.serialize(value, specifiedType: const FullType(String))); + return result; } @@ -93,8 +93,7 @@ class _$UserSerializer implements StructuredSerializer { while (iterator.moveNext()) { final key = iterator.current as String; iterator.moveNext(); - final dynamic value = iterator.current; - if (value == null) continue; + final Object value = iterator.current; switch (key) { case 'id': result.id = serializers.deserialize(value, @@ -136,6 +135,10 @@ class _$UserSerializer implements StructuredSerializer { result.permissions.replace(serializers.deserialize(value, specifiedType: const FullType(UserPermission)) as UserPermission); break; + case 'login_id': + result.loginId = serializers.deserialize(value, + specifiedType: const FullType(String)) as String; + break; } } @@ -181,8 +184,7 @@ class _$UserPermissionSerializer while (iterator.moveNext()) { final key = iterator.current as String; iterator.moveNext(); - final dynamic value = iterator.current; - if (value == null) continue; + final Object value = iterator.current; switch (key) { case 'become_user': result.become_user = serializers.deserialize(value, @@ -228,6 +230,8 @@ class _$User extends User { final String effectiveLocale; @override final UserPermission permissions; + @override + final String loginId; factory _$User([void Function(UserBuilder) updates]) => (new UserBuilder()..update(updates)).build(); @@ -242,14 +246,11 @@ class _$User extends User { this.primaryEmail, this.locale, this.effectiveLocale, - this.permissions}) + this.permissions, + this.loginId}) : super._() { - if (id == null) { - throw new BuiltValueNullFieldError('User', 'id'); - } - if (name == null) { - throw new BuiltValueNullFieldError('User', 'name'); - } + BuiltValueNullFieldError.checkNotNull(id, 'User', 'id'); + BuiltValueNullFieldError.checkNotNull(name, 'User', 'name'); } @override @@ -272,7 +273,8 @@ class _$User extends User { primaryEmail == other.primaryEmail && locale == other.locale && effectiveLocale == other.effectiveLocale && - permissions == other.permissions; + permissions == other.permissions && + loginId == other.loginId; } @override @@ -284,15 +286,17 @@ class _$User extends User { $jc( $jc( $jc( - $jc($jc($jc(0, id.hashCode), name.hashCode), - sortableName.hashCode), - shortName.hashCode), - pronouns.hashCode), - avatarUrl.hashCode), - primaryEmail.hashCode), - locale.hashCode), - effectiveLocale.hashCode), - permissions.hashCode)); + $jc( + $jc($jc($jc(0, id.hashCode), name.hashCode), + sortableName.hashCode), + shortName.hashCode), + pronouns.hashCode), + avatarUrl.hashCode), + primaryEmail.hashCode), + locale.hashCode), + effectiveLocale.hashCode), + permissions.hashCode), + loginId.hashCode)); } @override @@ -307,7 +311,8 @@ class _$User extends User { ..add('primaryEmail', primaryEmail) ..add('locale', locale) ..add('effectiveLocale', effectiveLocale) - ..add('permissions', permissions)) + ..add('permissions', permissions) + ..add('loginId', loginId)) .toString(); } } @@ -358,22 +363,28 @@ class UserBuilder implements Builder { set permissions(UserPermissionBuilder permissions) => _$this._permissions = permissions; + String _loginId; + String get loginId => _$this._loginId; + set loginId(String loginId) => _$this._loginId = loginId; + UserBuilder() { User._initializeBuilder(this); } UserBuilder get _$this { - if (_$v != null) { - _id = _$v.id; - _name = _$v.name; - _sortableName = _$v.sortableName; - _shortName = _$v.shortName; - _pronouns = _$v.pronouns; - _avatarUrl = _$v.avatarUrl; - _primaryEmail = _$v.primaryEmail; - _locale = _$v.locale; - _effectiveLocale = _$v.effectiveLocale; - _permissions = _$v.permissions?.toBuilder(); + final $v = _$v; + if ($v != null) { + _id = $v.id; + _name = $v.name; + _sortableName = $v.sortableName; + _shortName = $v.shortName; + _pronouns = $v.pronouns; + _avatarUrl = $v.avatarUrl; + _primaryEmail = $v.primaryEmail; + _locale = $v.locale; + _effectiveLocale = $v.effectiveLocale; + _permissions = $v.permissions?.toBuilder(); + _loginId = $v.loginId; _$v = null; } return this; @@ -381,9 +392,7 @@ class UserBuilder implements Builder { @override void replace(User other) { - if (other == null) { - throw new ArgumentError.notNull('other'); - } + ArgumentError.checkNotNull(other, 'other'); _$v = other as _$User; } @@ -398,8 +407,8 @@ class UserBuilder implements Builder { try { _$result = _$v ?? new _$User._( - id: id, - name: name, + id: BuiltValueNullFieldError.checkNotNull(id, 'User', 'id'), + name: BuiltValueNullFieldError.checkNotNull(name, 'User', 'name'), sortableName: sortableName, shortName: shortName, pronouns: pronouns, @@ -407,7 +416,8 @@ class UserBuilder implements Builder { primaryEmail: primaryEmail, locale: locale, effectiveLocale: effectiveLocale, - permissions: _permissions?.build()); + permissions: _permissions?.build(), + loginId: loginId); } catch (_) { String _$failedField; try { @@ -443,19 +453,14 @@ class _$UserPermission extends UserPermission { this.canUpdateAvatar, this.limitParentAppWebAccess}) : super._() { - if (become_user == null) { - throw new BuiltValueNullFieldError('UserPermission', 'become_user'); - } - if (canUpdateName == null) { - throw new BuiltValueNullFieldError('UserPermission', 'canUpdateName'); - } - if (canUpdateAvatar == null) { - throw new BuiltValueNullFieldError('UserPermission', 'canUpdateAvatar'); - } - if (limitParentAppWebAccess == null) { - throw new BuiltValueNullFieldError( - 'UserPermission', 'limitParentAppWebAccess'); - } + BuiltValueNullFieldError.checkNotNull( + become_user, 'UserPermission', 'become_user'); + BuiltValueNullFieldError.checkNotNull( + canUpdateName, 'UserPermission', 'canUpdateName'); + BuiltValueNullFieldError.checkNotNull( + canUpdateAvatar, 'UserPermission', 'canUpdateAvatar'); + BuiltValueNullFieldError.checkNotNull( + limitParentAppWebAccess, 'UserPermission', 'limitParentAppWebAccess'); } @override @@ -523,11 +528,12 @@ class UserPermissionBuilder } UserPermissionBuilder get _$this { - if (_$v != null) { - _become_user = _$v.become_user; - _canUpdateName = _$v.canUpdateName; - _canUpdateAvatar = _$v.canUpdateAvatar; - _limitParentAppWebAccess = _$v.limitParentAppWebAccess; + final $v = _$v; + if ($v != null) { + _become_user = $v.become_user; + _canUpdateName = $v.canUpdateName; + _canUpdateAvatar = $v.canUpdateAvatar; + _limitParentAppWebAccess = $v.limitParentAppWebAccess; _$v = null; } return this; @@ -535,9 +541,7 @@ class UserPermissionBuilder @override void replace(UserPermission other) { - if (other == null) { - throw new ArgumentError.notNull('other'); - } + ArgumentError.checkNotNull(other, 'other'); _$v = other as _$UserPermission; } @@ -550,13 +554,19 @@ class UserPermissionBuilder _$UserPermission build() { final _$result = _$v ?? new _$UserPermission._( - become_user: become_user, - canUpdateName: canUpdateName, - canUpdateAvatar: canUpdateAvatar, - limitParentAppWebAccess: limitParentAppWebAccess); + become_user: BuiltValueNullFieldError.checkNotNull( + become_user, 'UserPermission', 'become_user'), + canUpdateName: BuiltValueNullFieldError.checkNotNull( + canUpdateName, 'UserPermission', 'canUpdateName'), + canUpdateAvatar: BuiltValueNullFieldError.checkNotNull( + canUpdateAvatar, 'UserPermission', 'canUpdateAvatar'), + limitParentAppWebAccess: BuiltValueNullFieldError.checkNotNull( + limitParentAppWebAccess, + 'UserPermission', + 'limitParentAppWebAccess')); replace(_$result); return _$result; } } -// ignore_for_file: always_put_control_body_on_new_line,always_specify_types,annotate_overrides,avoid_annotating_with_dynamic,avoid_as,avoid_catches_without_on_clauses,avoid_returning_this,lines_longer_than_80_chars,omit_local_variable_types,prefer_expression_function_bodies,sort_constructors_first,test_types_in_equals,unnecessary_const,unnecessary_new +// ignore_for_file: always_put_control_body_on_new_line,always_specify_types,annotate_overrides,avoid_annotating_with_dynamic,avoid_as,avoid_catches_without_on_clauses,avoid_returning_this,deprecated_member_use_from_same_package,lines_longer_than_80_chars,omit_local_variable_types,prefer_expression_function_bodies,sort_constructors_first,test_types_in_equals,unnecessary_const,unnecessary_new diff --git a/apps/flutter_parent/lib/screens/settings/settings_interactor.dart b/apps/flutter_parent/lib/screens/settings/settings_interactor.dart index 5eb14752d1..74dffb7968 100644 --- a/apps/flutter_parent/lib/screens/settings/settings_interactor.dart +++ b/apps/flutter_parent/lib/screens/settings/settings_interactor.dart @@ -12,14 +12,19 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . +import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; +import 'package:flutter_parent/l10n/app_localizations.dart'; import 'package:flutter_parent/network/utils/analytics.dart'; +import 'package:flutter_parent/network/utils/api_prefs.dart'; import 'package:flutter_parent/screens/remote_config/remote_config_screen.dart'; import 'package:flutter_parent/utils/debug_flags.dart'; import 'package:flutter_parent/utils/design/parent_theme.dart'; import 'package:flutter_parent/utils/design/theme_transition/theme_transition_target.dart'; import 'package:flutter_parent/utils/quick_nav.dart'; import 'package:flutter_parent/utils/service_locator.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:package_info/package_info.dart'; import '../theme_viewer_screen.dart'; @@ -29,7 +34,7 @@ class SettingsInteractor { void routeToThemeViewer(BuildContext context) { locator().push(context, ThemeViewerScreen()); } - + void routeToRemoteConfig(BuildContext context) { locator().push(context, RemoteConfigScreen()); } @@ -51,4 +56,51 @@ class SettingsInteractor { } ParentTheme.of(context).toggleHC(); } + + void showAboutDialog(context) { + showDialog( + context: context, + builder: (context) => AlertDialog( + title: Text(L10n(context).about), + content: FutureBuilder( + future: PackageInfo.fromPlatform(), + builder: + (BuildContext context, AsyncSnapshot snapshot) { + return SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + mainAxisSize: MainAxisSize.min, + children: [ + Text(L10n(context).aboutAppTitle, + style: TextStyle(fontSize: 16)), + Text(snapshot.data.appName, style: TextStyle(fontSize: 14)), + SizedBox(height: 24), + Text(L10n(context).aboutDomainTitle, + style: TextStyle(fontSize: 16)), + Text(ApiPrefs.getDomain(), style: TextStyle(fontSize: 14)), + SizedBox(height: 24), + Text(L10n(context).aboutLoginIdTitle, + style: TextStyle(fontSize: 16)), + Text(ApiPrefs.getUser().loginId, + style: TextStyle(fontSize: 14)), + SizedBox(height: 24), + Text(L10n(context).aboutEmailTitle, + style: TextStyle(fontSize: 16)), + Text(ApiPrefs.getUser().primaryEmail, + style: TextStyle(fontSize: 14)), + SizedBox(height: 24), + Text(L10n(context).aboutVersionTitle, + style: TextStyle(fontSize: 16)), + Text(snapshot.data.version, style: TextStyle(fontSize: 14)), + SizedBox(height: 32), + SvgPicture.asset( + 'assets/svg/ic_instructure_logo.svg', + alignment: Alignment.bottomCenter, + ) + ], + ), + ); + }, + ))); + } } diff --git a/apps/flutter_parent/lib/screens/settings/settings_screen.dart b/apps/flutter_parent/lib/screens/settings/settings_screen.dart index 6efb9f81bc..9b3d4bb798 100644 --- a/apps/flutter_parent/lib/screens/settings/settings_screen.dart +++ b/apps/flutter_parent/lib/screens/settings/settings_screen.dart @@ -41,7 +41,8 @@ class _SettingsScreenState extends State { builder: (context) => Scaffold( appBar: AppBar( title: Text(L10n(context).settings), - bottom: ParentTheme.of(context).appBarDivider(shadowInLightMode: false), + bottom: + ParentTheme.of(context).appBarDivider(shadowInLightMode: false), ), body: ListView( children: [ @@ -52,8 +53,10 @@ class _SettingsScreenState extends State { ), _themeButtons(context), SizedBox(height: 16), - if (ParentTheme.of(context).isDarkMode) _webViewDarkModeSwitch(context), + if (ParentTheme.of(context).isDarkMode) + _webViewDarkModeSwitch(context), _highContrastModeSwitch(context), + _about(context), if (_interactor.isDebugMode()) _themeViewer(context), if (_interactor.isDebugMode()) _remoteConfigs(context) ], @@ -77,7 +80,8 @@ class _SettingsScreenState extends State { semanticsLabel: L10n(context).lightModeLabel, child: SvgPicture.asset( 'assets/svg/panda-light-mode.svg', - excludeFromSemantics: true, // Semantic label is set in _themeOption() + excludeFromSemantics: + true, // Semantic label is set in _themeOption() ), ), _themeOption( @@ -88,7 +92,8 @@ class _SettingsScreenState extends State { semanticsLabel: L10n(context).darkModeLabel, child: SvgPicture.asset( 'assets/svg/panda-dark-mode.svg', - excludeFromSemantics: true, // Semantic label is set in _themeOption() + excludeFromSemantics: + true, // Semantic label is set in _themeOption() ), ), ], @@ -118,7 +123,8 @@ class _SettingsScreenState extends State { foregroundDecoration: selected ? BoxDecoration( borderRadius: BorderRadius.circular(100), - border: Border.all(color: Theme.of(context).accentColor, width: 2), + border: + Border.all(color: Theme.of(context).accentColor, width: 2), ) : null, child: ClipRRect( @@ -130,7 +136,9 @@ class _SettingsScreenState extends State { type: MaterialType.transparency, child: InkWell( key: buttonKey, - onTap: selected ? null : () => _interactor.toggleDarkMode(context, anchorKey), + onTap: selected + ? null + : () => _interactor.toggleDarkMode(context, anchorKey), ), ) ], @@ -182,6 +190,13 @@ class _SettingsScreenState extends State { _interactor.toggleHCMode(context); } + Widget _about(BuildContext context) => ListTile( + key: Key('about'), + title: Row( + children: [Text(L10n(context).about)], + ), + onTap: () => _interactor.showAboutDialog(context)); + Widget _themeViewer(BuildContext context) => ListTile( key: Key('theme-viewer'), title: Row( @@ -194,8 +209,7 @@ class _SettingsScreenState extends State { onTap: () => _interactor.routeToThemeViewer(context), ); - Widget _remoteConfigs(BuildContext context) => - ListTile( + Widget _remoteConfigs(BuildContext context) => ListTile( key: Key('remote-configs'), title: Row( children: [ diff --git a/apps/student/build.gradle b/apps/student/build.gradle index 9ee7e0e952..fb269772e7 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 = 248 - versionName = '6.22.0' + versionCode = 250 + versionName = '6.23.1' vectorDrawables.useSupportLibrary = true multiDexEnabled = true diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/AnnouncementsE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/AnnouncementsE2ETest.kt index 6bd7fb66f6..d34e9611d7 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/AnnouncementsE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/AnnouncementsE2ETest.kt @@ -37,9 +37,7 @@ import java.lang.Thread.sleep class AnnouncementsE2ETest : StudentTest() { override fun displaysPageObjects() = Unit - override fun enableAndConfigureAccessibilityChecks() { - //We don't want to see accessibility errors on E2E tests - } + override fun enableAndConfigureAccessibilityChecks() = Unit @E2E @Test diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/AssignmentsE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/AssignmentsE2ETest.kt index 27c5ca055e..ef04d7437c 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/AssignmentsE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/AssignmentsE2ETest.kt @@ -24,9 +24,7 @@ import com.instructure.canvas.espresso.E2E import com.instructure.dataseeding.api.AssignmentGroupsApi import com.instructure.dataseeding.api.AssignmentsApi import com.instructure.dataseeding.api.SubmissionsApi -import com.instructure.dataseeding.model.FileUploadType -import com.instructure.dataseeding.model.GradingType -import com.instructure.dataseeding.model.SubmissionType +import com.instructure.dataseeding.model.* import com.instructure.dataseeding.util.ago import com.instructure.dataseeding.util.days import com.instructure.dataseeding.util.fromNow @@ -69,14 +67,7 @@ class AssignmentsE2ETest: StudentTest() { val course = data.coursesList[0] Log.d(PREPARATION_TAG,"Seeding 'Text Entry' assignment for ${course.name} course.") - val pointsTextAssignment = AssignmentsApi.createAssignment(AssignmentsApi.CreateAssignmentRequest( - courseId = course.id, - submissionTypes = listOf(SubmissionType.ONLINE_TEXT_ENTRY), - gradingType = GradingType.POINTS, - teacherToken = teacher.token, - pointsPossible = 15.0, - dueAt = 1.days.fromNow.iso8601 - )) + val pointsTextAssignment = createAssignment(course.id, teacher, GradingType.POINTS, 15.0, 1.days.fromNow.iso8601) Log.d(STEP_TAG, "Login with user: ${student.name}, login id: ${student.loginId}.") tokenLogin(student) @@ -105,29 +96,14 @@ class AssignmentsE2ETest: StudentTest() { Espresso.pressBack() Log.d(PREPARATION_TAG,"Submit assignment: ${pointsTextAssignment.name} for student: ${student.name}.") - SubmissionsApi.seedAssignmentSubmission(SubmissionsApi.SubmissionSeedRequest( - assignmentId = pointsTextAssignment.id, - courseId = course.id, - studentToken = student.token, - submissionSeedsList = listOf(SubmissionsApi.SubmissionSeedInfo( - amount = 1, - submissionType = SubmissionType.ONLINE_TEXT_ENTRY - )) - )) + submitAssignment(pointsTextAssignment, course, student) assignmentDetailsPage.refresh() assignmentDetailsPage.assertStatusSubmitted() assignmentDetailsPage.assertSubmissionAndRubricLabel() Log.d(PREPARATION_TAG,"Grade submission: ${pointsTextAssignment.name} with 13 points.") - val textGrade = SubmissionsApi.gradeSubmission( - teacherToken = teacher.token, - courseId = course.id, - assignmentId = pointsTextAssignment.id, - studentId = student.id, - postedGrade = "13", - excused = false - ) + val textGrade = gradeSubmission(teacher, course, pointsTextAssignment.id, student, "13") Log.d(STEP_TAG,"Refresh the page. Assert that the assignment ${pointsTextAssignment.name} has been graded with 13 points.") assignmentDetailsPage.refresh() @@ -137,7 +113,6 @@ class AssignmentsE2ETest: StudentTest() { Espresso.pressBack() assignmentListPage.refresh() assignmentListPage.assertHasAssignment(pointsTextAssignment, textGrade.grade) - } @E2E @@ -152,34 +127,13 @@ class AssignmentsE2ETest: StudentTest() { val course = data.coursesList[0] Log.d(PREPARATION_TAG,"Seeding 'Text Entry' assignment for ${course.name} course.") - val letterGradeTextAssignment = AssignmentsApi.createAssignment(AssignmentsApi.CreateAssignmentRequest( - courseId = course.id, - submissionTypes = listOf(SubmissionType.ONLINE_TEXT_ENTRY), - gradingType = GradingType.LETTER_GRADE, - teacherToken = teacher.token, - pointsPossible = 20.0 - )) + val letterGradeTextAssignment = createAssignment(course.id, teacher, GradingType.LETTER_GRADE, 20.0) Log.d(PREPARATION_TAG,"Submit assignment: ${letterGradeTextAssignment.name} for student: ${student.name}.") - SubmissionsApi.seedAssignmentSubmission(SubmissionsApi.SubmissionSeedRequest( - assignmentId = letterGradeTextAssignment.id, - courseId = course.id, - studentToken = student.token, - submissionSeedsList = listOf(SubmissionsApi.SubmissionSeedInfo( - amount = 1, - submissionType = SubmissionType.ONLINE_TEXT_ENTRY - )) - )) + submitAssignment(letterGradeTextAssignment, course, student) Log.d(PREPARATION_TAG,"Grade submission: ${letterGradeTextAssignment.name} with 13 points.") - val submissionGrade = SubmissionsApi.gradeSubmission( - teacherToken = teacher.token, - courseId = course.id, - assignmentId = letterGradeTextAssignment.id, - studentId = student.id, - postedGrade = "16", - excused = false - ) + val submissionGrade = gradeSubmission(teacher, course, letterGradeTextAssignment.id, student, "13") Log.d(STEP_TAG, "Login with user: ${student.name}, login id: ${student.loginId}.") tokenLogin(student) @@ -191,7 +145,6 @@ class AssignmentsE2ETest: StudentTest() { Log.d(STEP_TAG,"Assert that ${letterGradeTextAssignment.name} assignment is displayed with the corresponding grade: ${submissionGrade.grade}.") assignmentListPage.assertHasAssignment(letterGradeTextAssignment, submissionGrade.grade) - } @E2E @@ -206,14 +159,7 @@ class AssignmentsE2ETest: StudentTest() { val course = data.coursesList[0] Log.d(PREPARATION_TAG,"Seeding assignment for ${course.name} course.") - val percentageFileAssignment = AssignmentsApi.createAssignment(AssignmentsApi.CreateAssignmentRequest( - courseId = course.id, - submissionTypes = listOf(SubmissionType.ONLINE_UPLOAD), - gradingType = GradingType.PERCENT, - teacherToken = teacher.token, - pointsPossible = 25.0, - allowedExtensions = listOf("txt", "pdf", "jpg") - )) + val percentageFileAssignment = createAssignment(course.id, teacher, GradingType.PERCENT, 25.0, allowedExtensions = listOf("txt", "pdf", "jpg"), submissionType = listOf(SubmissionType.ONLINE_UPLOAD)) Log.d(STEP_TAG, "Login with user: ${student.name}, login id: ${student.loginId}.") tokenLogin(student) @@ -226,41 +172,21 @@ class AssignmentsE2ETest: StudentTest() { Log.d(STEP_TAG,"Assert that ${percentageFileAssignment.name} assignment is displayed.") assignmentListPage.assertHasAssignment(percentageFileAssignment) - Log.d(STEP_TAG,"Select assignment: ${percentageFileAssignment.name}.") - Log.d(STEP_TAG,"Click on ${percentageFileAssignment.name} assignment.") assignmentListPage.clickAssignment(percentageFileAssignment) Log.d(PREPARATION_TAG, "Seed a text file.") - val uploadInfo = uploadTextFile( - courseId = course.id, - assignmentId = percentageFileAssignment.id, - token = student.token, - fileUploadType = FileUploadType.ASSIGNMENT_SUBMISSION - ) + val uploadInfo = uploadTextFile(courseId = course.id, assignmentId = percentageFileAssignment.id, token = student.token, fileUploadType = FileUploadType.ASSIGNMENT_SUBMISSION) Log.d(PREPARATION_TAG,"Submit ${percentageFileAssignment.name} assignment for ${student.name} student.") - SubmissionsApi.submitCourseAssignment( - submissionType = SubmissionType.ONLINE_UPLOAD, - courseId = course.id, - assignmentId = percentageFileAssignment.id, - fileIds = listOf(uploadInfo.id).toMutableList(), - studentToken = student.token - ) + submitCourseAssignment(course, percentageFileAssignment, uploadInfo, student) Log.d(STEP_TAG,"Refresh the page. Assert that the ${percentageFileAssignment.name} assignment has been submitted.") assignmentDetailsPage.refresh() assignmentDetailsPage.assertAssignmentSubmitted() Log.d(PREPARATION_TAG,"Grade ${percentageFileAssignment.name} assignment with 22 percentage.") - SubmissionsApi.gradeSubmission( - teacherToken = teacher.token, - courseId = course.id, - assignmentId = percentageFileAssignment.id, - studentId = student.id, - postedGrade = "22", - excused = false - ) + gradeSubmission(teacher, course, percentageFileAssignment, student,"22") Log.d(STEP_TAG,"Refresh the page. Assert that the ${percentageFileAssignment.name} assignment has been graded with 22 percentage.") assignmentDetailsPage.refresh() @@ -271,9 +197,7 @@ class AssignmentsE2ETest: StudentTest() { submissionDetailsPage.openComments() Log.d(STEP_TAG,"Assert that ${uploadInfo.fileName} file has been displayed as a comment.") - submissionDetailsPage.assertCommentDisplayed( - uploadInfo.fileName, - student) + submissionDetailsPage.assertCommentDisplayed(uploadInfo.fileName, student) val newComment = "My comment!!" Log.d(STEP_TAG,"Add a new comment ($newComment) and send it.") @@ -288,7 +212,21 @@ class AssignmentsE2ETest: StudentTest() { Log.d(STEP_TAG,"Assert that ${uploadInfo.fileName} file has been displayed.") submissionDetailsPage.assertFileDisplayed(uploadInfo.fileName) + } + private fun submitCourseAssignment( + course: CourseApiModel, + percentageFileAssignment: AssignmentApiModel, + uploadInfo: AttachmentApiModel, + student: CanvasUserApiModel + ) { + SubmissionsApi.submitCourseAssignment( + submissionType = SubmissionType.ONLINE_UPLOAD, + courseId = course.id, + assignmentId = percentageFileAssignment.id, + fileIds = listOf(uploadInfo.id).toMutableList(), + studentToken = student.token + ) } @E2E @@ -303,65 +241,22 @@ class AssignmentsE2ETest: StudentTest() { val course = data.coursesList[0] Log.d(PREPARATION_TAG,"Seeding assignment for ${course.name} course.") - val letterGradeTextAssignment = AssignmentsApi.createAssignment(AssignmentsApi.CreateAssignmentRequest( - courseId = course.id, - submissionTypes = listOf(SubmissionType.ONLINE_TEXT_ENTRY), - gradingType = GradingType.LETTER_GRADE, - teacherToken = teacher.token, - pointsPossible = 20.0 - )) + val letterGradeTextAssignment = createAssignment(course.id, teacher, GradingType.LETTER_GRADE, 20.0) Log.d(PREPARATION_TAG,"Submit ${letterGradeTextAssignment.name} assignment for ${student.name} student.") - SubmissionsApi.seedAssignmentSubmission(SubmissionsApi.SubmissionSeedRequest( - assignmentId = letterGradeTextAssignment.id, - courseId = course.id, - studentToken = student.token, - submissionSeedsList = listOf(SubmissionsApi.SubmissionSeedInfo( - amount = 1, - submissionType = SubmissionType.ONLINE_TEXT_ENTRY - )) - )) + submitAssignment(letterGradeTextAssignment, course, student) Log.d(PREPARATION_TAG,"Grade ${letterGradeTextAssignment.name} assignment with 16.") - SubmissionsApi.gradeSubmission( - teacherToken = teacher.token, - courseId = course.id, - assignmentId = letterGradeTextAssignment.id, - studentId = student.id, - postedGrade = "16", - excused = false - ) + gradeSubmission(teacher, course, letterGradeTextAssignment, student, "16") Log.d(PREPARATION_TAG,"Seeding assignment for ${course.name} course.") - val pointsTextAssignment = AssignmentsApi.createAssignment(AssignmentsApi.CreateAssignmentRequest( - courseId = course.id, - submissionTypes = listOf(SubmissionType.ONLINE_TEXT_ENTRY), - gradingType = GradingType.POINTS, - teacherToken = teacher.token, - pointsPossible = 15.0, - dueAt = 1.days.fromNow.iso8601 - )) + val pointsTextAssignment = createAssignment(course.id, teacher, GradingType.POINTS, 15.0, 1.days.fromNow.iso8601) Log.d(PREPARATION_TAG,"Submit ${pointsTextAssignment.name} assignment for ${student.name} student.") - SubmissionsApi.seedAssignmentSubmission(SubmissionsApi.SubmissionSeedRequest( - assignmentId = pointsTextAssignment.id, - courseId = course.id, - studentToken = student.token, - submissionSeedsList = listOf(SubmissionsApi.SubmissionSeedInfo( - amount = 1, - submissionType = SubmissionType.ONLINE_TEXT_ENTRY - )) - )) + submitAssignment(pointsTextAssignment, course, student) Log.d(PREPARATION_TAG,"Grade ${pointsTextAssignment.name} assignment with 13 points.") - SubmissionsApi.gradeSubmission( - teacherToken = teacher.token, - courseId = course.id, - assignmentId = pointsTextAssignment.id, - studentId = student.id, - postedGrade = "13", - excused = false - ) + gradeSubmission(teacher, course, pointsTextAssignment.id, student, "13") Log.d(STEP_TAG, "Login with user: ${student.name}, login id: ${student.loginId}.") tokenLogin(student) @@ -389,62 +284,22 @@ class AssignmentsE2ETest: StudentTest() { val course = data.coursesList[0] Log.d(PREPARATION_TAG,"Seeding assignment for '${course.name}' course.") - val upcomingAssignment = AssignmentsApi.createAssignment(AssignmentsApi.CreateAssignmentRequest( - courseId = course.id, - submissionTypes = listOf(SubmissionType.ONLINE_TEXT_ENTRY), - gradingType = GradingType.LETTER_GRADE, - teacherToken = teacher.token, - pointsPossible = 20.0 - )) + val upcomingAssignment = createAssignment(course.id, teacher, GradingType.LETTER_GRADE, 20.0) Log.d(PREPARATION_TAG,"Seeding assignment for '${course.name}' course.") - val missingAssignment = AssignmentsApi.createAssignment(AssignmentsApi.CreateAssignmentRequest( - courseId = course.id, - submissionTypes = listOf(SubmissionType.ONLINE_TEXT_ENTRY), - gradingType = GradingType.LETTER_GRADE, - teacherToken = teacher.token, - pointsPossible = 20.0, - dueAt = 2.days.ago.iso8601 - )) + val missingAssignment = createAssignment(course.id, teacher, GradingType.LETTER_GRADE, 20.0, 2.days.ago.iso8601) Log.d(PREPARATION_TAG,"Seeding a GRADED assignment for ${course.name} course.") - val gradedAssignment = AssignmentsApi.createAssignment(AssignmentsApi.CreateAssignmentRequest( - courseId = course.id, - submissionTypes = listOf(SubmissionType.ONLINE_TEXT_ENTRY), - gradingType = GradingType.LETTER_GRADE, - teacherToken = teacher.token, - pointsPossible = 20.0 - )) + val gradedAssignment = createAssignment(course.id, teacher, GradingType.LETTER_GRADE, 20.0) Log.d(PREPARATION_TAG,"Grade the '${gradedAssignment.name}' with '11' points out of 20.") - SubmissionsApi.gradeSubmission( - teacherToken = teacher.token, - courseId = course.id, - assignmentId = gradedAssignment.id, - studentId = student.id, - postedGrade = "11", - excused = false - ) + gradeSubmission(teacher, course, gradedAssignment, student, "11") Log.d(PREPARATION_TAG,"Create an Assignment Group for '${course.name}' course.") - val assignmentGroup = AssignmentGroupsApi.createAssignmentGroup( - token = teacher.token, - courseId = course.id, - name = "Discussions", - position = null, - groupWeight = null, - sisSourceId = null - ) + val assignmentGroup = createAssignmentGroup(teacher, course) Log.d(PREPARATION_TAG,"Seeding assignment for '${course.name}' course.") - val otherTypeAssignment = AssignmentsApi.createAssignment(AssignmentsApi.CreateAssignmentRequest( - courseId = course.id, - submissionTypes = listOf(SubmissionType.ONLINE_TEXT_ENTRY), - gradingType = GradingType.LETTER_GRADE, - teacherToken = teacher.token, - pointsPossible = 20.0, - assignmentGroupId = assignmentGroup.id - )) + val otherTypeAssignment = createAssignment(course.id, teacher, GradingType.LETTER_GRADE, 20.0, assignmentGroupId = assignmentGroup.id) Log.d(STEP_TAG, "Login with user: ${student.name}, login id: ${student.loginId}.") tokenLogin(student) @@ -535,9 +390,20 @@ class AssignmentsE2ETest: StudentTest() { assignmentListPage.assertAssignmentNotDisplayed(upcomingAssignment.name) assignmentListPage.assertAssignmentNotDisplayed(otherTypeAssignment.name) assignmentListPage.assertAssignmentNotDisplayed(gradedAssignment.name) - } + private fun createAssignmentGroup( + teacher: CanvasUserApiModel, + course: CourseApiModel + ) = AssignmentGroupsApi.createAssignmentGroup( + token = teacher.token, + courseId = course.id, + name = "Discussions", + position = null, + groupWeight = null, + sisSourceId = null + ) + @E2E @Test @TestMetaData(Priority.MANDATORY, FeatureCategory.COMMENTS, TestCategory.E2E) @@ -550,25 +416,10 @@ class AssignmentsE2ETest: StudentTest() { val course = data.coursesList[0] Log.d(PREPARATION_TAG,"Seeding assignment for ${course.name} course.") - val assignment = AssignmentsApi.createAssignment(AssignmentsApi.CreateAssignmentRequest( - courseId = course.id, - submissionTypes = listOf(SubmissionType.ONLINE_TEXT_ENTRY), - gradingType = GradingType.POINTS, - teacherToken = teacher.token, - pointsPossible = 15.0, - dueAt = 1.days.fromNow.iso8601 - )) + val assignment = createAssignment(course.id, teacher, GradingType.POINTS, 15.0, 1.days.fromNow.iso8601) Log.d(PREPARATION_TAG,"Submit ${assignment.name} assignment for ${student.name} student.") - SubmissionsApi.seedAssignmentSubmission(SubmissionsApi.SubmissionSeedRequest( - assignmentId = assignment.id, - courseId = course.id, - studentToken = student.token, - submissionSeedsList = listOf(SubmissionsApi.SubmissionSeedInfo( - amount = 1, - submissionType = SubmissionType.ONLINE_TEXT_ENTRY - )) - )) + submitAssignment(assignment, course, student) Log.d(STEP_TAG, "Login with user: ${student.name}, login id: ${student.loginId}.") tokenLogin(student) @@ -609,16 +460,7 @@ class AssignmentsE2ETest: StudentTest() { val course = data.coursesList[0] Log.d(PREPARATION_TAG, "Seeding 'Text Entry' assignment for ${course.name} course.") - val pointsTextAssignment = AssignmentsApi.createAssignment( - AssignmentsApi.CreateAssignmentRequest( - courseId = course.id, - submissionTypes = listOf(SubmissionType.ONLINE_TEXT_ENTRY), - gradingType = GradingType.POINTS, - teacherToken = teacher.token, - pointsPossible = 15.0, - dueAt = 1.days.fromNow.iso8601 - ) - ) + val pointsTextAssignment = createAssignment(course.id, teacher, GradingType.POINTS, 15.0, 1.days.fromNow.iso8601) Log.d(STEP_TAG, "Login with user: ${student.name}, login id: ${student.loginId}.") tokenLogin(student) @@ -636,13 +478,7 @@ class AssignmentsE2ETest: StudentTest() { assignmentListPage.clickAssignment(pointsTextAssignment) Log.d(PREPARATION_TAG,"Submit assignment: ${pointsTextAssignment.name} for student: ${student.name}.") - SubmissionsApi.submitCourseAssignment( - submissionType = SubmissionType.ONLINE_TEXT_ENTRY, - courseId = course.id, - assignmentId = pointsTextAssignment.id, - studentToken = student.token, - fileIds = emptyList().toMutableList() - ) + submitAssignment(pointsTextAssignment, course, student) Log.d(STEP_TAG, "Refresh the page.") assignmentDetailsPage.refresh() @@ -651,13 +487,7 @@ class AssignmentsE2ETest: StudentTest() { assignmentDetailsPage.assertNoAttemptSpinner() Log.d(PREPARATION_TAG,"Generate another submission for assignment: ${pointsTextAssignment.name} for student: ${student.name}.") - SubmissionsApi.submitCourseAssignment( - submissionType = SubmissionType.ONLINE_TEXT_ENTRY, - courseId = course.id, - assignmentId = pointsTextAssignment.id, - studentToken = student.token, - fileIds = emptyList().toMutableList() - ) + submitAssignment(pointsTextAssignment, course, student) Log.d(STEP_TAG, "Refresh the page.") assignmentDetailsPage.refresh() @@ -692,14 +522,7 @@ class AssignmentsE2ETest: StudentTest() { val course = data.coursesList[0] Log.d(PREPARATION_TAG,"Seeding 'Text Entry' assignment for ${course.name} course.") - val pointsTextAssignment = AssignmentsApi.createAssignment(AssignmentsApi.CreateAssignmentRequest( - courseId = course.id, - submissionTypes = listOf(SubmissionType.ONLINE_TEXT_ENTRY), - gradingType = GradingType.POINTS, - teacherToken = teacher.token, - pointsPossible = 15.0, - dueAt = 1.days.fromNow.iso8601 - )) + val pointsTextAssignment = createAssignment(course.id, teacher, GradingType.POINTS, 15.0, 1.days.fromNow.iso8601) Log.d(STEP_TAG, "Login with user: ${student.name}, login id: ${student.loginId}.") tokenLogin(student) @@ -723,36 +546,22 @@ class AssignmentsE2ETest: StudentTest() { assignmentDetailsPage.assertSubmissionAndRubricLabel() assignmentDetailsPage.goToSubmissionDetails() - Log.d(STEP_TAG, "Assert that there is no submission yet for the '${pointsTextAssignment.name}' assignment.") + Log.d(STEP_TAG, "Assert that there is no submission yet for the '${pointsTextAssignment.name}' assignment. Navigate back to Assignment Details page.") submissionDetailsPage.assertNoSubmissionEmptyView() Espresso.pressBack() Log.d(PREPARATION_TAG,"Submit assignment: ${pointsTextAssignment.name} for student: ${student.name}.") - val firstSubmissionAttempt = SubmissionsApi.seedAssignmentSubmission(SubmissionsApi.SubmissionSeedRequest( - assignmentId = pointsTextAssignment.id, - courseId = course.id, - studentToken = student.token, - submissionSeedsList = listOf(SubmissionsApi.SubmissionSeedInfo( - amount = 1, - submissionType = SubmissionType.ONLINE_TEXT_ENTRY - )) - )) + submitAssignment(pointsTextAssignment, course, student) + Log.d(STEP_TAG, "Refresh the Assignment Details Page. Assert that the assignment's status is submitted and the 'Submission and Rubric' label is displayed.") assignmentDetailsPage.refresh() assignmentDetailsPage.assertStatusSubmitted() assignmentDetailsPage.assertSubmissionAndRubricLabel() Log.d(PREPARATION_TAG,"Make another submission for assignment: ${pointsTextAssignment.name} for student: ${student.name}.") - val secondSubmissionAttempt = SubmissionsApi.seedAssignmentSubmission(SubmissionsApi.SubmissionSeedRequest( - assignmentId = pointsTextAssignment.id, - courseId = course.id, - studentToken = student.token, - submissionSeedsList = listOf(SubmissionsApi.SubmissionSeedInfo( - amount = 1, - submissionType = SubmissionType.ONLINE_TEXT_ENTRY - )) - )) + val secondSubmissionAttempt = submitAssignment(pointsTextAssignment, course, student) + Log.d(STEP_TAG, "Refresh the Assignment Details Page. Assert that the assignment's status is submitted and the 'Submission and Rubric' label is displayed.") assignmentDetailsPage.refresh() assignmentDetailsPage.assertStatusSubmitted() assignmentDetailsPage.assertSubmissionAndRubricLabel() @@ -794,4 +603,81 @@ class AssignmentsE2ETest: StudentTest() { Log.d(STEP_TAG,"Assert that '${secondSubmissionAttempt[0].body}' text submission has been displayed as a comment.") submissionDetailsPage.assertTextSubmissionDisplayedAsComment() } + + private fun createAssignment( + courseId: Long, + teacher: CanvasUserApiModel, + gradingType: GradingType, + pointsPossible: Double, + dueAt: String = EMPTY_STRING, + allowedExtensions: List? = null, + assignmentGroupId: Long? = null, + submissionType: List = listOf(SubmissionType.ONLINE_TEXT_ENTRY) + ): AssignmentApiModel { + return AssignmentsApi.createAssignment( + AssignmentsApi.CreateAssignmentRequest( + courseId = courseId, + submissionTypes = submissionType, + gradingType = gradingType, + teacherToken = teacher.token, + pointsPossible = pointsPossible, + dueAt = dueAt, + allowedExtensions = allowedExtensions, + assignmentGroupId = assignmentGroupId + ) + ) + } + + private fun submitAssignment( + assignment: AssignmentApiModel, + course: CourseApiModel, + student: CanvasUserApiModel + ): List { + return SubmissionsApi.seedAssignmentSubmission( + SubmissionsApi.SubmissionSeedRequest( + assignmentId = assignment.id, + courseId = course.id, + studentToken = student.token, + submissionSeedsList = listOf( + SubmissionsApi.SubmissionSeedInfo( + amount = 1, + submissionType = SubmissionType.ONLINE_TEXT_ENTRY + ) + ) + ) + ) + } + + private fun gradeSubmission( + teacher: CanvasUserApiModel, + course: CourseApiModel, + assignment: AssignmentApiModel, + student: CanvasUserApiModel, + postedGrade: String, + excused: Boolean = false + ) { + SubmissionsApi.gradeSubmission( + teacherToken = teacher.token, + courseId = course.id, + assignmentId = assignment.id, + studentId = student.id, + postedGrade = postedGrade, + excused = excused + ) + } + + private fun gradeSubmission( + teacher: CanvasUserApiModel, + course: CourseApiModel, + assignmentId: Long, + student: CanvasUserApiModel, + postedGrade: String + ) = SubmissionsApi.gradeSubmission( + teacherToken = teacher.token, + courseId = course.id, + assignmentId = assignmentId, + studentId = student.id, + postedGrade = postedGrade, + excused = false + ) } \ No newline at end of file diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/BookmarksE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/BookmarksE2ETest.kt index c720a5ee66..2e0ae5bba7 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/BookmarksE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/BookmarksE2ETest.kt @@ -21,8 +21,7 @@ import androidx.test.espresso.Espresso import com.instructure.canvas.espresso.E2E import com.instructure.canvas.espresso.refresh import com.instructure.dataseeding.api.AssignmentsApi -import com.instructure.dataseeding.model.GradingType -import com.instructure.dataseeding.model.SubmissionType +import com.instructure.dataseeding.model.* import com.instructure.dataseeding.util.days import com.instructure.dataseeding.util.fromNow import com.instructure.dataseeding.util.iso8601 @@ -39,13 +38,9 @@ import org.junit.Test @HiltAndroidTest class BookmarksE2ETest : StudentTest() { - override fun displaysPageObjects() { - TODO("not implemented") //To change body of created functions use File | Settings | File Templates. - } + override fun displaysPageObjects() = Unit - override fun enableAndConfigureAccessibilityChecks() { - //We don't want to see accessibility errors on E2E tests - } + override fun enableAndConfigureAccessibilityChecks() = Unit @E2E @Test @@ -59,15 +54,7 @@ class BookmarksE2ETest : StudentTest() { val course = data.coursesList[0] Log.d(PREPARATION_TAG,"Preparing an assignment which will be saved as a bookmark.") - val assignment = AssignmentsApi.createAssignment( - AssignmentsApi.CreateAssignmentRequest( - courseId = course.id, - submissionTypes = listOf(SubmissionType.ONLINE_TEXT_ENTRY), - gradingType = GradingType.POINTS, - teacherToken = teacher.token, - pointsPossible = 15.0, - dueAt = 1.days.fromNow.iso8601 - )) + val assignment = createAssignment(course, teacher) Log.d(STEP_TAG,"Login with user: ${student.name}, login id: ${student.loginId}.") tokenLogin(student) @@ -119,4 +106,20 @@ class BookmarksE2ETest : StudentTest() { bookmarkPage.assertEmptyView() } + private fun createAssignment( + course: CourseApiModel, + teacher: CanvasUserApiModel + ): AssignmentApiModel { + return AssignmentsApi.createAssignment( + AssignmentsApi.CreateAssignmentRequest( + courseId = course.id, + submissionTypes = listOf(SubmissionType.ONLINE_TEXT_ENTRY), + gradingType = GradingType.POINTS, + teacherToken = teacher.token, + pointsPossible = 15.0, + dueAt = 1.days.fromNow.iso8601 + ) + ) + } + } \ No newline at end of file diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/ConferencesE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/ConferencesE2ETest.kt index 0e4b170ac8..90a7e6e01a 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/ConferencesE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/ConferencesE2ETest.kt @@ -16,13 +16,9 @@ import org.junit.Test @HiltAndroidTest class ConferencesE2ETest: StudentTest() { - override fun displaysPageObjects() { - TODO("not implemented") //To change body of created functions use File | Settings | File Templates. - } + override fun displaysPageObjects() = Unit - override fun enableAndConfigureAccessibilityChecks() { - //We don't want to see accessibility errors on E2E tests - } + override fun enableAndConfigureAccessibilityChecks() = Unit // Fairly basic test that we can create and view a conference with the app. // I didn't attempt to actually start the conference because that goes through diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/DiscussionsE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/DiscussionsE2ETest.kt index e8f3650e4a..05502815f9 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/DiscussionsE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/DiscussionsE2ETest.kt @@ -21,6 +21,8 @@ import android.util.Log import androidx.test.espresso.Espresso import com.instructure.canvas.espresso.E2E import com.instructure.dataseeding.api.DiscussionTopicsApi +import com.instructure.dataseeding.model.CanvasUserApiModel +import com.instructure.dataseeding.model.CourseApiModel import com.instructure.espresso.ViewUtils import com.instructure.panda_annotations.FeatureCategory import com.instructure.panda_annotations.Priority @@ -34,13 +36,9 @@ import org.junit.Test @HiltAndroidTest class DiscussionsE2ETest: StudentTest() { - override fun displaysPageObjects() { - TODO("not implemented") //To change body of created functions use File | Settings | File Templates. - } + override fun displaysPageObjects() = Unit - override fun enableAndConfigureAccessibilityChecks() { - //We don't want to see accessibility errors on E2E tests - } + override fun enableAndConfigureAccessibilityChecks() = Unit @E2E @Test @@ -54,28 +52,16 @@ class DiscussionsE2ETest: StudentTest() { val course = data.coursesList[0] Log.d(PREPARATION_TAG,"Seed a discussion topic.") - val topic1 = DiscussionTopicsApi.createDiscussion( - courseId = course.id, - token = teacher.token - ) + val topic1 = createDiscussion(course, teacher) Log.d(PREPARATION_TAG,"Seed another discussion topic.") - val topic2 = DiscussionTopicsApi.createDiscussion( - courseId = course.id, - token = teacher.token - ) + val topic2 = createDiscussion(course, teacher) Log.d(STEP_TAG,"Seed an announcement for ${course.name} course.") - val announcement = DiscussionTopicsApi.createAnnouncement( - courseId = course.id, - token = teacher.token - ) + val announcement = createAnnouncement(course, teacher) Log.d(STEP_TAG,"Seed another announcement for ${course.name} course.") - val announcement2 = DiscussionTopicsApi.createAnnouncement( - courseId = course.id, - token = teacher.token - ) + val announcement2 = createAnnouncement(course, teacher) Log.d(STEP_TAG,"Login with user: ${student.name}, login id: ${student.loginId}.") tokenLogin(student) @@ -168,4 +154,20 @@ class DiscussionsE2ETest: StudentTest() { discussionListPage.assertReplyCount(newTopicName, 1) } + + private fun createAnnouncement( + course: CourseApiModel, + teacher: CanvasUserApiModel + ) = DiscussionTopicsApi.createAnnouncement( + courseId = course.id, + token = teacher.token + ) + + private fun createDiscussion( + course: CourseApiModel, + teacher: CanvasUserApiModel + ) = DiscussionTopicsApi.createDiscussion( + courseId = course.id, + token = teacher.token + ) } \ No newline at end of file diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/FilesE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/FilesE2ETest.kt index 4dad8fe5f3..7140ec0967 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/FilesE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/FilesE2ETest.kt @@ -28,8 +28,7 @@ import com.instructure.canvasapi2.utils.weave.tryWeave import com.instructure.dataseeding.api.AssignmentsApi import com.instructure.dataseeding.api.DiscussionTopicsApi import com.instructure.dataseeding.api.SubmissionsApi -import com.instructure.dataseeding.model.FileUploadType -import com.instructure.dataseeding.model.SubmissionType +import com.instructure.dataseeding.model.* import com.instructure.dataseeding.util.Randomizer import com.instructure.panda_annotations.FeatureCategory import com.instructure.panda_annotations.Priority @@ -59,13 +58,7 @@ class FilesE2ETest: StudentTest() { val course = data.coursesList[0] Log.d(PREPARATION_TAG,"Seeding assignment for ${course.name} course.") - val assignment = AssignmentsApi.createAssignment(AssignmentsApi.CreateAssignmentRequest( - courseId = course.id, - withDescription = false, - submissionTypes = listOf(SubmissionType.ONLINE_UPLOAD), - allowedExtensions = listOf("txt"), - teacherToken = teacher.token - )) + val assignment = createAssignment(course, teacher) Log.d(PREPARATION_TAG, "Seed a text file.") val submissionUploadInfo = uploadTextFile( @@ -76,13 +69,7 @@ class FilesE2ETest: StudentTest() { ) Log.d(PREPARATION_TAG,"Submit ${assignment.name} assignment for ${student.name} student.") - SubmissionsApi.submitCourseAssignment( - submissionType = SubmissionType.ONLINE_UPLOAD, - courseId = course.id, - assignmentId = assignment.id, - fileIds = mutableListOf(submissionUploadInfo.id), - studentToken = student.token - ) + submitAssignment(course, assignment, submissionUploadInfo, student) Log.d(STEP_TAG,"Seed a comment attachment upload.") val commentUploadInfo = uploadTextFile( @@ -91,19 +78,10 @@ class FilesE2ETest: StudentTest() { token = student.token, fileUploadType = FileUploadType.COMMENT_ATTACHMENT ) - - SubmissionsApi.commentOnSubmission( - studentToken = student.token, - courseId = course.id, - assignmentId = assignment.id, - fileIds = mutableListOf(commentUploadInfo.id) - ) + commentOnSubmission(student, course, assignment, commentUploadInfo) Log.d(STEP_TAG,"Seed a discussion for ${course.name} course.") - val discussionTopic = DiscussionTopicsApi.createDiscussion( - courseId = course.id, - token = student.token - ) + val discussionTopic = createDiscussion(course, student) Log.d(STEP_TAG,"Login with user: ${student.name}, login id: ${student.loginId}.") tokenLogin(student) @@ -213,4 +191,54 @@ class FilesE2ETest: StudentTest() { Log.d(STEP_TAG,"Assert that empty view is displayed after deletion.") fileListPage.assertViewEmpty() } + + private fun commentOnSubmission( + student: CanvasUserApiModel, + course: CourseApiModel, + assignment: AssignmentApiModel, + commentUploadInfo: AttachmentApiModel + ) { + SubmissionsApi.commentOnSubmission( + studentToken = student.token, + courseId = course.id, + assignmentId = assignment.id, + fileIds = mutableListOf(commentUploadInfo.id) + ) + } + + private fun createAssignment( + course: CourseApiModel, + teacher: CanvasUserApiModel + ) = AssignmentsApi.createAssignment( + AssignmentsApi.CreateAssignmentRequest( + courseId = course.id, + withDescription = false, + submissionTypes = listOf(SubmissionType.ONLINE_UPLOAD), + allowedExtensions = listOf("txt"), + teacherToken = teacher.token + ) + ) + + private fun submitAssignment( + course: CourseApiModel, + assignment: AssignmentApiModel, + submissionUploadInfo: AttachmentApiModel, + student: CanvasUserApiModel + ) { + SubmissionsApi.submitCourseAssignment( + submissionType = SubmissionType.ONLINE_UPLOAD, + courseId = course.id, + assignmentId = assignment.id, + fileIds = mutableListOf(submissionUploadInfo.id), + studentToken = student.token + ) + } + + private fun createDiscussion( + course: CourseApiModel, + student: CanvasUserApiModel + ) = DiscussionTopicsApi.createDiscussion( + courseId = course.id, + token = student.token + ) } \ No newline at end of file diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/GradesE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/GradesE2ETest.kt index 1511a783d3..bde17c791b 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/GradesE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/GradesE2ETest.kt @@ -7,10 +7,7 @@ import com.instructure.canvas.espresso.containsTextCaseInsensitive import com.instructure.dataseeding.api.AssignmentsApi import com.instructure.dataseeding.api.QuizzesApi import com.instructure.dataseeding.api.SubmissionsApi -import com.instructure.dataseeding.model.GradingType -import com.instructure.dataseeding.model.QuizAnswer -import com.instructure.dataseeding.model.QuizQuestion -import com.instructure.dataseeding.model.SubmissionType +import com.instructure.dataseeding.model.* import com.instructure.dataseeding.util.days import com.instructure.dataseeding.util.fromNow import com.instructure.dataseeding.util.iso8601 @@ -27,13 +24,9 @@ import org.junit.Test @HiltAndroidTest class GradesE2ETest: StudentTest() { - override fun displaysPageObjects() { - TODO("not implemented") //To change body of created functions use File | Settings | File Templates. - } + override fun displaysPageObjects() = Unit - override fun enableAndConfigureAccessibilityChecks() { - //We don't want to see accessibility errors on E2E tests - } + override fun enableAndConfigureAccessibilityChecks() = Unit @E2E @Test @@ -47,40 +40,10 @@ class GradesE2ETest: StudentTest() { val course = data.coursesList[0] Log.d(PREPARATION_TAG,"Seeding assignment for ${course.name} course.") - val assignment = AssignmentsApi.createAssignment(AssignmentsApi.CreateAssignmentRequest( - courseId = course.id, - withDescription = true, - dueAt = 1.days.fromNow.iso8601, - submissionTypes = listOf(SubmissionType.ONLINE_TEXT_ENTRY), - teacherToken = teacher.token, - gradingType = GradingType.PERCENT, - pointsPossible = 15.0 - )) + val assignment = createAssignment(course, teacher) Log.d(PREPARATION_TAG,"Create a quiz with some questions.") - val quizQuestions = listOf( - QuizQuestion( - pointsPossible = 5, - questionType = "multiple_choice_question", - questionText = "Odd or even?", - answers = listOf( - QuizAnswer(id = 1, weight = 1, text = "Odd"), - QuizAnswer(id = 1, weight = 1, text = "Even") - ) - - ), - QuizQuestion( - pointsPossible = 5, - questionType = "multiple_choice_question", - questionText = "How many roads must a man walk down?", - answers = listOf( - QuizAnswer(id = 1, weight = 1, text = "42"), - QuizAnswer(id = 1, weight = 1, text = "A Gazillion"), - QuizAnswer(id = 1, weight = 1, text = "13") - ) - - ) - ) + val quizQuestions = makeQuizQuestions() Log.d(STEP_TAG,"Publish the previously made quiz.") val quiz = QuizzesApi.createAndPublishQuiz(course.id, teacher.token, quizQuestions) @@ -123,22 +86,10 @@ class GradesE2ETest: StudentTest() { courseGradesPage.assertTotalGrade(withText(R.string.noGradeText)) Log.d(PREPARATION_TAG,"Seed a submission for ${assignment.name} assignment.") - SubmissionsApi.submitCourseAssignment( - submissionType = SubmissionType.ONLINE_TEXT_ENTRY, - courseId = course.id, - assignmentId = assignment.id, - fileIds = mutableListOf(), - studentToken = student.token - ) + submitAssignment(course, assignment, student) Log.d(PREPARATION_TAG,"Grade the previously seeded submission for ${assignment.name} assignment.") - SubmissionsApi.gradeSubmission( - teacherToken = teacher.token, - courseId = course.id, - assignmentId = assignment.id, - studentId = student.id, - postedGrade="9", - excused = false) + gradeSubmission(teacher, course, assignment, student) Log.d(STEP_TAG,"Refresh the page. Assert that the assignment's score is '60'.") courseGradesPage.refresh() @@ -170,4 +121,75 @@ class GradesE2ETest: StudentTest() { */ } + private fun makeQuizQuestions() = listOf( + QuizQuestion( + pointsPossible = 5, + questionType = "multiple_choice_question", + questionText = "Odd or even?", + answers = listOf( + QuizAnswer(id = 1, weight = 1, text = "Odd"), + QuizAnswer(id = 1, weight = 1, text = "Even") + ) + + ), + QuizQuestion( + pointsPossible = 5, + questionType = "multiple_choice_question", + questionText = "How many roads must a man walk down?", + answers = listOf( + QuizAnswer(id = 1, weight = 1, text = "42"), + QuizAnswer(id = 1, weight = 1, text = "A Gazillion"), + QuizAnswer(id = 1, weight = 1, text = "13") + ) + + ) + ) + + private fun createAssignment( + course: CourseApiModel, + teacher: CanvasUserApiModel + ): AssignmentApiModel { + return AssignmentsApi.createAssignment( + AssignmentsApi.CreateAssignmentRequest( + courseId = course.id, + withDescription = true, + dueAt = 1.days.fromNow.iso8601, + submissionTypes = listOf(SubmissionType.ONLINE_TEXT_ENTRY), + teacherToken = teacher.token, + gradingType = GradingType.PERCENT, + pointsPossible = 15.0 + ) + ) + } + + private fun submitAssignment( + course: CourseApiModel, + assignment: AssignmentApiModel, + student: CanvasUserApiModel + ) { + SubmissionsApi.submitCourseAssignment( + submissionType = SubmissionType.ONLINE_TEXT_ENTRY, + courseId = course.id, + assignmentId = assignment.id, + fileIds = mutableListOf(), + studentToken = student.token + ) + } + + private fun gradeSubmission( + teacher: CanvasUserApiModel, + course: CourseApiModel, + assignment: AssignmentApiModel, + student: CanvasUserApiModel + ) { + SubmissionsApi.gradeSubmission( + teacherToken = teacher.token, + courseId = course.id, + assignmentId = assignment.id, + studentId = student.id, + postedGrade = "9", + excused = false + ) + } + } 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 28147fef0e..6e8c9a8398 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 @@ -24,6 +24,7 @@ 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.panda_annotations.FeatureCategory import com.instructure.panda_annotations.Priority import com.instructure.panda_annotations.TestCategory @@ -68,10 +69,7 @@ class InboxE2ETest: StudentTest() { inboxPage.assertInboxEmpty() Log.d(PREPARATION_TAG,"Seed an email from the teacher to ${student1.name} and ${student2.name} students.") - val seededConversation = ConversationsApi.createConversation( - token = teacher.token, - recipients = listOf(student1.id.toString(), student2.id.toString()) - )[0] + val seededConversation = createConversation(teacher, student1, student2)[0] Log.d(STEP_TAG,"Refresh the page. Assert that there is a conversation and it is the previously seeded one.") refresh() @@ -84,12 +82,7 @@ class InboxE2ETest: StudentTest() { val newMessageSubject = "Hey There" val newMessage = "Just checking in" Log.d(STEP_TAG,"Create a new message with subject: $newMessageSubject, and message: $newMessage") - newMessagePage.populateMessage( - course, - student2, - newMessageSubject, - newMessage - ) + newMessagePage.populateMessage(course, student2, newMessageSubject, newMessage) Log.d(STEP_TAG,"Click on 'Send' button.") newMessagePage.clickSend() @@ -100,17 +93,12 @@ class InboxE2ETest: StudentTest() { val newGroupMessageSubject = "Group Message" val newGroupMessage = "Testing Group ${group.name}" Log.d(STEP_TAG,"Create a new message with subject: $newGroupMessageSubject, and message: $newGroupMessage") - newMessagePage.populateGroupMessage( - group, - student2, - newGroupMessageSubject, - newGroupMessage - ) + newMessagePage.populateGroupMessage(group, student2, newGroupMessageSubject, newGroupMessage) Log.d(STEP_TAG,"Click on 'Send' button.") newMessagePage.clickSend() - sleep(3000) // Allow time for messages to propagate + sleep(2000) // Allow time for messages to propagate Log.d(STEP_TAG,"Navigate back to Dashboard Page.") inboxPage.goToDashboard() @@ -184,8 +172,11 @@ class InboxE2ETest: StudentTest() { inboxPage.selectConversation(seededConversation) inboxPage.assertSelectedConversationNumber("1") inboxPage.clickUnArchive() + inboxPage.assertInboxEmpty() inboxPage.assertConversationNotDisplayed(seededConversation.subject) + sleep(2000) + Log.d(STEP_TAG,"Navigate to 'INBOX' scope and assert that ${seededConversation.subject} conversation is displayed.") inboxPage.filterInbox("Inbox") inboxPage.assertConversationDisplayed(seededConversation.subject) @@ -212,6 +203,8 @@ class InboxE2ETest: StudentTest() { inboxPage.assertConversationNotDisplayed(seededConversation.subject) inboxPage.assertConversationNotDisplayed(newGroupMessageSubject) + sleep(2000) + Log.d(STEP_TAG, "Navigate to 'ARCHIVED' scope and assert that both of the conversations are displayed there.") inboxPage.filterInbox("Archived") inboxPage.assertConversationDisplayed(seededConversation.subject) @@ -233,6 +226,8 @@ class InboxE2ETest: StudentTest() { inboxPage.assertConversationNotDisplayed(seededConversation.subject) inboxPage.assertConversationNotDisplayed(newGroupMessageSubject) + sleep(2000) + Log.d(STEP_TAG, "Navigate to 'ARCHIVED' scope and assert that both of the conversations are displayed there.") inboxPage.filterInbox("Archived") inboxPage.assertConversationDisplayed(seededConversation.subject) @@ -244,6 +239,8 @@ class InboxE2ETest: StudentTest() { inboxPage.assertConversationNotDisplayed(seededConversation.subject) inboxPage.assertConversationNotDisplayed(newGroupMessageSubject) + sleep(2000) + Log.d(STEP_TAG, "Navigate to 'INBOX' scope and assert that both of the conversations are displayed there.") inboxPage.filterInbox("Inbox") inboxPage.assertConversationDisplayed(seededConversation.subject) @@ -280,6 +277,8 @@ class InboxE2ETest: StudentTest() { inboxPage.assertSelectedConversationNumber("2") inboxPage.clickMarkAsUnread() + sleep(1000) + Log.d(STEP_TAG, "Navigate to 'STARRED' scope. Assert that both of the conversation are displayed in the 'STARRED' scope.") inboxPage.filterInbox("Starred") inboxPage.assertConversationDisplayed(seededConversation.subject) @@ -318,4 +317,13 @@ class InboxE2ETest: StudentTest() { inboxPage.confirmDelete() inboxPage.assertConversationNotDisplayed(newGroupMessageSubject) } + + private fun createConversation( + teacher: CanvasUserApiModel, + student1: CanvasUserApiModel, + student2: CanvasUserApiModel + ) = ConversationsApi.createConversation( + token = teacher.token, + recipients = listOf(student1.id.toString(), student2.id.toString()) + ) } \ No newline at end of file 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 fd9cee769c..f891bdb88b 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 @@ -39,13 +39,9 @@ import org.junit.Test @HiltAndroidTest class LoginE2ETest : StudentTest() { - override fun displaysPageObjects() { - TODO("not implemented") //To change body of created functions use File | Settings | File Templates. - } + override fun displaysPageObjects() = Unit - override fun enableAndConfigureAccessibilityChecks() { - //We don't want to see accessibility errors on E2E tests - } + override fun enableAndConfigureAccessibilityChecks() = Unit @E2E @Test @@ -229,20 +225,10 @@ class LoginE2ETest : StudentTest() { val course = CoursesApi.createCourse(coursesService = coursesService) Log.d(PREPARATION_TAG,"Enroll ${student.name} student to ${course.name} course.") - EnrollmentsApi.enrollUser( - courseId = course.id, - userId = student.id, - enrollmentType = STUDENT_ENROLLMENT, - enrollmentService = enrollmentsService - ) + enrollUser(course, student, STUDENT_ENROLLMENT, enrollmentsService) Log.d(PREPARATION_TAG,"Enroll ${teacher.name} teacher to ${course.name} course.") - EnrollmentsApi.enrollUser( - courseId = course.id, - userId = teacher.id, - enrollmentType = TEACHER_ENROLLMENT, - enrollmentService = enrollmentsService - ) + enrollUser(course, teacher, TEACHER_ENROLLMENT, enrollmentsService) Log.d(STEP_TAG,"Login with user: ${student.name}, login id: ${student.loginId}.") loginWithUser(student) @@ -257,6 +243,20 @@ class LoginE2ETest : StudentTest() { leftSideNavigationDrawerPage.logout() } + private fun enrollUser( + course: CourseApiModel, + student: CanvasUserApiModel, + enrollmentType: String, + enrollmentsService: EnrollmentsApi.EnrollmentsService + ) { + EnrollmentsApi.enrollUser( + courseId = course.id, + userId = student.id, + enrollmentType = enrollmentType, + enrollmentService = enrollmentsService + ) + } + private fun loginWithUser(user: CanvasUserApiModel, lastSchoolSaved: Boolean = false) { if(lastSchoolSaved) { diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/ModulesE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/ModulesE2ETest.kt index e7db107328..ffb0d5a829 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/ModulesE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/ModulesE2ETest.kt @@ -20,8 +20,7 @@ import android.util.Log import androidx.test.espresso.Espresso import com.instructure.canvas.espresso.E2E import com.instructure.dataseeding.api.* -import com.instructure.dataseeding.model.ModuleItemTypes -import com.instructure.dataseeding.model.SubmissionType +import com.instructure.dataseeding.model.* import com.instructure.dataseeding.util.days import com.instructure.dataseeding.util.fromNow import com.instructure.dataseeding.util.iso8601 @@ -37,17 +36,13 @@ import org.junit.Test @HiltAndroidTest class ModulesE2ETest: StudentTest() { - override fun displaysPageObjects() { - TODO("not implemented") //To change body of created functions use File | Settings | File Templates. - } + override fun displaysPageObjects() = Unit - override fun enableAndConfigureAccessibilityChecks() { - //We don't want to see accessibility errors on E2E tests - } + override fun enableAndConfigureAccessibilityChecks() = Unit @E2E @Test - @TestMetaData(Priority.MANDATORY, FeatureCategory.MODULES, TestCategory.E2E, false) + @TestMetaData(Priority.MANDATORY, FeatureCategory.MODULES, TestCategory.E2E) fun testModulesE2E() { Log.d(PREPARATION_TAG, "Seeding data.") @@ -57,109 +52,41 @@ class ModulesE2ETest: StudentTest() { val course = data.coursesList[0] Log.d(PREPARATION_TAG,"Seeding assignment for ${course.name} course.") - val assignment1 = AssignmentsApi.createAssignment(AssignmentsApi.CreateAssignmentRequest( - courseId = course.id, - withDescription = true, - submissionTypes = listOf(SubmissionType.ONLINE_TEXT_ENTRY), - teacherToken = teacher.token, - dueAt = 1.days.fromNow.iso8601 - )) + val assignment1 = createAssignment(course, true, teacher, 1.days.fromNow.iso8601) Log.d(PREPARATION_TAG,"Seeding another assignment for ${course.name} course.") - val assignment2 = AssignmentsApi.createAssignment(AssignmentsApi.CreateAssignmentRequest( - courseId = course.id, - withDescription = true, - submissionTypes = listOf(SubmissionType.ONLINE_TEXT_ENTRY), - teacherToken = teacher.token, - dueAt = 2.days.fromNow.iso8601 - )) + val assignment2 = createAssignment(course, true, teacher, 2.days.fromNow.iso8601) Log.d(PREPARATION_TAG,"Create a PUBLISHED quiz for ${course.name} course.") - val quiz1 = QuizzesApi.createQuiz(QuizzesApi.CreateQuizRequest( - courseId = course.id, - withDescription = true, - dueAt = 3.days.fromNow.iso8601, - token = teacher.token, - published = true - )) + val quiz1 = createQuiz(course, teacher) Log.d(PREPARATION_TAG,"Create a page for ${course.name} course.") - val page1 = PagesApi.createCoursePage( - courseId = course.id, - published = true, - frontPage = false, - token = teacher.token - ) + val page1 = createCoursePage(course, teacher) Log.d(PREPARATION_TAG,"Create a discussion topic for ${course.name} course.") - val discussionTopic1 = DiscussionTopicsApi.createDiscussion( - courseId = course.id, - token = teacher.token - ) + val discussionTopic1 = createDiscussion(course, teacher) //Modules start out as unpublished. Log.d(PREPARATION_TAG,"Create a module for ${course.name} course.") - val module1 = ModulesApi.createModule( - courseId = course.id, - teacherToken = teacher.token, - unlockAt = null) + val module1 = createModule(course, teacher) Log.d(PREPARATION_TAG,"Create another module for ${course.name} course.") - val module2 = ModulesApi.createModule( - courseId = course.id, - teacherToken = teacher.token, - unlockAt = null) + val module2 = createModule(course, teacher) Log.d(PREPARATION_TAG,"Associate ${assignment1.name} assignment with ${module1.name} module.") - ModulesApi.createModuleItem( - courseId = course.id, - moduleId = module1.id, - teacherToken = teacher.token, - title = assignment1.name, - type = ModuleItemTypes.ASSIGNMENT.stringVal, - contentId = assignment1.id.toString() - ) + createModuleItem(course.id, module1.id, teacher, assignment1.name, ModuleItemTypes.ASSIGNMENT.stringVal, assignment1.id.toString()) Log.d(PREPARATION_TAG,"Associate ${quiz1.title} quiz with ${module1.name} module.") - ModulesApi.createModuleItem( - courseId = course.id, - moduleId = module1.id, - teacherToken = teacher.token, - title = quiz1.title, - type = ModuleItemTypes.QUIZ.stringVal, - contentId = quiz1.id.toString() - ) + createModuleItem(course.id, module1.id, teacher, quiz1.title, ModuleItemTypes.QUIZ.stringVal, quiz1.id.toString()) Log.d(PREPARATION_TAG,"Associate ${assignment2.name} assignment with ${module2.name} module.") - ModulesApi.createModuleItem( - courseId = course.id, - moduleId = module2.id, - teacherToken = teacher.token, - title = assignment2.name, - type = ModuleItemTypes.ASSIGNMENT.stringVal, - contentId = assignment2.id.toString() - ) + createModuleItem(course.id, module2.id, teacher, assignment2.name, ModuleItemTypes.ASSIGNMENT.stringVal, assignment2.id.toString()) Log.d(PREPARATION_TAG,"Associate ${page1.title} page with ${module2.name} module.") - ModulesApi.createModuleItem( - courseId = course.id, - moduleId = module2.id, - teacherToken = teacher.token, - title = page1.title, - type = ModuleItemTypes.PAGE.stringVal, - contentId = null, // Not necessary for Page item - pageUrl = page1.url // Only necessary for Page item - ) + createModuleItem(course.id, module2.id, teacher, page1.title, ModuleItemTypes.PAGE.stringVal, null, page1.url) Log.d(PREPARATION_TAG,"Associate ${discussionTopic1.title} discussion topic with ${module2.name} module.") - ModulesApi.createModuleItem( - courseId = course.id, - moduleId = module2.id, - teacherToken = teacher.token, - title = discussionTopic1.title, - type = ModuleItemTypes.DISCUSSION.stringVal, - contentId = discussionTopic1.id.toString() - ) + createModuleItem(course.id, module2.id, teacher, discussionTopic1.title, ModuleItemTypes.DISCUSSION.stringVal, discussionTopic1.id.toString()) Log.d(STEP_TAG, "Login with user: ${teacher.name}, login id: ${teacher.loginId}.") tokenLogin(student) @@ -183,20 +110,10 @@ class ModulesE2ETest: StudentTest() { Espresso.pressBack() Log.d(PREPARATION_TAG,"Publish ${module1.name} module.") - ModulesApi.updateModule( - courseId = course.id, - id = module1.id, - published = true, - teacherToken = teacher.token - ) + updateModule(course, module1, teacher) Log.d(PREPARATION_TAG,"Publish ${module2.name} module.") - ModulesApi.updateModule( - courseId = course.id, - id = module2.id, - published = true, - teacherToken = teacher.token - ) + updateModule(course, module2, teacher) Log.d(STEP_TAG,"Refresh the page. Assert that the 'Modules' Tab is displayed.") courseBrowserPage.refresh() @@ -217,4 +134,92 @@ class ModulesE2ETest: StudentTest() { modulesPage.assertModuleItemDisplayed(module2, page1.title) modulesPage.assertModuleItemDisplayed(module2, discussionTopic1.title) } + + private fun updateModule( + course: CourseApiModel, + module1: ModuleApiModel, + teacher: CanvasUserApiModel + ) { + ModulesApi.updateModule( + courseId = course.id, + id = module1.id, + published = true, + teacherToken = teacher.token + ) + } + + private fun createModuleItem( + courseId: Long, + moduleId: Long, + teacher: CanvasUserApiModel, + title: String, + moduleItemType: String, + contentId: String?, + pageUrl: String? = null + ) { + ModulesApi.createModuleItem( + courseId = courseId, + moduleId = moduleId, + teacherToken = teacher.token, + title = title, + type = moduleItemType, + contentId = contentId, + pageUrl = pageUrl + ) + } + + private fun createModule( + course: CourseApiModel, + teacher: CanvasUserApiModel + ) = ModulesApi.createModule( + courseId = course.id, + teacherToken = teacher.token, + unlockAt = null + ) + + private fun createDiscussion( + course: CourseApiModel, + teacher: CanvasUserApiModel + ) = DiscussionTopicsApi.createDiscussion( + courseId = course.id, + token = teacher.token + ) + + private fun createCoursePage( + course: CourseApiModel, + teacher: CanvasUserApiModel + ) = PagesApi.createCoursePage( + courseId = course.id, + published = true, + frontPage = false, + token = teacher.token + ) + + private fun createQuiz( + course: CourseApiModel, + teacher: CanvasUserApiModel + ) = QuizzesApi.createQuiz( + QuizzesApi.CreateQuizRequest( + courseId = course.id, + withDescription = true, + dueAt = 3.days.fromNow.iso8601, + token = teacher.token, + published = true + ) + ) + + private fun createAssignment( + course: CourseApiModel, + withDescription: Boolean, + teacher: CanvasUserApiModel, + dueAt: String + ) = AssignmentsApi.createAssignment( + AssignmentsApi.CreateAssignmentRequest( + courseId = course.id, + withDescription = withDescription, + submissionTypes = listOf(SubmissionType.ONLINE_TEXT_ENTRY), + teacherToken = teacher.token, + dueAt = dueAt + ) + ) } \ No newline at end of file diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/NotificationsE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/NotificationsE2ETest.kt index da37dff858..bdeaec2ebf 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/NotificationsE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/NotificationsE2ETest.kt @@ -22,6 +22,9 @@ import com.instructure.canvas.espresso.refresh import com.instructure.dataseeding.api.AssignmentsApi import com.instructure.dataseeding.api.QuizzesApi import com.instructure.dataseeding.api.SubmissionsApi +import com.instructure.dataseeding.model.AssignmentApiModel +import com.instructure.dataseeding.model.CanvasUserApiModel +import com.instructure.dataseeding.model.CourseApiModel import com.instructure.dataseeding.model.GradingType import com.instructure.dataseeding.model.QuizAnswer import com.instructure.dataseeding.model.QuizQuestion @@ -58,40 +61,10 @@ class NotificationsE2ETest : StudentTest() { val course = data.coursesList[0] Log.d(PREPARATION_TAG,"Seed an assignment for ${course.name} course.") - val testAssignment = AssignmentsApi.createAssignment( - AssignmentsApi.CreateAssignmentRequest( - courseId = course.id, - submissionTypes = listOf(SubmissionType.ONLINE_TEXT_ENTRY), - gradingType = GradingType.POINTS, - teacherToken = teacher.token, - pointsPossible = 15.0, - dueAt = 1.days.fromNow.iso8601 - ) - ) + val testAssignment = createAssignment(course, teacher) Log.d(PREPARATION_TAG,"Seed a quiz for ${course.name} course with some questions.") - val quizQuestions = listOf( - QuizQuestion( - questionText = "What's your favorite color?", - questionType = "multiple_choice_question", - pointsPossible = 5, - answers = listOf( - QuizAnswer(id = 1, weight = 0, text = "Red"), - QuizAnswer(id = 1, weight = 1, text = "Blue"), - QuizAnswer(id = 1, weight = 0, text = "Yellow") - ) - ), - QuizQuestion( - questionText = "Who let the dogs out?", - questionType = "multiple_choice_question", - pointsPossible = 5, - answers = listOf( - QuizAnswer(id = 1, weight = 1, text = "Who Who Who-Who"), - QuizAnswer(id = 1, weight = 0, text = "Who Who-Who-Who"), - QuizAnswer(id = 1, weight = 0, text = "Who-Who Who-Who") - ) - ) - ) + val quizQuestions = makeQuizQuestions() Log.d(PREPARATION_TAG,"Create and publish a quiz with the previously seeded questions.") QuizzesApi.createAndPublishQuiz(course.id, teacher.token, quizQuestions) @@ -112,6 +85,7 @@ class NotificationsE2ETest : StudentTest() { break } catch (e: java.lang.AssertionError) { try { + sleep(3000) //Wait for the notifications to be displayed (API is slow sometimes, it might take some time) refresh() notificationPage.assertNotificationCountIsGreaterThan(0) //At least one notification is displayed. notificationApiResponseAttempt++ @@ -130,15 +104,23 @@ class NotificationsE2ETest : StudentTest() { } Log.d(PREPARATION_TAG,"Submit ${testAssignment.name} assignment with student: ${student.name}.") - SubmissionsApi.submitCourseAssignment( - submissionType = SubmissionType.ONLINE_TEXT_ENTRY, - courseId = course.id, - assignmentId = testAssignment.id, - studentToken = student.token, - fileIds = emptyList().toMutableList() - ) + submitAssignment(course, testAssignment, student) Log.d(PREPARATION_TAG,"Grade the submission of ${student.name} student for assignment: ${testAssignment.name}.") + gradeSubmission(teacher, course, testAssignment, student) + + Log.d(STEP_TAG,"Refresh the Notifications Page. Assert that there is a notification about the submission grading appearing.") + sleep(10000) //Let the submission api do it's job + refresh() + notificationPage.assertHasGrade(testAssignment.name,"13") + } + + private fun gradeSubmission( + teacher: CanvasUserApiModel, + course: CourseApiModel, + testAssignment: AssignmentApiModel, + student: CanvasUserApiModel + ) { SubmissionsApi.gradeSubmission( teacherToken = teacher.token, courseId = course.id, @@ -147,11 +129,59 @@ class NotificationsE2ETest : StudentTest() { postedGrade = "13", excused = false ) + } + + private fun submitAssignment( + course: CourseApiModel, + testAssignment: AssignmentApiModel, + student: CanvasUserApiModel + ) { + SubmissionsApi.submitCourseAssignment( + submissionType = SubmissionType.ONLINE_TEXT_ENTRY, + courseId = course.id, + assignmentId = testAssignment.id, + studentToken = student.token, + fileIds = emptyList().toMutableList() + ) + } - Log.d(STEP_TAG,"Refresh the Notifications Page. Assert that there is a notification about the submission grading appearing.") - sleep(5000) //Let the submission api do it's job - refresh() - notificationPage.assertHasGrade(testAssignment.name,"13") + private fun makeQuizQuestions() = listOf( + QuizQuestion( + questionText = "What's your favorite color?", + questionType = "multiple_choice_question", + pointsPossible = 5, + answers = listOf( + QuizAnswer(id = 1, weight = 0, text = "Red"), + QuizAnswer(id = 1, weight = 1, text = "Blue"), + QuizAnswer(id = 1, weight = 0, text = "Yellow") + ) + ), + QuizQuestion( + questionText = "Who let the dogs out?", + questionType = "multiple_choice_question", + pointsPossible = 5, + answers = listOf( + QuizAnswer(id = 1, weight = 1, text = "Who Who Who-Who"), + QuizAnswer(id = 1, weight = 0, text = "Who Who-Who-Who"), + QuizAnswer(id = 1, weight = 0, text = "Who-Who Who-Who") + ) + ) + ) + + private fun createAssignment( + course: CourseApiModel, + teacher: CanvasUserApiModel + ) : AssignmentApiModel { + return AssignmentsApi.createAssignment( + AssignmentsApi.CreateAssignmentRequest( + courseId = course.id, + submissionTypes = listOf(SubmissionType.ONLINE_TEXT_ENTRY), + gradingType = GradingType.POINTS, + teacherToken = teacher.token, + pointsPossible = 15.0, + dueAt = 1.days.fromNow.iso8601 + ) + ) } } \ No newline at end of file diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/PagesE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/PagesE2ETest.kt index 203a32fd48..44c0552ddf 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/PagesE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/PagesE2ETest.kt @@ -21,6 +21,9 @@ import androidx.test.espresso.Espresso import androidx.test.espresso.web.webdriver.Locator import com.instructure.canvas.espresso.E2E import com.instructure.dataseeding.api.PagesApi +import com.instructure.dataseeding.model.CanvasUserApiModel +import com.instructure.dataseeding.model.CourseApiModel +import com.instructure.dataseeding.util.Randomizer import com.instructure.panda_annotations.FeatureCategory import com.instructure.panda_annotations.Priority import com.instructure.panda_annotations.TestCategory @@ -34,13 +37,9 @@ import org.junit.Test @HiltAndroidTest class PagesE2ETest: StudentTest() { - override fun displaysPageObjects() { - TODO("not implemented") //To change body of created functions use File | Settings | File Templates. - } + override fun displaysPageObjects() = Unit - override fun enableAndConfigureAccessibilityChecks() { - //We don't want to see accessibility errors on E2E tests - } + override fun enableAndConfigureAccessibilityChecks() = Unit @E2E @Test @@ -54,30 +53,13 @@ class PagesE2ETest: StudentTest() { val course = data.coursesList[0] Log.d(PREPARATION_TAG,"Seed an UNPUBLISHED page for ${course.name} course.") - val pageUnpublished = PagesApi.createCoursePage( - courseId = course.id, - published = false, - frontPage = false, - token = teacher.token - ) + val pageUnpublished = createCoursePage(course, teacher, published = false, frontPage = false) Log.d(PREPARATION_TAG,"Seed a PUBLISHED page for ${course.name} course.") - val pagePublished = PagesApi.createCoursePage( - courseId = course.id, - published = true, - frontPage = false, - token = teacher.token, - body = "

Regular Page Text

" - ) + val pagePublished = createCoursePage(course, teacher, published = true, frontPage = false, body = "

Regular Page Text

") Log.d(PREPARATION_TAG,"Seed a PUBLISHED, FRONT page for ${course.name} course.") - val pagePublishedFront = PagesApi.createCoursePage( - courseId = course.id, - published = true, - frontPage = true, - token = teacher.token, - body = "

Front Page Text

" - ) + val pagePublishedFront = createCoursePage(course, teacher, published = true, frontPage = true, body = "

Front Page Text

") Log.d(STEP_TAG,"Login with user: ${student.name}, login id: ${student.loginId}.") tokenLogin(student) @@ -98,18 +80,28 @@ class PagesE2ETest: StudentTest() { Log.d(STEP_TAG,"Open ${pagePublishedFront.title} page. Assert that it is really a front (published) page via web view assertions.") pageListPage.selectFrontPage(pagePublishedFront) - canvasWebViewPage.runTextChecks( - WebViewTextCheck(Locator.ID, "header1", "Front Page Text") - ) + canvasWebViewPage.runTextChecks(WebViewTextCheck(Locator.ID, "header1", "Front Page Text")) Log.d(STEP_TAG,"Navigate back to Pages page.") Espresso.pressBack() Log.d(STEP_TAG,"Open ${pagePublished.title} page. Assert that it is really a regular published page via web view assertions.") pageListPage.selectRegularPage(pagePublished) - canvasWebViewPage.runTextChecks( - WebViewTextCheck(Locator.ID, "header1", "Regular Page Text") - ) + canvasWebViewPage.runTextChecks(WebViewTextCheck(Locator.ID, "header1", "Regular Page Text")) } + + private fun createCoursePage( + course: CourseApiModel, + teacher: CanvasUserApiModel, + published: Boolean, + frontPage: Boolean, + body: String = Randomizer.randomPageBody() + ) = PagesApi.createCoursePage( + courseId = course.id, + published = published, + frontPage = frontPage, + token = teacher.token, + body = body + ) } \ No newline at end of file diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/PeopleE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/PeopleE2ETest.kt index b88df73e7a..f5900bdbbf 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/PeopleE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/PeopleE2ETest.kt @@ -29,18 +29,11 @@ import com.instructure.student.ui.utils.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test -private const val STEP_TAG = "PeopleE2ETest #STEP# " -private const val PREPARATION_TAG = "PeopleE2ETest #PREPARATION# " - @HiltAndroidTest class PeopleE2ETest : StudentTest() { - override fun displaysPageObjects() { - TODO("not implemented") //To change body of created functions use File | Settings | File Templates. - } + override fun displaysPageObjects() = Unit - override fun enableAndConfigureAccessibilityChecks() { - //We don't want to see accessibility errors on E2E tests - } + override fun enableAndConfigureAccessibilityChecks() = Unit @E2E @Test diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/QuizzesE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/QuizzesE2ETest.kt index be955c6ebb..472584de69 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/QuizzesE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/QuizzesE2ETest.kt @@ -33,6 +33,8 @@ import com.instructure.canvas.espresso.containsTextCaseInsensitive import com.instructure.canvas.espresso.isElementDisplayed import com.instructure.dataseeding.api.QuizzesApi import com.instructure.dataseeding.api.QuizzesApi.createAndPublishQuiz +import com.instructure.dataseeding.model.CanvasUserApiModel +import com.instructure.dataseeding.model.CourseApiModel import com.instructure.dataseeding.model.QuizAnswer import com.instructure.dataseeding.model.QuizQuestion import com.instructure.panda_annotations.FeatureCategory @@ -51,13 +53,9 @@ import org.junit.Test @HiltAndroidTest class QuizzesE2ETest: StudentTest() { - override fun displaysPageObjects() { - TODO("not implemented") //To change body of created functions use File | Settings | File Templates. - } + override fun displaysPageObjects() = Unit - override fun enableAndConfigureAccessibilityChecks() { - //We don't want to see accessibility errors on E2E tests - } + override fun enableAndConfigureAccessibilityChecks() = Unit // Fairly basic test of webview-based quizzes. Seeds/takes a quiz with two multiple-choice // questions. @@ -77,49 +75,14 @@ class QuizzesE2ETest: StudentTest() { val course = data.coursesList[0] Log.d(PREPARATION_TAG,"Seed a quiz for ${course.name} course.") - val quizUnpublished = QuizzesApi.createQuiz(QuizzesApi.CreateQuizRequest( - courseId = course.id, - withDescription = true, - published = false, - token = teacher.token - )) + val quizUnpublished = createQuiz(course, teacher, withDescription = true, published = false) Log.d(PREPARATION_TAG,"Seed another quiz for ${course.name} with some questions.") - val quizQuestions = listOf( - QuizQuestion( - questionText = "What's your favorite color?", - questionType = "multiple_choice_question", - pointsPossible = 5, - answers = listOf( - QuizAnswer(id=1, weight=0, text="Red"), - QuizAnswer(id=1, weight=1, text="Blue"), - QuizAnswer(id=1, weight=0, text="Yellow") - ) - ), - QuizQuestion( - questionText = "Who let the dogs out?", - questionType = "multiple_choice_question", - pointsPossible = 5, - answers = listOf( - QuizAnswer(id=1, weight=1, text="Who Who Who-Who"), - QuizAnswer(id=1, weight=0, text="Who Who-Who-Who"), - QuizAnswer(id=1, weight=0, text="Who-Who Who-Who") - ) - ) - - // Can't test essay questions yet. More specifically, can't test answering essay questions. -// QuizQuestion( -// questionText = "Why should I give you an A?", -// questionType = "essay_question", -// pointsPossible = 12, -// answers = listOf() -// ) - ) + val quizQuestions = makeQuizQuestions() Log.d(PREPARATION_TAG,"Publish the previously seeded quiz.") val quizPublished = createAndPublishQuiz(course.id, teacher.token, quizQuestions) - Log.d(STEP_TAG, "Login with user: ${student.name}, login id: ${student.loginId}.") tokenLogin(student) dashboardPage.waitForRender() @@ -134,9 +97,7 @@ class QuizzesE2ETest: StudentTest() { Log.d(STEP_TAG,"Select ${quizPublished.title} quiz. Assert that the ${quizPublished.title} quiz title is displayed.") quizListPage.selectQuiz(quizPublished) - canvasWebViewPage.runTextChecks( - WebViewTextCheck(locatorType = Locator.ID, locatorValue = "quiz_title", textValue = quizPublished.title) - ) + canvasWebViewPage.runTextChecks(WebViewTextCheck(locatorType = Locator.ID, locatorValue = "quiz_title", textValue = quizPublished.title)) // Launch the quiz // Pressing the "Take the Quiz" button does not work on an FTL Api 25 device. @@ -242,4 +203,49 @@ class QuizzesE2ETest: StudentTest() { courseGradesPage.assertGradeDisplayed(withText(quizPublished.title), containsTextCaseInsensitive("10")) } + + private fun createQuiz( + course: CourseApiModel, + teacher: CanvasUserApiModel, + withDescription: Boolean, + published: Boolean, + ) = QuizzesApi.createQuiz( + QuizzesApi.CreateQuizRequest( + courseId = course.id, + withDescription = withDescription, + published = published, + token = teacher.token + ) + ) + + private fun makeQuizQuestions() = listOf( + QuizQuestion( + questionText = "What's your favorite color?", + questionType = "multiple_choice_question", + pointsPossible = 5, + answers = listOf( + QuizAnswer(id = 1, weight = 0, text = "Red"), + QuizAnswer(id = 1, weight = 1, text = "Blue"), + QuizAnswer(id = 1, weight = 0, text = "Yellow") + ) + ), + QuizQuestion( + questionText = "Who let the dogs out?", + questionType = "multiple_choice_question", + pointsPossible = 5, + answers = listOf( + QuizAnswer(id = 1, weight = 1, text = "Who Who Who-Who"), + QuizAnswer(id = 1, weight = 0, text = "Who Who-Who-Who"), + QuizAnswer(id = 1, weight = 0, text = "Who-Who Who-Who") + ) + ) + + // Can't test essay questions yet. More specifically, can't test answering essay questions. + // QuizQuestion( + // questionText = "Why should I give you an A?", + // questionType = "essay_question", + // pointsPossible = 12, + // answers = listOf() + // ) + ) } \ No newline at end of file diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/SettingsE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/SettingsE2ETest.kt index 95421a23f0..cb42b97277 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/SettingsE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/SettingsE2ETest.kt @@ -103,7 +103,6 @@ class SettingsE2ETest : StudentTest() { val newSavedPandaAvatarCount = getSavedPandaAvatarCount() Log.d(STEP_TAG, "Assert that saved panda avatar count has increased by one. Old value: $originalSavedPandaAvatarCount, new value: $newSavedPandaAvatarCount.") Assert.assertTrue(newSavedPandaAvatarCount == originalSavedPandaAvatarCount + 1) - } @E2E @@ -207,9 +206,11 @@ class SettingsE2ETest : StudentTest() { Log.d(STEP_TAG,"Check that e-mail is equal to: ${student.loginId} (student's Login ID).") aboutPage.emailIs(student.loginId) + Log.d(STEP_TAG,"Assert that the Instructure company logo has been displayed on the About page.") + aboutPage.assertInstructureLogoDisplayed() } - //The remote config settings page only available on debug builds. + //The remote config settings page only available on DEBUG/DEV builds. So this test is testing a non user facing feature. @E2E @Test @TestMetaData(Priority.NICE_TO_HAVE, FeatureCategory.SETTINGS, TestCategory.E2E) @@ -257,7 +258,6 @@ class SettingsE2ETest : StudentTest() { RemoteConfigParam.values().forEach { param -> remoteConfigSettingsPage.verifyRemoteConfigParamValue(param, initialValues.get(param.rc_name)!!) } - } @E2E diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/ShareExtensionE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/ShareExtensionE2ETest.kt index 9ee9901944..823a59ecde 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/ShareExtensionE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/ShareExtensionE2ETest.kt @@ -25,8 +25,7 @@ import androidx.test.uiautomator.UiDevice import androidx.test.uiautomator.UiSelector import com.instructure.canvas.espresso.E2E import com.instructure.dataseeding.api.AssignmentsApi -import com.instructure.dataseeding.model.GradingType -import com.instructure.dataseeding.model.SubmissionType +import com.instructure.dataseeding.model.* import com.instructure.dataseeding.util.days import com.instructure.dataseeding.util.fromNow import com.instructure.dataseeding.util.iso8601 @@ -43,6 +42,7 @@ import java.io.File class ShareExtensionE2ETest: StudentTest() { override fun displaysPageObjects() = Unit + override fun enableAndConfigureAccessibilityChecks() = Unit @E2E @@ -60,25 +60,10 @@ class ShareExtensionE2ETest: StudentTest() { val teacher = data.teachersList[0] Log.d(PREPARATION_TAG,"Seeding 'Text Entry' assignment for ${course.name} course.") - val testAssignmentOne = AssignmentsApi.createAssignment( - AssignmentsApi.CreateAssignmentRequest( - courseId = course.id, - submissionTypes = listOf(SubmissionType.ONLINE_UPLOAD), - gradingType = GradingType.POINTS, - teacherToken = teacher.token, - pointsPossible = 15.0, - dueAt = 1.days.fromNow.iso8601 - )) - - AssignmentsApi.createAssignment( - AssignmentsApi.CreateAssignmentRequest( - courseId = course.id, - submissionTypes = listOf(SubmissionType.ONLINE_UPLOAD), - gradingType = GradingType.POINTS, - teacherToken = teacher.token, - pointsPossible = 30.0, - dueAt = 1.days.fromNow.iso8601 - )) + val testAssignmentOne = createAssignment(course, teacher, 1.days.fromNow.iso8601, 15.0) + + Log.d(PREPARATION_TAG,"Seeding another 'Text Entry' assignment for ${course.name} course.") + createAssignment(course, teacher, 1.days.fromNow.iso8601, 30.0) Log.d(PREPARATION_TAG, "Get the device to be able to perform app-independent actions on it.") val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) @@ -199,7 +184,24 @@ class ShareExtensionE2ETest: StudentTest() { fileListPage.assertItemDisplayed("unfiled") fileListPage.selectItem("unfiled") fileListPage.assertItemDisplayed(jpgTestFileName) + } + private fun createAssignment( + course: CourseApiModel, + teacher: CanvasUserApiModel, + dueAt: String, + pointsPossible: Double + ): AssignmentApiModel { + return AssignmentsApi.createAssignment( + AssignmentsApi.CreateAssignmentRequest( + courseId = course.id, + submissionTypes = listOf(SubmissionType.ONLINE_UPLOAD), + gradingType = GradingType.POINTS, + teacherToken = teacher.token, + pointsPossible = pointsPossible, + dueAt = dueAt + ) + ) } private fun setupFileOnDevice(fileName: String): Uri { diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/SyllabusE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/SyllabusE2ETest.kt index 1df4dbb0b1..524f3a9c29 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/SyllabusE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/SyllabusE2ETest.kt @@ -20,6 +20,8 @@ import android.util.Log import com.instructure.canvas.espresso.E2E import com.instructure.dataseeding.api.AssignmentsApi import com.instructure.dataseeding.api.QuizzesApi +import com.instructure.dataseeding.model.CanvasUserApiModel +import com.instructure.dataseeding.model.CourseApiModel import com.instructure.dataseeding.model.SubmissionType import com.instructure.dataseeding.util.days import com.instructure.dataseeding.util.fromNow @@ -36,13 +38,9 @@ import org.junit.Test @HiltAndroidTest class SyllabusE2ETest: StudentTest() { - override fun displaysPageObjects() { - TODO("not implemented") //To change body of created functions use File | Settings | File Templates. - } + override fun displaysPageObjects() = Unit - override fun enableAndConfigureAccessibilityChecks() { - //We don't want to see accessibility errors on E2E tests - } + override fun enableAndConfigureAccessibilityChecks() = Unit @E2E @Test @@ -67,22 +65,10 @@ class SyllabusE2ETest: StudentTest() { syllabusPage.assertEmptyView() Log.d(PREPARATION_TAG,"Seed an assignment for ${course.name} course.") - val assignment = AssignmentsApi.createAssignment(AssignmentsApi.CreateAssignmentRequest( - courseId = course.id, - teacherToken = teacher.token, - submissionTypes = listOf(SubmissionType.ON_PAPER), - dueAt = 1.days.fromNow.iso8601, - withDescription = true - )) + val assignment = createAssignment(course, teacher) Log.d(PREPARATION_TAG,"Seed a quiz for ${course.name} course.") - val quiz = QuizzesApi.createQuiz(QuizzesApi.CreateQuizRequest( - courseId = course.id, - withDescription = true, - published = true, - token = teacher.token, - dueAt = 2.days.fromNow.iso8601 - )) + val quiz = createQuiz(course, teacher) // TODO: Seed a generic calendar event @@ -91,4 +77,30 @@ class SyllabusE2ETest: StudentTest() { syllabusPage.assertItemDisplayed(assignment.name) syllabusPage.assertItemDisplayed(quiz.title) } + + private fun createQuiz( + course: CourseApiModel, + teacher: CanvasUserApiModel + ) = QuizzesApi.createQuiz( + QuizzesApi.CreateQuizRequest( + courseId = course.id, + withDescription = true, + published = true, + token = teacher.token, + dueAt = 2.days.fromNow.iso8601 + ) + ) + + private fun createAssignment( + course: CourseApiModel, + teacher: CanvasUserApiModel + ) = AssignmentsApi.createAssignment( + AssignmentsApi.CreateAssignmentRequest( + courseId = course.id, + teacherToken = teacher.token, + submissionTypes = listOf(SubmissionType.ON_PAPER), + dueAt = 1.days.fromNow.iso8601, + withDescription = true + ) + ) } \ No newline at end of file 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 7c68c774d6..58054b12c7 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 @@ -7,8 +7,7 @@ import com.instructure.canvas.espresso.refresh import com.instructure.dataseeding.api.AssignmentsApi import com.instructure.dataseeding.api.QuizzesApi import com.instructure.dataseeding.api.SubmissionsApi -import com.instructure.dataseeding.model.GradingType -import com.instructure.dataseeding.model.SubmissionType +import com.instructure.dataseeding.model.* import com.instructure.dataseeding.util.days import com.instructure.dataseeding.util.fromNow import com.instructure.dataseeding.util.iso8601 @@ -43,15 +42,7 @@ class TodoE2ETest: StudentTest() { val favoriteCourse = data.coursesList[1] Log.d(PREPARATION_TAG,"Seed an assignment for ${course.name} course with tomorrow due date.") - val testAssignment = AssignmentsApi.createAssignment( - AssignmentsApi.CreateAssignmentRequest( - courseId = course.id, - submissionTypes = listOf(SubmissionType.ONLINE_TEXT_ENTRY), - gradingType = GradingType.POINTS, - teacherToken = teacher.token, - pointsPossible = 15.0, - dueAt = 1.days.fromNow.iso8601 - )) + val testAssignment = createAssignment(course, teacher) Log.d(PREPARATION_TAG,"Seed another assignment for ${course.name} course with 7 days from now due date.") val seededAssignments2 = seedAssignments( @@ -63,24 +54,10 @@ class TodoE2ETest: StudentTest() { val borderDateAssignment = seededAssignments2[0] //We show items in the to do section which are within 7 days. Log.d(PREPARATION_TAG,"Seed a quiz for ${course.name} course with tomorrow due date.") - val quiz = QuizzesApi.createQuiz( - QuizzesApi.CreateQuizRequest( - courseId = course.id, - withDescription = true, - published = true, - token = teacher.token, - dueAt = 1.days.fromNow.iso8601) - ) + val quiz = createQuiz(course, teacher, 1.days.fromNow.iso8601) Log.d(PREPARATION_TAG,"Seed another quiz for ${course.name} course with 8 days from now due date..") - val tooFarAwayQuiz = QuizzesApi.createQuiz( - QuizzesApi.CreateQuizRequest( - courseId = course.id, - withDescription = true, - published = true, - token = teacher.token, - dueAt = 8.days.fromNow.iso8601) - ) + val tooFarAwayQuiz = createQuiz(course, teacher, 8.days.fromNow.iso8601) Log.d(STEP_TAG, "Login with user: ${student.name}, login id: ${student.loginId}.") tokenLogin(student) @@ -133,15 +110,7 @@ class TodoE2ETest: StudentTest() { todoPage.assertQuizNotDisplayed(tooFarAwayQuiz) Log.d(PREPARATION_TAG,"Seed an assignment for ${favoriteCourse.name} course with tomorrow due date.") - val favoriteCourseAssignment = AssignmentsApi.createAssignment( - AssignmentsApi.CreateAssignmentRequest( - courseId = favoriteCourse.id, - submissionTypes = listOf(SubmissionType.ONLINE_TEXT_ENTRY), - gradingType = GradingType.POINTS, - teacherToken = teacher.token, - pointsPossible = 15.0, - dueAt = 1.days.fromNow.iso8601 - )) + val favoriteCourseAssignment = createAssignment(favoriteCourse, teacher) Log.d(STEP_TAG, "Navigate back to the Dashboard Page. Open ${favoriteCourse.name} course. Mark it as favorite.") Espresso.pressBack() @@ -165,6 +134,35 @@ class TodoE2ETest: StudentTest() { todoPage.assertAssignmentNotDisplayed(borderDateAssignment) todoPage.assertQuizNotDisplayed(quiz) todoPage.assertQuizNotDisplayed(tooFarAwayQuiz) + } + private fun createQuiz( + course: CourseApiModel, + teacher: CanvasUserApiModel, + dueAt: String + ) = QuizzesApi.createQuiz( + QuizzesApi.CreateQuizRequest( + courseId = course.id, + withDescription = true, + published = true, + token = teacher.token, + dueAt = dueAt + ) + ) + + private fun createAssignment( + course: CourseApiModel, + teacher: CanvasUserApiModel + ): AssignmentApiModel { + return AssignmentsApi.createAssignment( + AssignmentsApi.CreateAssignmentRequest( + courseId = course.id, + submissionTypes = listOf(SubmissionType.ONLINE_TEXT_ENTRY), + gradingType = GradingType.POINTS, + teacherToken = teacher.token, + pointsPossible = 15.0, + dueAt = 1.days.fromNow.iso8601 + ) + ) } } \ No newline at end of file diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/k5/GradesElementaryE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/k5/GradesElementaryE2ETest.kt index a216e2b755..4b675a8b75 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/k5/GradesElementaryE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/k5/GradesElementaryE2ETest.kt @@ -23,6 +23,8 @@ import com.instructure.canvasapi2.utils.toApiString import com.instructure.dataseeding.api.AssignmentsApi import com.instructure.dataseeding.api.GradingPeriodsApi import com.instructure.dataseeding.api.SubmissionsApi +import com.instructure.dataseeding.model.AssignmentApiModel +import com.instructure.dataseeding.model.CanvasUserApiModel import com.instructure.dataseeding.model.GradingType import com.instructure.dataseeding.model.SubmissionType import com.instructure.espresso.page.getStringFromResource @@ -66,47 +68,17 @@ class GradesElementaryE2ETest : StudentTest() { val calendar = Calendar.getInstance(TimeZone.getTimeZone("UTC")) val testGradingPeriodListApiModel = GradingPeriodsApi.getGradingPeriodsOfCourse(nonHomeroomCourses[0].id) - Log.d(PREPARATION_TAG,"Seeding 'Text Entry' assignment for ${nonHomeroomCourses[0].name} course.") - val testAssignment = AssignmentsApi.createAssignment( - AssignmentsApi.CreateAssignmentRequest( - courseId = nonHomeroomCourses[1].id, - submissionTypes = listOf(SubmissionType.ONLINE_TEXT_ENTRY), - gradingType = GradingType.PERCENT, - teacherToken = teacher.token, - pointsPossible = 100.0, - dueAt = calendar.time.toApiString() - ) - ) + Log.d(PREPARATION_TAG,"Seeding 'Text Entry' assignment for ${nonHomeroomCourses[1].name} course.") + val testAssignment = createAssignment(nonHomeroomCourses[1].id, teacher, calendar, GradingType.PERCENT, 100.0) Log.d(PREPARATION_TAG,"Seeding another 'Text Entry' assignment for ${nonHomeroomCourses[0].name} course.") - val testAssignment2 = AssignmentsApi.createAssignment( - AssignmentsApi.CreateAssignmentRequest( - courseId = nonHomeroomCourses[0].id, - submissionTypes = listOf(SubmissionType.ONLINE_TEXT_ENTRY), - gradingType = GradingType.LETTER_GRADE, - teacherToken = teacher.token, - pointsPossible = 100.0, - dueAt = calendar.time.toApiString() - ) - ) + val testAssignment2 = createAssignment(nonHomeroomCourses[0].id, teacher, calendar, GradingType.LETTER_GRADE, 100.0) Log.d(PREPARATION_TAG,"Grade the previously seeded submission for ${nonHomeroomCourses[1].name} assignment.") - SubmissionsApi.gradeSubmission( - teacherToken = teacher.token, - courseId = nonHomeroomCourses[1].id, - assignmentId = testAssignment.id, - studentId = student.id, - postedGrade="9", - excused = false) + gradeSubmission(teacher,nonHomeroomCourses[1].id, student, testAssignment.id, "9") Log.d(PREPARATION_TAG,"Grade the previously seeded submission for ${nonHomeroomCourses[0].name} assignment.") - SubmissionsApi.gradeSubmission( - teacherToken = teacher.token, - courseId = nonHomeroomCourses[0].id, - assignmentId = testAssignment2.id, - studentId = student.id, - postedGrade="A-", - excused = false) + gradeSubmission(teacher, nonHomeroomCourses[0].id, student, testAssignment2.id, "A-") Log.d(STEP_TAG,"Login with user: ${student.name}, login id: ${student.loginId}.") tokenLoginElementary(student) @@ -123,13 +95,7 @@ class GradesElementaryE2ETest : StudentTest() { gradesPage.assertCourseShownWithGrades(nonHomeroomCourses[2].name, "Not Graded") Log.d(PREPARATION_TAG,"Grade the previously seeded submission for ${testAssignment2.name} assignment.") - SubmissionsApi.gradeSubmission( - teacherToken = teacher.token, - courseId = nonHomeroomCourses[0].id, - assignmentId = testAssignment2.id, - studentId = student.id, - postedGrade="C-", - excused = false) + gradeSubmission(teacher,nonHomeroomCourses[0].id, student, testAssignment2.id, "C-") Thread.sleep(5000) //This time is needed here to let the SubMissionApi does it's job. @@ -154,5 +120,41 @@ class GradesElementaryE2ETest : StudentTest() { gradesPage.assertPageObjects() } + + private fun createAssignment( + courseId: Long, + teacher: CanvasUserApiModel, + calendar: Calendar, + gradingType: GradingType, + pointsPossible: Double + ): AssignmentApiModel { + return AssignmentsApi.createAssignment( + AssignmentsApi.CreateAssignmentRequest( + courseId = courseId, + submissionTypes = listOf(SubmissionType.ONLINE_TEXT_ENTRY), + gradingType = gradingType, + teacherToken = teacher.token, + pointsPossible = pointsPossible, + dueAt = calendar.time.toApiString() + ) + ) + } + + private fun gradeSubmission( + teacher: CanvasUserApiModel, + courseId: Long, + student: CanvasUserApiModel, + assignmentId: Long, + postedGrade: String + ) { + SubmissionsApi.gradeSubmission( + teacherToken = teacher.token, + courseId = courseId, + assignmentId = assignmentId, + studentId = student.id, + postedGrade = postedGrade, + excused = false + ) + } } diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/k5/HomeroomE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/k5/HomeroomE2ETest.kt index 5581585111..6088a3f2f7 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/k5/HomeroomE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/k5/HomeroomE2ETest.kt @@ -20,6 +20,8 @@ import android.util.Log import androidx.test.espresso.Espresso import com.instructure.canvas.espresso.E2E import com.instructure.dataseeding.api.AssignmentsApi +import com.instructure.dataseeding.model.AssignmentApiModel +import com.instructure.dataseeding.model.CanvasUserApiModel import com.instructure.dataseeding.model.GradingType import com.instructure.dataseeding.model.SubmissionType import com.instructure.dataseeding.util.ago @@ -67,27 +69,10 @@ class HomeroomE2ETest : StudentTest() { val nonHomeroomCourses = data.coursesList.filter { !it.homeroomCourse } Log.d(PREPARATION_TAG,"Seeding 'Text Entry' assignment for ${nonHomeroomCourses[2].name} course.") - val testAssignment = AssignmentsApi.createAssignment( - AssignmentsApi.CreateAssignmentRequest( - courseId = nonHomeroomCourses[2].id, - submissionTypes = listOf(SubmissionType.ONLINE_TEXT_ENTRY), - gradingType = GradingType.LETTER_GRADE, - teacherToken = teacher.token, - pointsPossible = 100.0, - dueAt = OffsetDateTime.now().plusHours(1).format(DateTimeFormatter.ISO_DATE_TIME) - ) - ) + val testAssignment = createAssignment(nonHomeroomCourses[2].id, teacher, GradingType.LETTER_GRADE, 100.0, OffsetDateTime.now().plusHours(1).format(DateTimeFormatter.ISO_DATE_TIME)) Log.d(PREPARATION_TAG,"Seeding 'Text Entry' MISSING assignment for ${nonHomeroomCourses[2].name} course.") - val testAssignmentMissing = AssignmentsApi.createAssignment( - AssignmentsApi.CreateAssignmentRequest( - courseId = nonHomeroomCourses[2].id, - submissionTypes = listOf(SubmissionType.ONLINE_TEXT_ENTRY), - gradingType = GradingType.PERCENT, - teacherToken = teacher.token, - pointsPossible = 100.0, - dueAt = 3.days.ago.iso8601 - )) + val testAssignmentMissing = createAssignment(nonHomeroomCourses[2].id, teacher, GradingType.PERCENT, 100.0, 3.days.ago.iso8601) Log.d(STEP_TAG,"Login with user: ${student.name}, login id: ${student.loginId}.") tokenLoginElementary(student) @@ -95,20 +80,15 @@ class HomeroomE2ETest : StudentTest() { Log.d(STEP_TAG, "Navigate to K5 Important Dates Page and assert it is loaded.") elementaryDashboardPage.selectTab(ElementaryDashboardPage.ElementaryTabType.HOMEROOM) - //TODO: Maybe at this point is why this test is flaky. Suggestion: Check if homeroomPage's page object has been loaded, and check if homeroomPage and elementaryDashboardPage is different at all. Log.d(STEP_TAG, "Assert that there is a welcome text with the student's shortname (${student.shortName}).") homeroomPage.assertWelcomeText(student.shortName) Log.d(STEP_TAG, "Assert that the ${homeroomAnnouncement.title} announcement (which belongs to ${homeroomCourse.name} homeroom course) is displayed.") - homeroomPage.assertAnnouncementDisplayed( - homeroomCourse.name, - homeroomAnnouncement.title, - homeroomAnnouncement.message - ) + homeroomPage.assertAnnouncementDisplayed(homeroomCourse.name, homeroomAnnouncement.title, homeroomAnnouncement.message) Log.d(STEP_TAG, "Assert that under the 'My Subject' section there are 3 items.") - homeroomPage.assertCourseItemsCount(3) //gives back the number of courses under 'My Subject' list + homeroomPage.assertCourseItemsCount(3) Log.d(STEP_TAG, "Click on 'View Previous Announcements'." + "Assert that the Announcement List Page is displayed" + @@ -120,7 +100,7 @@ class HomeroomE2ETest : StudentTest() { Log.d(STEP_TAG, "Navigate back to Homeroom Page and assert it is displayed well.") Espresso.pressBack() homeroomPage.assertPageObjects() - elementaryDashboardPage.waitForRender() //TODO: might not need this here. + elementaryDashboardPage.waitForRender() for (i in 0 until nonHomeroomCourses.size - 1) { Log.d(STEP_TAG, "Assert that the ${nonHomeroomCourses[i].name} course is displayed with the announcements which belongs to it.") @@ -160,5 +140,25 @@ class HomeroomE2ETest : StudentTest() { assignmentListPage.assertHasAssignment(testAssignment) assignmentListPage.assertHasAssignment(testAssignmentMissing) } + + + private fun createAssignment( + courseId: Long, + teacher: CanvasUserApiModel, + gradingType: GradingType, + pointsPossible: Double, + dueAt: String + ): AssignmentApiModel { + return AssignmentsApi.createAssignment( + AssignmentsApi.CreateAssignmentRequest( + courseId = courseId, + submissionTypes = listOf(SubmissionType.ONLINE_TEXT_ENTRY), + gradingType = gradingType, + teacherToken = teacher.token, + pointsPossible = pointsPossible, + dueAt = dueAt + ) + ) + } } diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/k5/ImportantDatesE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/k5/ImportantDatesE2ETest.kt index 680f57b22d..106a18af78 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/k5/ImportantDatesE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/k5/ImportantDatesE2ETest.kt @@ -21,6 +21,8 @@ import androidx.test.espresso.Espresso import com.instructure.canvas.espresso.E2E import com.instructure.canvasapi2.utils.toDate import com.instructure.dataseeding.api.AssignmentsApi +import com.instructure.dataseeding.model.AssignmentApiModel +import com.instructure.dataseeding.model.CanvasUserApiModel import com.instructure.dataseeding.model.GradingType import com.instructure.dataseeding.model.SubmissionType import com.instructure.dataseeding.util.days @@ -65,58 +67,17 @@ class ImportantDatesE2ETest : StudentTest() { val elementaryCourse3 = data.coursesList[2] val elementaryCourse4 = data.coursesList[3] - Log.d(PREPARATION_TAG,"Seeding 'Text Entry' IMPORTANT assignment for ${elementaryCourse1.name} course.") - val testAssignment1 = AssignmentsApi.createAssignment( - AssignmentsApi.CreateAssignmentRequest( - courseId = elementaryCourse1.id, - submissionTypes = listOf(SubmissionType.ONLINE_TEXT_ENTRY), - gradingType = GradingType.POINTS, - teacherToken = teacher.token, - pointsPossible = 100.0, - dueAt = 3.days.fromNow.iso8601, - importantDate = true - ) - ) + val testAssignment1 = createAssignment(elementaryCourse1.id,teacher, GradingType.POINTS, 100.0, 3.days.fromNow.iso8601, importantDate = true) Log.d(PREPARATION_TAG,"Seeding 'Text Entry' IMPORTANT assignment for ${elementaryCourse2.name} course.") - val testAssignment2 = AssignmentsApi.createAssignment( - AssignmentsApi.CreateAssignmentRequest( - courseId = elementaryCourse2.id, - submissionTypes = listOf(SubmissionType.ONLINE_TEXT_ENTRY), - gradingType = GradingType.POINTS, - teacherToken = teacher.token, - pointsPossible = 100.0, - dueAt = 4.days.fromNow.iso8601, - importantDate = true - ) - ) + val testAssignment2 = createAssignment(elementaryCourse2.id,teacher, GradingType.POINTS, 100.0, 4.days.fromNow.iso8601, importantDate = true) Log.d(PREPARATION_TAG,"Seeding 'Text Entry' IMPORTANT assignment for ${elementaryCourse3.name} course.") - val testAssignment3 = AssignmentsApi.createAssignment( - AssignmentsApi.CreateAssignmentRequest( - courseId = elementaryCourse3.id, - submissionTypes = listOf(SubmissionType.ONLINE_TEXT_ENTRY), - gradingType = GradingType.POINTS, - teacherToken = teacher.token, - pointsPossible = 100.0, - dueAt = 4.days.fromNow.iso8601, - importantDate = true - ) - ) + val testAssignment3 = createAssignment(elementaryCourse3.id,teacher, GradingType.POINTS, 100.0, 4.days.fromNow.iso8601, importantDate = true) Log.d(PREPARATION_TAG,"Seeding 'Text Entry' NOT IMPORTANT assignment for ${elementaryCourse4.name} course.") - val testNotImportantAssignment = AssignmentsApi.createAssignment( - AssignmentsApi.CreateAssignmentRequest( - courseId = elementaryCourse4.id, - submissionTypes = listOf(SubmissionType.ONLINE_TEXT_ENTRY), - gradingType = GradingType.POINTS, - teacherToken = teacher.token, - pointsPossible = 100.0, - dueAt = 4.days.fromNow.iso8601, - importantDate = false - ) - ) + val testNotImportantAssignment = createAssignment(elementaryCourse4.id,teacher, GradingType.POINTS, 100.0, 4.days.fromNow.iso8601, importantDate = false) Log.d(STEP_TAG,"Login with user: ${student.name}, login id: ${student.loginId}.") tokenLoginElementary(student) @@ -163,5 +124,26 @@ class ImportantDatesE2ETest : StudentTest() { private fun generateDayString(date: Date?): String { return SimpleDateFormat("EEEE, MMMM dd", Locale.getDefault()).format(date) } + + private fun createAssignment( + courseId: Long, + teacher: CanvasUserApiModel, + gradingType: GradingType, + pointsPossible: Double, + dueAt: String, + importantDate: Boolean + ): AssignmentApiModel { + return AssignmentsApi.createAssignment( + AssignmentsApi.CreateAssignmentRequest( + courseId = courseId, + submissionTypes = listOf(SubmissionType.ONLINE_TEXT_ENTRY), + gradingType = gradingType, + teacherToken = teacher.token, + pointsPossible = pointsPossible, + dueAt = dueAt, + importantDate = importantDate + ) + ) + } } diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/k5/ScheduleE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/k5/ScheduleE2ETest.kt index 016d6864bd..2548685af9 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/k5/ScheduleE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/k5/ScheduleE2ETest.kt @@ -21,6 +21,8 @@ import androidx.test.espresso.Espresso import com.instructure.canvas.espresso.E2E import com.instructure.canvasapi2.utils.toApiString import com.instructure.dataseeding.api.AssignmentsApi +import com.instructure.dataseeding.model.AssignmentApiModel +import com.instructure.dataseeding.model.CanvasUserApiModel import com.instructure.dataseeding.model.GradingType import com.instructure.dataseeding.model.SubmissionType import com.instructure.espresso.page.getStringFromResource @@ -72,40 +74,13 @@ class ScheduleE2ETest : StudentTest() { val twoWeeksAfterCalendar = getCustomDateCalendar(15) Log.d(PREPARATION_TAG,"Seeding 'Text Entry' MISSING assignment for ${nonHomeroomCourses[2].name} course.") - val testMissingAssignment = AssignmentsApi.createAssignment( - AssignmentsApi.CreateAssignmentRequest( - courseId = nonHomeroomCourses[2].id, - submissionTypes = listOf(SubmissionType.ONLINE_TEXT_ENTRY), - gradingType = GradingType.LETTER_GRADE, - teacherToken = teacher.token, - pointsPossible = 100.0, - dueAt = currentDateCalendar.time.toApiString() - ) - ) + val testMissingAssignment = createAssignment(nonHomeroomCourses[2].id, teacher, currentDateCalendar, GradingType.LETTER_GRADE,100.0) Log.d(PREPARATION_TAG,"Seeding 'Text Entry' Two weeks before end date assignment for ${nonHomeroomCourses[1].name} course.") - val testTwoWeeksBeforeAssignment = AssignmentsApi.createAssignment( - AssignmentsApi.CreateAssignmentRequest( - courseId = nonHomeroomCourses[1].id, - submissionTypes = listOf(SubmissionType.ONLINE_TEXT_ENTRY), - gradingType = GradingType.PERCENT, - teacherToken = teacher.token, - pointsPossible = 100.0, - dueAt = twoWeeksBeforeCalendar.time.toApiString() - ) - ) + val testTwoWeeksBeforeAssignment = createAssignment(nonHomeroomCourses[1].id, teacher, twoWeeksBeforeCalendar, GradingType.PERCENT,100.0) Log.d(PREPARATION_TAG,"Seeding 'Text Entry' Two weeks after end date assignment for ${nonHomeroomCourses[0].name} course.") - val testTwoWeeksAfterAssignment = AssignmentsApi.createAssignment( - AssignmentsApi.CreateAssignmentRequest( - courseId = nonHomeroomCourses[0].id, - submissionTypes = listOf(SubmissionType.ONLINE_TEXT_ENTRY), - gradingType = GradingType.POINTS, - teacherToken = teacher.token, - pointsPossible = 25.0, - dueAt = twoWeeksAfterCalendar.time.toApiString() - ) - ) + val testTwoWeeksAfterAssignment = createAssignment(nonHomeroomCourses[0].id, teacher, twoWeeksAfterCalendar, GradingType.POINTS,25.0) Log.d(STEP_TAG,"Login with user: ${student.name}, login id: ${student.loginId}.") tokenLoginElementary(student) @@ -239,5 +214,24 @@ class ScheduleE2ETest : StudentTest() { return cal } + private fun createAssignment( + courseId: Long, + teacher: CanvasUserApiModel, + calendar: Calendar, + gradingType: GradingType, + pointsPossible: Double + ): AssignmentApiModel { + return AssignmentsApi.createAssignment( + AssignmentsApi.CreateAssignmentRequest( + courseId = courseId, + submissionTypes = listOf(SubmissionType.ONLINE_TEXT_ENTRY), + gradingType = gradingType, + teacherToken = teacher.token, + pointsPossible = pointsPossible, + dueAt = calendar.time.toApiString() + ) + ) + } + } diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/InAppUpdateInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/InAppUpdateInteractionTest.kt index 75bd92c7fb..f9d0bd8da6 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/InAppUpdateInteractionTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/InAppUpdateInteractionTest.kt @@ -23,6 +23,7 @@ import androidx.test.uiautomator.By import androidx.test.uiautomator.UiDevice import androidx.test.uiautomator.Until import com.google.android.play.core.appupdate.testing.FakeAppUpdateManager +import com.instructure.canvas.espresso.StubTablet import com.instructure.canvas.espresso.mockCanvas.MockCanvas import com.instructure.canvas.espresso.mockCanvas.init import com.instructure.canvasapi2.utils.toApiString @@ -268,6 +269,7 @@ class InAppUpdateInteractionTest : StudentTest() { } @Test + @StubTablet("Fails on Nexus 7 API level 26, phone version works correctly") fun flexibleUpdateCompletesIfAppRestarts() { with(appUpdateManager) { setUpdateAvailable(400) diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/TodoInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/TodoInteractionTest.kt index ddc7753a77..25eab4948a 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/TodoInteractionTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/TodoInteractionTest.kt @@ -67,7 +67,6 @@ class TodoInteractionTest : StudentTest() { @StubLandscape("Stubbed because on lowres device in landscape mode, the space is too narrow to scroll properly. Will be refactored and running when we changed to non-lowres device on nightly runs.") @TestMetaData(Priority.IMPORTANT, FeatureCategory.TODOS, TestCategory.INTERACTION, false) fun testFilters() { - //TODO: Check and refactor (if necessary) after migrated nightly runs from lowres device to non-lowres one. val data = goToTodos(courseCount = 2, favoriteCourseCount = 1) val favoriteCourse = data.courses.values.first {course -> course.isFavorite} val notFavoriteCourse = data.courses.values.first {course -> !course.isFavorite} diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/AboutPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/AboutPage.kt index af6d2f458b..7218029b17 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/AboutPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/AboutPage.kt @@ -18,17 +18,13 @@ package com.instructure.student.ui.pages import androidx.test.espresso.Espresso.onView import androidx.test.espresso.matcher.ViewMatchers.withId -import androidx.test.espresso.matcher.ViewMatchers.withParent -import com.instructure.canvas.espresso.containsTextCaseInsensitive -import com.instructure.espresso.OnViewWithId import com.instructure.espresso.OnViewWithText import com.instructure.espresso.assertDisplayed -import com.instructure.espresso.click import com.instructure.espresso.page.BasePage import com.instructure.espresso.page.plus import com.instructure.espresso.page.withText +import com.instructure.espresso.scrollTo import com.instructure.student.R -import org.hamcrest.Matchers.allOf class AboutPage : BasePage(R.id.aboutPage) { @@ -47,4 +43,8 @@ class AboutPage : BasePage(R.id.aboutPage) { fun emailIs(email: String) { onView(withId(R.id.email) + withText(email)).assertDisplayed() } + + fun assertInstructureLogoDisplayed() { + onView(withId(R.id.instructureLogo)).scrollTo().assertDisplayed() + } } \ 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 8bc8556ca8..1e2cb4072b 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 @@ -179,7 +179,7 @@ class DashboardPage : BasePage(R.id.dashboardPage) { fun selectCourse(course: CourseApiModel) { assertDisplaysCourse(course) - onView(withText(course.name)).click() + onView(withText(course.name) + withId(R.id.titleTextView)).click() } fun assertAnnouncementShowing(announcement: AccountNotification) { 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 ef23271446..223ceaa4de 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 @@ -32,8 +32,25 @@ import com.instructure.canvas.espresso.waitForMatcherWithRefreshes import com.instructure.canvasapi2.models.Conversation import com.instructure.canvasapi2.models.Course import com.instructure.dataseeding.model.ConversationApiModel -import com.instructure.espresso.* -import com.instructure.espresso.page.* +import com.instructure.espresso.OnViewWithId +import com.instructure.espresso.RecyclerViewItemCountGreaterThanAssertion +import com.instructure.espresso.WaitForViewWithId +import com.instructure.espresso.assertDisplayed +import com.instructure.espresso.assertVisibility +import com.instructure.espresso.click +import com.instructure.espresso.longClick +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.waitForViewWithId +import com.instructure.espresso.page.waitForViewWithText +import com.instructure.espresso.page.withAncestor +import com.instructure.espresso.page.withId +import com.instructure.espresso.page.withText +import com.instructure.espresso.scrollTo +import com.instructure.espresso.swipeLeft +import com.instructure.espresso.swipeRight import com.instructure.student.R import org.hamcrest.Matchers.allOf import org.hamcrest.Matchers.not @@ -167,7 +184,7 @@ class InboxPage : BasePage(R.id.inboxPage) { } fun assertInboxEmpty() { - onView(withId(R.id.emptyInboxView)).assertDisplayed() + waitForView(withId(R.id.emptyInboxView)).assertDisplayed() } fun assertHasConversation() { 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 31d4060756..5de3687694 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 @@ -19,7 +19,11 @@ package com.instructure.student.ui.pages import com.instructure.espresso.OnViewWithId import com.instructure.espresso.TextViewColorAssertion import com.instructure.espresso.click -import com.instructure.espresso.page.* +import com.instructure.espresso.page.BasePage +import com.instructure.espresso.page.onView +import com.instructure.espresso.page.plus +import com.instructure.espresso.page.withParent +import com.instructure.espresso.page.withText import com.instructure.espresso.scrollTo import com.instructure.student.R @@ -39,7 +43,7 @@ class SettingsPage : BasePage(R.id.settingsFragment) { private val appThemeStatus by OnViewWithId(R.id.appThemeStatus) fun openAboutPage() { - aboutLabel.click() + aboutLabel.scrollTo().click() } fun openLegalPage() { diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/TodoPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/TodoPage.kt index 0ba41f40ea..75064e0331 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/TodoPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/TodoPage.kt @@ -27,7 +27,13 @@ import com.instructure.dataseeding.model.QuizApiModel import com.instructure.espresso.OnViewWithId import com.instructure.espresso.assertDisplayed import com.instructure.espresso.click -import com.instructure.espresso.page.* +import com.instructure.espresso.page.BasePage +import com.instructure.espresso.page.onView +import com.instructure.espresso.page.plus +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.student.R import org.hamcrest.Matchers @@ -77,7 +83,7 @@ class TodoPage: BasePage(R.id.todoPage) { fun chooseFavoriteCourseFilter() { onView(withId(R.id.todoListFilter)).click() - onView(withText(R.string.favoritedCoursesLabel)).click() + onView(withText(R.string.favoritedCoursesLabel) + withParent(R.id.select_dialog_listview)).click() onView(allOf(isAssignableFrom(Button::class.java), withText(R.string.ok))).click() } diff --git a/apps/student/src/main/AndroidManifest.xml b/apps/student/src/main/AndroidManifest.xml index 9695136a97..189c333817 100644 --- a/apps/student/src/main/AndroidManifest.xml +++ b/apps/student/src/main/AndroidManifest.xml @@ -39,6 +39,7 @@ + = ArrayDeque() + private val notificationsPermissionContract = registerForActivityResult(ActivityResultContracts.RequestPermission()) {} + override fun contentResId(): Int = R.layout.activity_navigation private val isDrawerOpen: Boolean @@ -272,6 +276,14 @@ class NavigationActivity : BaseRouterActivity(), Navigation, MasqueradingDialog. themeSelector.show(supportFragmentManager, ThemeSelectorBottomSheet::javaClass.name) ThemePrefs.themeSelectionShown = true } + + requestNotificationsPermission() + } + + private fun requestNotificationsPermission() { + if (checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) { + notificationsPermissionContract.launch(Manifest.permission.POST_NOTIFICATIONS) + } } private fun restoreBottomNavState(savedBottomScreens: List?) { diff --git a/apps/student/src/main/java/com/instructure/student/activity/VideoViewActivity.kt b/apps/student/src/main/java/com/instructure/student/activity/VideoViewActivity.kt index e7afb43222..ec2c09f226 100644 --- a/apps/student/src/main/java/com/instructure/student/activity/VideoViewActivity.kt +++ b/apps/student/src/main/java/com/instructure/student/activity/VideoViewActivity.kt @@ -62,7 +62,7 @@ class VideoViewActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setContentView(R.layout.activity_video_view) + setContentView(binding.root) binding.playerView.requestFocus() mediaDataSourceFactory = buildDataSourceFactory(true) mainHandler = Handler() 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 8df86cbb5f..5c5ef240c8 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 @@ -20,7 +20,6 @@ package com.instructure.student.adapter import android.app.Activity import android.view.View import androidx.recyclerview.widget.RecyclerView -import com.instructure.canvasapi2.apis.EnrollmentAPI import com.instructure.canvasapi2.managers.* import com.instructure.canvasapi2.models.* import com.instructure.canvasapi2.utils.isValidTerm @@ -30,7 +29,6 @@ import com.instructure.pandautils.utils.ColorApiHelper import com.instructure.student.flutterChannels.FlutterComm import com.instructure.student.holders.* import com.instructure.student.interfaces.CourseAdapterToFragmentCallback -import com.instructure.student.util.StudentPrefs import org.threeten.bp.OffsetDateTime import java.util.* @@ -140,8 +138,8 @@ class DashboardRecyclerAdapter( // Filter groups val allActiveGroups = groups.filter { group -> group.isActive(mCourseMap[group.courseId])} - val isAnyFavoritePresent = visibleCourses.any { it.isFavorite } || allActiveGroups.any { it.isFavorite } - val visibleGroups = if (isAnyFavoritePresent) allActiveGroups.filter { it.isFavorite } else allActiveGroups + val isAnyGroupFavorited = allActiveGroups.any { it.isFavorite } + val visibleGroups = if (isAnyGroupFavorited) allActiveGroups.filter { it.isFavorite } else allActiveGroups // Add courses addOrUpdateAllItems(ItemType.COURSE_HEADER, visibleCourses) diff --git a/apps/student/src/main/java/com/instructure/student/di/AboutModule.kt b/apps/student/src/main/java/com/instructure/student/di/AboutModule.kt new file mode 100644 index 0000000000..e94b9c9c6e --- /dev/null +++ b/apps/student/src/main/java/com/instructure/student/di/AboutModule.kt @@ -0,0 +1,41 @@ +/* + * 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 android.content.Context +import com.instructure.canvasapi2.utils.ApiPrefs +import com.instructure.pandautils.features.about.AboutRepository +import com.instructure.student.features.about.StudentAboutRepository +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.components.ViewModelComponent +import dagger.hilt.android.qualifiers.ApplicationContext + +@Module +@InstallIn(ViewModelComponent::class) +class AboutModule { + + @Provides + fun provideAboutRepository( + @ApplicationContext context: Context, + apiPrefs: ApiPrefs + ): AboutRepository { + return StudentAboutRepository(context, apiPrefs) + } +} \ No newline at end of file diff --git a/apps/student/src/main/java/com/instructure/student/di/DatabaseModule.kt b/apps/student/src/main/java/com/instructure/student/di/DatabaseModule.kt index 66b90efc0e..4d90364744 100644 --- a/apps/student/src/main/java/com/instructure/student/di/DatabaseModule.kt +++ b/apps/student/src/main/java/com/instructure/student/di/DatabaseModule.kt @@ -3,8 +3,7 @@ package com.instructure.student.di import android.content.Context import androidx.room.Room import com.instructure.pandautils.room.AppDatabase -import com.instructure.pandautils.room.MIGRATION_1_2 -import com.instructure.pandautils.room.MIGRATION_2_3 +import com.instructure.pandautils.room.appDatabaseMigrations import com.instructure.student.db.Db import com.instructure.student.db.StudentDb import com.instructure.student.db.getInstance @@ -23,7 +22,7 @@ class DatabaseModule { @Singleton fun provideDatabase(@ApplicationContext context: Context): AppDatabase { return Room.databaseBuilder(context, AppDatabase::class.java, "db-canvas-student") - .addMigrations(MIGRATION_1_2, MIGRATION_2_3) + .addMigrations(*appDatabaseMigrations) .build() } diff --git a/apps/student/src/main/java/com/instructure/student/features/about/StudentAboutRepository.kt b/apps/student/src/main/java/com/instructure/student/features/about/StudentAboutRepository.kt new file mode 100644 index 0000000000..4782a0013e --- /dev/null +++ b/apps/student/src/main/java/com/instructure/student/features/about/StudentAboutRepository.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.features.about + +import android.content.Context +import com.instructure.canvasapi2.utils.ApiPrefs +import com.instructure.pandautils.features.about.AboutRepository +import com.instructure.student.BuildConfig + +class StudentAboutRepository(context: Context, apiPrefs: ApiPrefs) : + AboutRepository(context, apiPrefs) { + + override val appVersion = BuildConfig.VERSION_NAME + +} \ No newline at end of file diff --git a/apps/student/src/main/java/com/instructure/student/features/assignmentdetails/AssignmentDetailsViewModel.kt b/apps/student/src/main/java/com/instructure/student/features/assignmentdetails/AssignmentDetailsViewModel.kt index 079aa44a6e..7c2c0ab375 100644 --- a/apps/student/src/main/java/com/instructure/student/features/assignmentdetails/AssignmentDetailsViewModel.kt +++ b/apps/student/src/main/java/com/instructure/student/features/assignmentdetails/AssignmentDetailsViewModel.kt @@ -131,7 +131,7 @@ class AssignmentDetailsViewModel @Inject constructor( } if (isUploading && submission.errorFlag) { _data.value?.attempts = attempts?.toMutableList()?.apply { - removeFirst() + if (isNotEmpty()) removeFirst() add(0, AssignmentDetailsAttemptItemViewModel( AssignmentDetailsAttemptViewData( resources.getString(R.string.attempt, attempts.size), diff --git a/apps/student/src/main/java/com/instructure/student/features/elementary/course/ElementaryCoursePagerAdapter.kt b/apps/student/src/main/java/com/instructure/student/features/elementary/course/ElementaryCoursePagerAdapter.kt index f86ae80d18..03cc6d155b 100644 --- a/apps/student/src/main/java/com/instructure/student/features/elementary/course/ElementaryCoursePagerAdapter.kt +++ b/apps/student/src/main/java/com/instructure/student/features/elementary/course/ElementaryCoursePagerAdapter.kt @@ -25,7 +25,7 @@ import android.widget.ProgressBar import androidx.fragment.app.FragmentActivity import androidx.viewpager.widget.PagerAdapter import com.instructure.canvasapi2.utils.ApiPrefs -import com.instructure.pandautils.utils.setDarkModeSupport +import com.instructure.pandautils.utils.enableAlgorithmicDarkening import com.instructure.pandautils.utils.setGone import com.instructure.pandautils.utils.setVisible import com.instructure.pandautils.views.CanvasWebView @@ -65,7 +65,7 @@ class ElementaryCoursePagerAdapter( val baseContext = (webView.context as ContextWrapper).baseContext val activity = (baseContext as? FragmentActivity) activity?.let { webView.addVideoClient(it) } - webView.setDarkModeSupport() + webView.enableAlgorithmicDarkening() webView.setZoomSettings(false) webView.canvasWebViewClientCallback = object : CanvasWebView.CanvasWebViewClientCallback { override fun openMediaFromWebView(mime: String, url: String, filename: String) { 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 0d79356be1..82129d4483 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 @@ -31,6 +31,7 @@ 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 +import com.instructure.pandautils.features.about.AboutFragment import com.instructure.pandautils.features.notification.preferences.EmailNotificationPreferencesFragment import com.instructure.pandautils.features.notification.preferences.PushNotificationPreferencesFragment import com.instructure.pandautils.fragments.RemoteConfigParamsFragment @@ -39,7 +40,6 @@ import com.instructure.student.BuildConfig import com.instructure.student.R import com.instructure.student.activity.NothingToSeeHereFragment import com.instructure.student.activity.SettingsActivity -import com.instructure.student.databinding.DialogAboutBinding import com.instructure.student.databinding.FragmentApplicationSettingsBinding import com.instructure.student.dialog.LegalDialogStyled import com.instructure.student.mobius.settings.pairobserver.ui.PairObserverFragment @@ -108,17 +108,7 @@ class ApplicationSettingsFragment : ParentFragment() { } about.onClick { - val binding = DialogAboutBinding.inflate(layoutInflater) - AlertDialog.Builder(requireContext()) - .setTitle(R.string.about) - .setView(binding.root) - .show() - .apply { - binding.domain.text = ApiPrefs.domain - binding.loginId.text = ApiPrefs.user!!.loginId - binding.email.text = ApiPrefs.user!!.email ?: ApiPrefs.user!!.primaryEmail - binding.version.text = "${getString(R.string.canvasVersionNum)} ${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})" - } + AboutFragment.newInstance().show(childFragmentManager, null) } if (ApiPrefs.canvasForElementary) { diff --git a/apps/student/src/main/java/com/instructure/student/fragment/AssignmentListFragment.kt b/apps/student/src/main/java/com/instructure/student/fragment/AssignmentListFragment.kt index cede1cd23a..59d9c327e7 100644 --- a/apps/student/src/main/java/com/instructure/student/fragment/AssignmentListFragment.kt +++ b/apps/student/src/main/java/com/instructure/student/fragment/AssignmentListFragment.kt @@ -91,6 +91,7 @@ class AssignmentListFragment : ParentFragment(), Bookmarkable { private val adapterToAssignmentsCallback = object : AdapterToAssignmentsCallback { override fun assignmentLoadingFinished() { + if (view == null) return // If we only have one grading period we want to disable the spinner val termCount = termAdapter?.count ?: 0 binding.termSpinner.isEnabled = termCount > 1 @@ -99,6 +100,7 @@ class AssignmentListFragment : ParentFragment(), Bookmarkable { } override fun gradingPeriodsFetched(periods: List) { + if (view == null) return setupGradingPeriods(periods) } diff --git a/apps/student/src/main/java/com/instructure/student/fragment/BasicQuizViewFragment.kt b/apps/student/src/main/java/com/instructure/student/fragment/BasicQuizViewFragment.kt index 175cc86279..8ee29fb316 100644 --- a/apps/student/src/main/java/com/instructure/student/fragment/BasicQuizViewFragment.kt +++ b/apps/student/src/main/java/com/instructure/student/fragment/BasicQuizViewFragment.kt @@ -88,7 +88,7 @@ class BasicQuizViewFragment : InternalWebviewFragment() { // Make sure we are prepared to handle file uploads for quizzes that allow them setupFilePicker() - binding.canvasWebViewWrapper.webView.setDarkModeSupport() + binding.canvasWebViewWrapper.webView.enableAlgorithmicDarkening() } override fun onActivityCreated(savedInstanceState: Bundle?) { @@ -102,8 +102,8 @@ class BasicQuizViewFragment : InternalWebviewFragment() { } val uri = Uri.parse(baseURL) val host = uri.host ?: "" - getCanvasWebView().settings.javaScriptCanOpenWindowsAutomatically = true - getCanvasWebView().webViewClient = object : WebViewClient() { + getCanvasWebView()?.settings?.javaScriptCanOpenWindowsAutomatically = true + getCanvasWebView()?.webViewClient = object : WebViewClient() { override fun shouldOverrideUrlLoading(view: WebView?, request: WebResourceRequest?): Boolean = handleOverrideURlLoading(view, request?.url?.toString()) override fun shouldOverrideUrlLoading(view: WebView, url: String): Boolean = handleOverrideURlLoading(view, url) @@ -130,18 +130,18 @@ class BasicQuizViewFragment : InternalWebviewFragment() { override fun onPageStarted(view: WebView, url: String, favicon: Bitmap?) { super.onPageStarted(view, url, favicon) - getCanvasLoading().visibility = View.VISIBLE + getCanvasLoading()?.visibility = View.VISIBLE } override fun onPageFinished(view: WebView, url: String) { super.onPageFinished(view, url) - getCanvasLoading().visibility = View.GONE + getCanvasLoading()?.visibility = View.GONE } } } private fun setupFilePicker() { - getCanvasWebView().setCanvasWebChromeClientShowFilePickerCallback(object : CanvasWebView.VideoPickerCallback { + getCanvasWebView()?.setCanvasWebChromeClientShowFilePickerCallback(object : CanvasWebView.VideoPickerCallback { override fun requestStartActivityForResult(intent: Intent, requestCode: Int) { startActivityForResult(intent, requestCode) } @@ -167,13 +167,13 @@ class BasicQuizViewFragment : InternalWebviewFragment() { @Subscribe(threadMode = ThreadMode.MAIN) fun onRequestPermissionsResult(result: PermissionRequester.PermissionResult) { if (PermissionUtils.allPermissionsGrantedResultSummary(result.grantResults)) { - getCanvasWebView().clearPickerCallback() + getCanvasWebView()?.clearPickerCallback() Toast.makeText(requireContext(), R.string.pleaseTryAgain, Toast.LENGTH_SHORT).show() } } override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - if (!getCanvasWebView().handleOnActivityResult(requestCode, resultCode, data)) { + if (getCanvasWebView()?.handleOnActivityResult(requestCode, resultCode, data) == false) { super.onActivityResult(requestCode, resultCode, data) } } @@ -213,7 +213,7 @@ class BasicQuizViewFragment : InternalWebviewFragment() { val authenticatedUrl = tryOrNull { awaitApi { OAuthManager.getAuthenticatedSession(url, it) }.sessionUrl } - getCanvasWebView().loadUrl(authenticatedUrl ?: url, APIHelper.referrer) + getCanvasWebView()?.loadUrl(authenticatedUrl ?: url, APIHelper.referrer) } } @@ -222,7 +222,7 @@ class BasicQuizViewFragment : InternalWebviewFragment() { quizDetailsJob?.cancel() } - override fun handleBackPressed() = getCanvasWebView().handleGoBack() ?: false + override fun handleBackPressed() = getCanvasWebView()?.handleGoBack() ?: false companion object { diff --git a/apps/student/src/main/java/com/instructure/student/fragment/CourseModuleProgressionFragment.kt b/apps/student/src/main/java/com/instructure/student/fragment/CourseModuleProgressionFragment.kt index b32db553b5..9140c858f7 100644 --- a/apps/student/src/main/java/com/instructure/student/fragment/CourseModuleProgressionFragment.kt +++ b/apps/student/src/main/java/com/instructure/student/fragment/CourseModuleProgressionFragment.kt @@ -24,11 +24,9 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.fragment.app.Fragment -import androidx.fragment.app.FragmentManager -import androidx.fragment.app.FragmentStatePagerAdapter +import androidx.lifecycle.DefaultLifecycleObserver +import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.lifecycleScope -import androidx.viewpager.widget.PagerAdapter -import androidx.viewpager.widget.ViewPager import com.instructure.canvasapi2.StatusCallback import com.instructure.canvasapi2.managers.* import com.instructure.canvasapi2.models.* @@ -63,7 +61,6 @@ import kotlinx.coroutines.Job import kotlinx.coroutines.launch import okhttp3.ResponseBody import retrofit2.Response -import java.util.* @PageView(url = "courses/{canvasContext}/modules") @ScreenView(SCREEN_VIEW_COURSE_MODULE_PROGRESSION) @@ -95,8 +92,6 @@ class CourseModuleProgressionFragment : ParentFragment(), Bookmarkable { // Default number will get reset private var itemsCount = 3 - private var adapter: CourseModuleProgressionAdapter? = null - // There's a case where we try to get the previous module and the previous module has a paginated list // of items. A task will get those items and populate them in the background, but it throws off the // indexes because it adds the items to (possibly) the middle of the arrayList that backs the adapter. @@ -146,9 +141,9 @@ class CourseModuleProgressionFragment : ParentFragment(), Bookmarkable { //region Fragment Overrides // This function is mostly for the internal web view fragments so we can go back in the webview override fun handleBackPressed(): Boolean = with(binding) { - if (viewPager.currentItem != -1 && items.isNotEmpty()) { - val pFrag = adapter?.instantiateItem(viewPager, viewPager.currentItem) as? ParentFragment - if (pFrag != null && pFrag.handleBackPressed()) { + if (items.isNotEmpty()) { + val pFrag = childFragmentManager.fragments.firstOrNull() as? ParentFragment + if (pFrag?.handleBackPressed() == true) { return true } } @@ -180,7 +175,7 @@ class CourseModuleProgressionFragment : ParentFragment(), Bookmarkable { setupPrevModuleName(currentPos) setupPreviousModule(getModuleItemGroup(currentPos)) if (currentPos >= 1) { - binding.viewPager.currentItem = --currentPos + showFragment(getItem(--currentPos)) } updateBottomNavBarButtons() @@ -190,7 +185,7 @@ class CourseModuleProgressionFragment : ParentFragment(), Bookmarkable { setupNextModuleName(currentPos) setupNextModule(getModuleItemGroup(currentPos)) if (currentPos < itemsCount - 1) { - binding.viewPager.currentItem = ++currentPos + showFragment(getItem(++currentPos)) } updateBottomNavBarButtons() } @@ -248,18 +243,7 @@ class CourseModuleProgressionFragment : ParentFragment(), Bookmarkable { getCurrentModuleItemPos(groupPos, childPos) } - // Setup adapter - adapter = CourseModuleProgressionAdapter(childFragmentManager) - - binding.viewPager.apply { - adapter = this@CourseModuleProgressionFragment.adapter - - // Set a custom page transformer for RTL - if (Locale.getDefault().isRtl) setPageTransformer(true, pageTransformer) - - // Set the item number in the adapter to be the overall position - currentItem = currentPos - } + showFragment(getItem(currentPos)) updatePrevNextButtons(currentPos) @@ -282,6 +266,23 @@ class CourseModuleProgressionFragment : ParentFragment(), Bookmarkable { updateModuleMarkDoneView(getCurrentModuleItem(currentPos)) } + + private fun showFragment(item: Fragment?) { + item?.let { + childFragmentManager.beginTransaction().replace(R.id.fragmentContainer, it).commitAllowingStateLoss() + applyFragmentTheme(it) + } + } + + private fun applyFragmentTheme(fragment: Fragment) { + fragment.lifecycle.addObserver(object : DefaultLifecycleObserver { + override fun onResume(owner: LifecycleOwner) { + super.onResume(owner) + (fragment as? FragmentInteractions)?.applyTheme() + fragment.lifecycle.removeObserver(this) + } + }) + } //endregion //region View Helpers @@ -300,7 +301,6 @@ class CourseModuleProgressionFragment : ParentFragment(), Bookmarkable { // Reload the sequential module object to update the subsequent items that may now be unlocked // The user has viewed the item, and may have completed the contribute/submit requirements for a // discussion/assignment. - adapter?.notifyDataSetChanged() addLockedIconIfNeeded(modules, items, groupPos, childPos) // Mark the item as viewed @@ -406,11 +406,9 @@ class CourseModuleProgressionFragment : ParentFragment(), Bookmarkable { currentPos += itemsAdded } - adapter?.notifyDataSetChanged() - // When we tap on a module item it will try to load the previous and next_item modules, this can throw off the module item that was already loaded, // so load it to the current position - binding.viewPager.currentItem = currentPos + showFragment(getItem(currentPos)) //prev_item/next_item buttons may now need to be visible (if we were on a module item that was the last in its group but //now we have info about the next_item module, we want the user to be able to navigate there) @@ -617,85 +615,31 @@ class CourseModuleProgressionFragment : ParentFragment(), Bookmarkable { private fun setLockedIcon() { binding.moduleNameIcon.setVisible() } - //endregion - - private fun getModelObject(): ModuleItem? = getCurrentModuleItem(currentPos) - - //region Adapter - inner class CourseModuleProgressionAdapter(fm: FragmentManager) : FragmentStatePagerAdapter(fm) { - - private var expectingUpdate: Boolean = false - - override fun finishUpdate(container: ViewGroup) { - super.finishUpdate(container) - if (!expectingUpdate) return - - expectingUpdate = false - val fragments = childFragmentManager.fragments - for (fragment in fragments) { - if (fragment.isResumed) - (fragment as? FragmentInteractions)?.applyTheme() - } - } - - override fun getItemPosition(`object`: Any): Int = PagerAdapter.POSITION_NONE - - override fun getCount(): Int = itemsCount - - override fun getItem(position: Int): Fragment { - expectingUpdate = true - // Position is the overall position, and we could have multiple modules with their individual positions (if 2 modules have 3 items each, the last - // item in the second module is position 5, not 2 (zero based)), - // so we need to find the correct one overall - val moduleItem = getCurrentModuleItem(position) ?: getCurrentModuleItem(0) // Default to the first item, band-aid for NPE + private fun getItem(position: Int): Fragment { + // Position is the overall position, and we could have multiple modules with their individual positions (if 2 modules have 3 items each, the last + // item in the second module is position 5, not 2 (zero based)), + // so we need to find the correct one overall + val moduleItem = getCurrentModuleItem(position) ?: getCurrentModuleItem(0) // Default to the first item, band-aid for NPE - val fragment = ModuleUtility.getFragment(moduleItem!!, canvasContext as Course, modules[groupPos], isDiscussionRedesignEnabled) - var args: Bundle? = fragment!!.arguments - if (args == null) { - args = Bundle() - fragment.arguments = args - } - - // Add module item ID to bundle for PageView tracking. - args.putLong(com.instructure.pandautils.utils.Const.MODULE_ITEM_ID, moduleItem.id) - - return fragment - // Don't update the actionbar title here, we'll do it later. When we update it here the actionbar title sometimes - // gets updated to the next_item fragment's title + val fragment = ModuleUtility.getFragment(moduleItem!!, canvasContext as Course, modules[groupPos], isDiscussionRedesignEnabled) + var args: Bundle? = fragment!!.arguments + if (args == null) { + args = Bundle() + fragment.arguments = args } - override fun setPrimaryItem(container: ViewGroup, position: Int, `object`: Any) { - super.setPrimaryItem(container, position, `object`) - // For PageView tracking - (`object` as? Fragment)?.userVisibleHint = true - } + // Add module item ID to bundle for PageView tracking. + args.putLong(com.instructure.pandautils.utils.Const.MODULE_ITEM_ID, moduleItem.id) - override fun destroyItem(container: ViewGroup, position: Int, `object`: Any) { - // Need to remove all the child fragments so they don't - // hang around and get attached to other activities - val fragment = `object` as? ParentFragment - fragment?.removeChildFragments() - super.destroyItem(container, position, `object`) - } + return fragment + // Don't update the actionbar title here, we'll do it later. When we update it here the actionbar title sometimes + // gets updated to the next_item fragment's title } - - // For RTL - this prevents the scrolling animations (ViewPager doesn't come with RTL support and default page transition animations are backwards) - private val pageTransformer = ViewPager.PageTransformer { page, position -> - // Page on right, position = 1 - // Page on left, position = -1 - // Page on screen, position = 0 - - // Position updates dynamically, scrolling halfway through a page means one page pos = -0.5, the other 0.5 - page.apply { - translationX = width * -position - visibility = if (position in -0.5..0.5) View.VISIBLE else View.GONE - } - - } - //endregion + private fun getModelObject(): ModuleItem? = getCurrentModuleItem(currentPos) + //region Bookmarks override val bookmark: Bookmarker get() = Bookmarker(true, canvasContext) diff --git a/apps/student/src/main/java/com/instructure/student/fragment/InboxConversationFragment.kt b/apps/student/src/main/java/com/instructure/student/fragment/InboxConversationFragment.kt index f903f9db9b..8ec882d009 100644 --- a/apps/student/src/main/java/com/instructure/student/fragment/InboxConversationFragment.kt +++ b/apps/student/src/main/java/com/instructure/student/fragment/InboxConversationFragment.kt @@ -401,7 +401,7 @@ class InboxConversationFragment : ParentFragment() { } private fun refreshConversationData() { - initConversationDetails() + if (view != null) initConversationDetails() } private fun onConversationUpdated(goBack: Boolean) { diff --git a/apps/student/src/main/java/com/instructure/student/fragment/InternalWebviewFragment.kt b/apps/student/src/main/java/com/instructure/student/fragment/InternalWebviewFragment.kt index 1fe134acf8..3bb125ecda 100644 --- a/apps/student/src/main/java/com/instructure/student/fragment/InternalWebviewFragment.kt +++ b/apps/student/src/main/java/com/instructure/student/fragment/InternalWebviewFragment.kt @@ -113,7 +113,6 @@ open class InternalWebviewFragment : ParentFragment() { originalUserAgentString = canvasWebViewWrapper.webView.settings.userAgentString canvasWebViewWrapper.webView.settings.userAgentString = ApiPrefs.userAgent canvasWebViewWrapper.webView.setInitialScale(100) - canvasWebViewWrapper.webView.setDarkModeSupport(webThemeDarkeningOnly = true) webViewLoading.setVisible(true) canvasWebViewWrapper.webView.canvasWebChromeClientCallback = @@ -345,8 +344,8 @@ open class InternalWebviewFragment : ParentFragment() { } else false } - fun getCanvasLoading(): ProgressBar = binding.webViewLoading - fun getCanvasWebView(): CanvasWebView = binding.canvasWebViewWrapper.webView + fun getCanvasLoading(): ProgressBar? = if (view != null) binding.webViewLoading else null + fun getCanvasWebView(): CanvasWebView? = if (view != null) binding.canvasWebViewWrapper.webView else null fun getIsUnsupportedFeature(): Boolean = isUnsupportedFeature private fun getReferer(): Map = mutableMapOf(Pair("Referer", ApiPrefs.domain)) diff --git a/apps/student/src/main/java/com/instructure/student/fragment/LockedModuleItemFragment.kt b/apps/student/src/main/java/com/instructure/student/fragment/LockedModuleItemFragment.kt index 6533e7172c..510a568fce 100644 --- a/apps/student/src/main/java/com/instructure/student/fragment/LockedModuleItemFragment.kt +++ b/apps/student/src/main/java/com/instructure/student/fragment/LockedModuleItemFragment.kt @@ -33,7 +33,6 @@ import com.instructure.pandautils.utils.Const import com.instructure.pandautils.utils.ParcelableArg import com.instructure.pandautils.utils.StringArg import com.instructure.pandautils.utils.ViewStyler -import com.instructure.pandautils.utils.setDarkModeSupport import com.instructure.pandautils.utils.setupAsBackButton import com.instructure.pandautils.views.CanvasWebView import com.instructure.student.R @@ -72,7 +71,6 @@ class LockedModuleItemFragment : ParentFragment() { override fun applyTheme() { } private fun setupWebView(canvasWebView: CanvasWebView) { - canvasWebView.setDarkModeSupport(webThemeDarkeningOnly = true) canvasWebView.settings.loadWithOverviewMode = true canvasWebView.settings.displayZoomControls = false canvasWebView.settings.setSupportZoom(true) diff --git a/apps/student/src/main/java/com/instructure/student/fragment/PageDetailsFragment.kt b/apps/student/src/main/java/com/instructure/student/fragment/PageDetailsFragment.kt index 11bb6b9a34..d38035dd5d 100644 --- a/apps/student/src/main/java/com/instructure/student/fragment/PageDetailsFragment.kt +++ b/apps/student/src/main/java/com/instructure/student/fragment/PageDetailsFragment.kt @@ -102,19 +102,19 @@ class PageDetailsFragment : InternalWebviewFragment(), Bookmarkable { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - getCanvasWebView().canvasEmbeddedWebViewCallback = object : CanvasWebView.CanvasEmbeddedWebViewCallback { + getCanvasWebView()?.canvasEmbeddedWebViewCallback = object : CanvasWebView.CanvasEmbeddedWebViewCallback { override fun shouldLaunchInternalWebViewFragment(url: String): Boolean = true override fun launchInternalWebViewFragment(url: String) = InternalWebviewFragment.loadInternalWebView(activity, InternalWebviewFragment.makeRoute(canvasContext, url, isLTITool)) } // Add to the webview client for clearing webview history after an update to prevent going back to old data - val callback = getCanvasWebView().canvasWebViewClientCallback + val callback = getCanvasWebView()?.canvasWebViewClientCallback callback?.let { - getCanvasWebView().canvasWebViewClientCallback = object : CanvasWebView.CanvasWebViewClientCallback by it { + getCanvasWebView()?.canvasWebViewClientCallback = object : CanvasWebView.CanvasWebViewClientCallback by it { override fun onPageFinishedCallback(webView: WebView, url: String) { it.onPageFinishedCallback(webView, url) // Only clear history after an update - if (isUpdated) getCanvasWebView().clearHistory() + if (isUpdated) getCanvasWebView()?.clearHistory() } override fun openMediaFromWebView(mime: String, url: String, filename: String) { @@ -289,12 +289,16 @@ class PageDetailsFragment : InternalWebviewFragment(), Bookmarkable { } private fun checkCanEdit() = with(binding) { - if (page.editingRoles?.contains("public") == true) { - toolbar.menu?.findItem(R.id.menu_edit)?.isVisible = true - } else if (page.editingRoles?.contains("student") == true && (canvasContext as? Course)?.isStudent == true) { - toolbar.menu?.findItem(R.id.menu_edit)?.isVisible = true - } else if (page.editingRoles?.contains("teacher") == true && (canvasContext as? Course)?.isTeacher == true) { - toolbar.menu?.findItem(R.id.menu_edit)?.isVisible = true + val course = canvasContext as? Course + val editingRoles = page.editingRoles.orEmpty() + if (course?.isStudent == true) { + if (page.lockInfo == null && (editingRoles.contains("public") || editingRoles.contains("student"))) { + toolbar.menu?.findItem(R.id.menu_edit)?.isVisible = true + } + } else if (course?.isTeacher == true) { + if ((editingRoles.contains("public") || editingRoles.contains("teacher"))) { + toolbar.menu?.findItem(R.id.menu_edit)?.isVisible = true + } } } diff --git a/apps/student/src/main/java/com/instructure/student/fragment/StudioWebViewFragment.kt b/apps/student/src/main/java/com/instructure/student/fragment/StudioWebViewFragment.kt index f7f1eda1e9..7cd4b3bf2f 100644 --- a/apps/student/src/main/java/com/instructure/student/fragment/StudioWebViewFragment.kt +++ b/apps/student/src/main/java/com/instructure/student/fragment/StudioWebViewFragment.kt @@ -50,16 +50,16 @@ class StudioWebViewFragment : InternalWebviewFragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - getCanvasWebView().setDarkModeSupport() - getCanvasWebView().addJavascriptInterface(JSInterface(), "HtmlViewer") + getCanvasWebView()?.enableAlgorithmicDarkening() + getCanvasWebView()?.addJavascriptInterface(JSInterface(), "HtmlViewer") - getCanvasWebView().canvasWebViewClientCallback = object : CanvasWebView.CanvasWebViewClientCallback { + getCanvasWebView()?.canvasWebViewClientCallback = object : CanvasWebView.CanvasWebViewClientCallback { override fun openMediaFromWebView(mime: String, url: String, filename: String) { openMedia(mime, url, filename, canvasContext) } override fun onPageFinishedCallback(webView: WebView, url: String) { - getCanvasLoading().visibility = View.GONE + getCanvasLoading()?.visibility = View.GONE // Check for a successful Studio submission if (url.contains("success/external_tool_dialog")) { @@ -68,7 +68,7 @@ class StudioWebViewFragment : InternalWebviewFragment() { } override fun onPageStartedCallback(webView: WebView, url: String) { - getCanvasLoading().visibility = View.VISIBLE + getCanvasLoading()?.visibility = View.VISIBLE } override fun canRouteInternallyDelegate(url: String): Boolean { @@ -80,7 +80,7 @@ class StudioWebViewFragment : InternalWebviewFragment() { } } - getCanvasWebView().setCanvasWebChromeClientShowFilePickerCallback(object : CanvasWebView.VideoPickerCallback { + getCanvasWebView()?.setCanvasWebChromeClientShowFilePickerCallback(object : CanvasWebView.VideoPickerCallback { override fun requestStartActivityForResult(intent: Intent, requestCode: Int) { startActivityForResult(intent, requestCode) } @@ -99,8 +99,8 @@ class StudioWebViewFragment : InternalWebviewFragment() { override fun handleBackPressed(): Boolean { if (canGoBack()) { // This prevents a silly bug where the Studio WebView cannot go back far enough to pop its fragment - val webBackForwardList = getCanvasWebView().copyBackForwardList() - val historyUrl = webBackForwardList.getItemAtIndex(webBackForwardList.currentIndex - 1)?.url + val webBackForwardList = getCanvasWebView()?.copyBackForwardList() + val historyUrl = webBackForwardList?.getItemAtIndex(webBackForwardList.currentIndex - 1)?.url if (historyUrl != null && historyUrl.contains("external_tools/") && historyUrl.contains("resource_selection")) { navigation?.popCurrentFragment() return true @@ -116,7 +116,7 @@ class StudioWebViewFragment : InternalWebviewFragment() { override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) { if (PermissionUtils.allPermissionsGrantedResultSummary(grantResults)) { - getCanvasWebView().clearPickerCallback() + getCanvasWebView()?.clearPickerCallback() Toast.makeText(requireContext(), R.string.pleaseTryAgain, Toast.LENGTH_SHORT).show() } super.onRequestPermissionsResult(requestCode, permissions, grantResults) @@ -143,7 +143,7 @@ class StudioWebViewFragment : InternalWebviewFragment() { } override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - if ((getCanvasWebView().handleOnActivityResult(requestCode, resultCode, data)) != true) { + if ((getCanvasWebView()?.handleOnActivityResult(requestCode, resultCode, data)) != true) { super.onActivityResult(requestCode, resultCode, data) } } diff --git a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/content/DiscussionSubmissionViewFragment.kt b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/content/DiscussionSubmissionViewFragment.kt index dba98d880d..b9b4934e62 100644 --- a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/content/DiscussionSubmissionViewFragment.kt +++ b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/content/DiscussionSubmissionViewFragment.kt @@ -30,7 +30,7 @@ import com.instructure.canvasapi2.utils.weave.StatusCallbackError import com.instructure.canvasapi2.utils.weave.awaitApi import com.instructure.pandautils.binding.viewBinding import com.instructure.pandautils.utils.StringArg -import com.instructure.pandautils.utils.setDarkModeSupport +import com.instructure.pandautils.utils.enableAlgorithmicDarkening import com.instructure.pandautils.utils.setGone import com.instructure.pandautils.utils.setVisible import com.instructure.pandautils.views.CanvasWebView @@ -61,7 +61,7 @@ class DiscussionSubmissionViewFragment : Fragment() { override fun onStart() { super.onStart() binding.progressBar.announceForAccessibility(getString(R.string.loading)) - binding.discussionSubmissionWebView.setDarkModeSupport() + binding.discussionSubmissionWebView.enableAlgorithmicDarkening() binding.discussionSubmissionWebView.webChromeClient = object : WebChromeClient() { override fun onProgressChanged(view: WebView?, newProgress: Int) { super.onProgressChanged(view, newProgress) diff --git a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/content/QuizSubmissionViewFragment.kt b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/content/QuizSubmissionViewFragment.kt index df65e0dc61..19b4699405 100644 --- a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/content/QuizSubmissionViewFragment.kt +++ b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/content/QuizSubmissionViewFragment.kt @@ -28,9 +28,9 @@ import com.instructure.student.fragment.InternalWebviewFragment @ScreenView(SCREEN_VIEW_QUIZ_SUBMISSION_VIEW) class QuizSubmissionViewFragment : InternalWebviewFragment() { override fun onActivityCreated(savedInstanceState: Bundle?) { - getCanvasLoading().setVisible() // Set visible so we can test it + getCanvasLoading()?.setVisible() // Set visible so we can test it binding.canvasWebViewWrapper.apply { - webView.setDarkModeSupport() + webView.enableAlgorithmicDarkening() webView.setInitialScale(100) setInvisible() // Set invisible so we can test it webView.webChromeClient = object : WebChromeClient() { @@ -40,10 +40,10 @@ class QuizSubmissionViewFragment : InternalWebviewFragment() { // Update visibilities if (newProgress >= 100) { - getCanvasLoading().setGone() + getCanvasLoading()?.setGone() setVisible() } else { - getCanvasLoading().announceForAccessibility(getString(R.string.loading)) + getCanvasLoading()?.announceForAccessibility(getString(R.string.loading)) } } } diff --git a/apps/student/src/main/java/com/instructure/student/router/RouteMatcher.kt b/apps/student/src/main/java/com/instructure/student/router/RouteMatcher.kt index 4ef1689826..cb24098b0e 100644 --- a/apps/student/src/main/java/com/instructure/student/router/RouteMatcher.kt +++ b/apps/student/src/main/java/com/instructure/student/router/RouteMatcher.kt @@ -139,6 +139,7 @@ object RouteMatcher : BaseRouteMatcher() { // Discussions routes.add(Route(courseOrGroup("/:${RouterParams.COURSE_ID}/discussion_topics"), DiscussionListFragment::class.java)) routes.add(Route(courseOrGroup("/:${RouterParams.COURSE_ID}/discussion_topics/:${RouterParams.MESSAGE_ID}"), DiscussionListFragment::class.java, CourseModuleProgressionFragment::class.java)) + routes.add(Route(courseOrGroup("/:${RouterParams.COURSE_ID}/discussion_topics/:${RouterParams.MESSAGE_ID}"), DiscussionListFragment::class.java, DiscussionRouterFragment::class.java)) // Route for bookmarking // Pages routes.add(Route(courseOrGroup("/:${RouterParams.COURSE_ID}/pages"), PageListFragment::class.java)) @@ -499,7 +500,11 @@ object RouteMatcher : BaseRouteMatcher() { fun generateUrl(type: CanvasContext.Type, masterCls: Class?, detailCls: Class?, replacementParams: HashMap?, queryParams: HashMap?): String? { val domain = ApiPrefs.fullDomain - val urlRoute = getInternalRoute(masterCls, detailCls) + + // Workaround for the discussion details because we bookmark a different class that we use for routing + val detailsClass = if (detailCls == DiscussionDetailsFragment::class.java) DiscussionRouterFragment::class.java else detailCls + + val urlRoute = getInternalRoute(masterCls, detailsClass) if(urlRoute != null) { var path = urlRoute.createUrl(replacementParams) if (path.contains(COURSE_OR_GROUP_REGEX)) { diff --git a/apps/student/src/main/java/com/instructure/student/view/AttachmentDogEarLayout.kt b/apps/student/src/main/java/com/instructure/student/view/AttachmentDogEarLayout.kt deleted file mode 100644 index ae0e03dd3f..0000000000 --- a/apps/student/src/main/java/com/instructure/student/view/AttachmentDogEarLayout.kt +++ /dev/null @@ -1,120 +0,0 @@ -/* - * Copyright (C) 2018 - 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.view - -import android.content.Context -import android.graphics.* -import androidx.core.content.ContextCompat -import androidx.core.view.ViewCompat -import android.util.AttributeSet -import android.view.View -import android.widget.FrameLayout -import com.instructure.student.R -import com.instructure.pandautils.utils.DP -import com.instructure.pandautils.utils.obtainFor - -class AttachmentDogEarLayout @JvmOverloads constructor( - context: Context, - attrs: AttributeSet? = null, - defStyleAttr: Int = 0 -) : FrameLayout(context, attrs, defStyleAttr) { - - private var dogEarSize = 0f - - private val rtlFlipMatrix: Matrix by lazy { - Matrix().apply { postScale(-1f, 1f, width / 2f, 0f) } - } - - private val dogEarPaint: Paint by lazy { - Paint(Paint.ANTI_ALIAS_FLAG).apply { color = ContextCompat.getColor(context, R.color.backgroundMedium) } - } - - private val dogEarShadowPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { color = 0x33000000 } - - private val clipPath: Path by lazy { - Path().apply { - moveTo(0f, 0f) - lineTo(dogEarPoint.x, 0f) - lineTo(width.toFloat(), dogEarPoint.y) - lineTo(width.toFloat(), height.toFloat()) - lineTo(0f, height.toFloat()) - close() - flipForRtlIfNecessary(this) - } - } - - private val dogEarPath: Path by lazy { - Path().apply { - moveTo(dogEarPoint.x, -1f) - lineTo(width + 1f, dogEarPoint.y) - lineTo(dogEarPoint.x, dogEarPoint.y) - close() - flipForRtlIfNecessary(this) - } - } - - private val dogEarPoint: PointF by lazy { - PointF(width - dogEarSize, dogEarSize) - } - - private val dogEarShadowPath: Path by lazy { - Path().apply { - moveTo(dogEarPoint.x, -1f) - lineTo(width + 1f, dogEarPoint.y + 1) - lineTo(dogEarPoint.x + dogEarPoint.y * DOG_EAR_SHADOW_OFFSET_MULTIPLIER_X, - dogEarPoint.y + dogEarPoint.y * DOG_EAR_SHADOW_OFFSET_MULTIPLIER_Y) - close() - flipForRtlIfNecessary(this) - } - } - - init { - // In edit mode, only the software layer type supports non-rectangular path clipping - if (isInEditMode) setLayerType(View.LAYER_TYPE_SOFTWARE, null) - - // Set defaults and get any XML attributes - dogEarSize = context.DP(DOG_EAR_DIMEN_DP) - attrs?.obtainFor(this, R.styleable.AttachmentDogEarLayout) { a, idx -> - when (idx) { - R.styleable.AttachmentDogEarLayout_adl_dogear_size -> dogEarSize = a.getDimension(idx, dogEarSize) - } - } - } - - override fun dispatchDraw(canvas: Canvas) { - // Perform clip, draw children - canvas.save() - canvas.clipPath(clipPath) - super.dispatchDraw(canvas) - canvas.restore() - - // Draw dog-ear - canvas.drawPath(dogEarShadowPath, dogEarShadowPaint) - canvas.drawPath(dogEarPath, dogEarPaint) - } - - private fun flipForRtlIfNecessary(path: Path) { - if (ViewCompat.getLayoutDirection(this) == ViewCompat.LAYOUT_DIRECTION_RTL) { - path.transform(rtlFlipMatrix) - } - } - - companion object { - private const val DOG_EAR_DIMEN_DP = 20f - private const val DOG_EAR_SHADOW_OFFSET_MULTIPLIER_X = 0.08f - private const val DOG_EAR_SHADOW_OFFSET_MULTIPLIER_Y = 0.15f - } -} 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 8a0712be2e..4918489d1c 100644 --- a/apps/student/src/main/res/layout/course_module_progression.xml +++ b/apps/student/src/main/res/layout/course_module_progression.xml @@ -23,8 +23,8 @@ android:orientation="vertical" android:id="@+id/moduleProgressionPage"> - @@ -34,8 +34,8 @@ android:id="@+id/progressBar" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_alignTop="@id/viewPager" - android:layout_alignBottom="@id/viewPager" + android:layout_alignTop="@id/fragmentContainer" + android:layout_alignBottom="@id/fragmentContainer" android:layout_centerHorizontal="true" android:visibility="gone"/> diff --git a/apps/student/src/main/res/layout/dialog_about.xml b/apps/student/src/main/res/layout/dialog_about.xml deleted file mode 100644 index b47feeb7b5..0000000000 --- a/apps/student/src/main/res/layout/dialog_about.xml +++ /dev/null @@ -1,76 +0,0 @@ - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/apps/student/src/main/res/layout/view_attachment.xml b/apps/student/src/main/res/layout/view_attachment.xml index 748dad14a8..50fee7f5c3 100644 --- a/apps/student/src/main/res/layout/view_attachment.xml +++ b/apps/student/src/main/res/layout/view_attachment.xml @@ -14,7 +14,7 @@ ~ You should have received a copy of the GNU General Public License ~ along with this program. If not, see . --> - - + diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/InboxPageTest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/InboxPageTest.kt index 19843d35f1..129b79248e 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/InboxPageTest.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/InboxPageTest.kt @@ -18,7 +18,11 @@ package com.instructure.teacher.ui import androidx.test.espresso.matcher.ViewMatchers -import com.instructure.canvas.espresso.mockCanvas.* +import com.instructure.canvas.espresso.mockCanvas.MockCanvas +import com.instructure.canvas.espresso.mockCanvas.addConversation +import com.instructure.canvas.espresso.mockCanvas.addConversations +import com.instructure.canvas.espresso.mockCanvas.createBasicConversation +import com.instructure.canvas.espresso.mockCanvas.init import com.instructure.canvasapi2.models.Conversation import com.instructure.canvasapi2.models.User import com.instructure.panda_annotations.FeatureCategory @@ -98,6 +102,7 @@ class InboxPageTest: TeacherTest() { inboxPage.assertConversationNotDisplayed(conversation2.subject!!) inboxPage.assertEditToolbarIs(ViewMatchers.Visibility.GONE) + inboxPage.refresh() inboxPage.filterInbox("Archived") inboxPage.assertConversationDisplayed(conversation1.subject!!) inboxPage.assertConversationDisplayed(conversation2.subject!!) diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/DashboardE2ETest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/DashboardE2ETest.kt index 72bf7ca58c..d1e7633b04 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/DashboardE2ETest.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/DashboardE2ETest.kt @@ -155,7 +155,7 @@ class DashboardE2ETest : TeacherTest() { dashboardPage.waitForRender() Log.d(STEP_TAG, "Open Help Menu.") - dashboardPage.openHelpMenu() + leftSideNavigationDrawerPage.clickHelpMenu() Log.d(STEP_TAG, "Assert Help Menu Dialog is displayed.") helpPage.assertHelpMenuDisplayed() diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/FilesE2ETest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/FilesE2ETest.kt index e5450a8385..afd8f76eec 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/FilesE2ETest.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/FilesE2ETest.kt @@ -29,7 +29,13 @@ import com.instructure.canvasapi2.utils.weave.tryWeave import com.instructure.dataseeding.api.AssignmentsApi import com.instructure.dataseeding.api.DiscussionTopicsApi import com.instructure.dataseeding.api.SubmissionsApi -import com.instructure.dataseeding.model.* +import com.instructure.dataseeding.model.AssignmentApiModel +import com.instructure.dataseeding.model.AttachmentApiModel +import com.instructure.dataseeding.model.CanvasUserApiModel +import com.instructure.dataseeding.model.CourseApiModel +import com.instructure.dataseeding.model.DiscussionApiModel +import com.instructure.dataseeding.model.FileUploadType +import com.instructure.dataseeding.model.SubmissionType import com.instructure.dataseeding.util.Randomizer import com.instructure.espresso.ViewUtils import com.instructure.panda_annotations.FeatureCategory @@ -120,8 +126,8 @@ class FilesE2ETest: TeacherTest() { Log.v(PREPARATION_TAG, "Discussion post error: $it") } - Log.d(STEP_TAG,"Navigate to 'Files' menu in user left-side menubar.") - dashboardPage.gotoGlobalFiles() + Log.d(STEP_TAG,"Navigate to 'Files' menu in user left-side menu.") + leftSideNavigationDrawerPage.clickFilesMenu() Log.d(STEP_TAG,"Assert that there is a directory called 'unfiled' is displayed.") fileListPage.assertItemDisplayed("unfiled") // Our discussion attachment goes under "unfiled" @@ -153,8 +159,8 @@ class FilesE2ETest: TeacherTest() { Log.d(STEP_TAG,"Navigate back to Dashboard Page.") ViewUtils.pressBackButton(5) - Log.d(STEP_TAG,"Navigate to 'Files' menu in user left-side menubar.") - dashboardPage.gotoGlobalFiles() + Log.d(STEP_TAG,"Navigate to 'Files' menu in user left-side menu.") + leftSideNavigationDrawerPage.clickFilesMenu() Log.d(STEP_TAG,"Assert that there is a directory called 'unfiled' is displayed.") fileListPage.assertItemDisplayed("unfiled") diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/LoginE2ETest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/LoginE2ETest.kt index 5233e60679..640a310972 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/LoginE2ETest.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/LoginE2ETest.kt @@ -52,22 +52,22 @@ class LoginE2ETest : TeacherTest() { loginWithUser(teacher1) Log.d(STEP_TAG,"Assert that the Dashboard Page is the landing page and it is loaded successfully.") - verifyDashboardPage(teacher1) + assertSuccessfulLogin(teacher1) Log.d(STEP_TAG,"Validate ${teacher1.name} user's role as a Teacher.") validateUserRole(teacher1, course, "Teacher") Log.d(STEP_TAG,"Log out with ${teacher1.name} student.") - dashboardPage.logOut() + leftSideNavigationDrawerPage.logout() Log.d(STEP_TAG, "Login with user: ${teacher2.name}, login id: ${teacher2.loginId}.") loginWithUser(teacher2, true) Log.d(STEP_TAG,"Assert that the Dashboard Page is the landing page and it is loaded successfully.") - verifyDashboardPage(teacher2) + assertSuccessfulLogin(teacher2) Log.d(STEP_TAG,"Click on 'Change User' button on the left-side menu.") - dashboardPage.pressChangeUser() + leftSideNavigationDrawerPage.clickChangeUserMenu() Log.d(STEP_TAG,"Assert that the previously logins has been displayed.") loginLandingPage.assertDisplaysPreviousLogins() @@ -76,10 +76,10 @@ class LoginE2ETest : TeacherTest() { loginWithUser(teacher1, true) Log.d(STEP_TAG,"Assert that the Dashboard Page is the landing page and it is loaded successfully.") - verifyDashboardPage(teacher1) + assertSuccessfulLogin(teacher1) Log.d(STEP_TAG,"Click on 'Change User' button on the left-side menu.") - dashboardPage.pressChangeUser() + leftSideNavigationDrawerPage.clickChangeUserMenu() Log.d(STEP_TAG,"Assert that the previously logins has been displayed.") loginLandingPage.assertDisplaysPreviousLogins() @@ -88,7 +88,7 @@ class LoginE2ETest : TeacherTest() { loginLandingPage.loginWithPreviousUser(teacher2) Log.d(STEP_TAG,"Assert that the Dashboard Page is the landing page and it is loaded successfully.") - verifyDashboardPage(teacher2) + assertSuccessfulLogin(teacher2) } @E2E @@ -145,17 +145,16 @@ class LoginE2ETest : TeacherTest() { loginWithUser(teacher1) Log.d(STEP_TAG, "Assert that the Dashboard Page is the landing page and it is loaded successfully.") - verifyDashboardPage(teacher1) + assertSuccessfulLogin(teacher1) Log.d(STEP_TAG, "Log out with ${teacher1.name} student.") - dashboardPage.logOut() + leftSideNavigationDrawerPage.logout() Log.d(STEP_TAG, "Login with user: ${teacher2.name}, login id: ${teacher2.loginId}, via the last saved school's button.") loginWithLastSavedSchool(teacher2) Log.d(STEP_TAG, "Assert that the Dashboard Page is the landing page and it is loaded successfully.") - verifyDashboardPage(teacher2) - + assertSuccessfulLogin(teacher2) } private fun loginWithUser(user: CanvasUserApiModel, lastSchoolSaved: Boolean = false) { @@ -201,10 +200,10 @@ class LoginE2ETest : TeacherTest() { ViewUtils.pressBackButton(2) } - private fun verifyDashboardPage(user: CanvasUserApiModel) + private fun assertSuccessfulLogin(user: CanvasUserApiModel) { dashboardPage.waitForRender() - dashboardPage.assertUserLoggedIn(user) + leftSideNavigationDrawerPage.assertUserLoggedIn(user) dashboardPage.assertDisplaysCourses() dashboardPage.assertPageObjects() } diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/SettingsE2ETest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/SettingsE2ETest.kt index aa824c9647..ff35dc9f9d 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/SettingsE2ETest.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/SettingsE2ETest.kt @@ -53,7 +53,7 @@ class SettingsE2ETest : TeacherTest() { dashboardPage.waitForRender() Log.d(STEP_TAG, "Navigate to User Settings Page.") - dashboardPage.openUserSettingsPage() + leftSideNavigationDrawerPage.clickSettingsMenu() settingsPage.assertPageObjects() Log.d(STEP_TAG, "Open Profile Settings Page.") @@ -119,7 +119,7 @@ class SettingsE2ETest : TeacherTest() { dashboardPage.waitForRender() Log.d(STEP_TAG, "Navigate to User Settings Page.") - dashboardPage.openUserSettingsPage() + leftSideNavigationDrawerPage.clickSettingsMenu() settingsPage.assertPageObjects() Log.d(STEP_TAG,"Navigate to Settings Page and open App Theme Settings.") @@ -139,9 +139,9 @@ class SettingsE2ETest : TeacherTest() { courseBrowserPage.assertTabLabelTextColor("Announcements","#FFFFFFFF") courseBrowserPage.assertTabLabelTextColor("Assignments","#FFFFFFFF") - Log.d(STEP_TAG,"Navigate to Settins Page and open App Theme Settings again.") + Log.d(STEP_TAG,"Navigate to Settings Page and open App Theme Settings again.") Espresso.pressBack() - dashboardPage.openUserSettingsPage() + leftSideNavigationDrawerPage.clickSettingsMenu() settingsPage.openAppThemeSettings() Log.d(STEP_TAG,"Select Light App Theme and assert that the App Theme Title and Status has the proper text color (which is used in Light mode).") @@ -168,7 +168,7 @@ class SettingsE2ETest : TeacherTest() { dashboardPage.waitForRender() Log.d(STEP_TAG,"Navigate to User Settings Page.") - dashboardPage.openUserSettingsPage() + leftSideNavigationDrawerPage.clickSettingsMenu() settingsPage.assertPageObjects() Log.d(STEP_TAG,"Open Legal Page and assert that all the corresponding buttons are displayed.") @@ -176,6 +176,40 @@ class SettingsE2ETest : TeacherTest() { legalPage.assertPageObjects() } + @E2E + @Test + @TestMetaData(Priority.IMPORTANT, FeatureCategory.SETTINGS, TestCategory.E2E) + fun testAboutE2E() { + + Log.d(PREPARATION_TAG, "Seeding data.") + val data = seedData(students = 1, teachers = 1, courses = 1) + val teacher = data.teachersList[0] + + Log.d(STEP_TAG, "Login with user: ${teacher.name}, login id: ${teacher.loginId}.") + tokenLogin(teacher) + dashboardPage.waitForRender() + + Log.d(STEP_TAG, "Navigate to Settings Page on the left-side menu.") + leftSideNavigationDrawerPage.clickSettingsMenu() + settingsPage.assertPageObjects() + + Log.d(STEP_TAG, "Click on 'About' link to open About Page. Assert that About Page has opened.") + settingsPage.openAboutPage() + aboutPage.assertPageObjects() + + Log.d(STEP_TAG,"Check that domain is equal to: ${teacher.domain} (teacher's domain).") + aboutPage.domainIs(teacher.domain) + + Log.d(STEP_TAG,"Check that Login ID is equal to: ${teacher.loginId} (teacher's Login ID).") + aboutPage.loginIdIs(teacher.loginId) + + Log.d(STEP_TAG,"Check that e-mail is equal to: ${teacher.loginId} (teacher's Login ID).") + aboutPage.emailIs(teacher.loginId) + + Log.d(STEP_TAG,"Assert that the Instructure company logo has been displayed on the About page.") + aboutPage.assertInstructureLogoDisplayed() + } + @E2E @Test @TestMetaData(Priority.MANDATORY, FeatureCategory.SETTINGS, TestCategory.E2E) @@ -190,7 +224,7 @@ class SettingsE2ETest : TeacherTest() { dashboardPage.waitForRender() Log.d(STEP_TAG,"Navigate to User Settings Page.") - dashboardPage.openUserSettingsPage() + leftSideNavigationDrawerPage.clickSettingsMenu() settingsPage.assertPageObjects() Log.d(STEP_TAG,"Open Legal Page and assert that all the corresponding buttons are displayed.") @@ -215,7 +249,7 @@ class SettingsE2ETest : TeacherTest() { dashboardPage.waitForRender() Log.d(STEP_TAG,"Navigate to User Settings Page.") - dashboardPage.openUserSettingsPage() + leftSideNavigationDrawerPage.clickSettingsMenu() Log.d(PREPARATION_TAG,"Capture the initial remote config values.") val initialValues = mutableMapOf() diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/AboutPage.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/AboutPage.kt new file mode 100644 index 0000000000..8a0827370b --- /dev/null +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/AboutPage.kt @@ -0,0 +1,51 @@ +/* + * 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.ui.pages + +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.matcher.ViewMatchers.withId +import com.instructure.espresso.OnViewWithText +import com.instructure.espresso.assertDisplayed +import com.instructure.espresso.page.BasePage +import com.instructure.espresso.page.plus +import com.instructure.espresso.page.withText +import com.instructure.espresso.scrollTo +import com.instructure.teacher.R + +class AboutPage : BasePage(R.id.aboutPage) { + + private val domainLabel by OnViewWithText(R.string.domain) + private val loginIdLabel by OnViewWithText(R.string.loginId) + private val emailLabel by OnViewWithText(R.string.email) + + fun domainIs(domain: String) { + onView(withId(R.id.domain) + withText(domain)).assertDisplayed() + + } + + fun loginIdIs(loginId: String) { + onView(withId(R.id.loginId) + withText(loginId)).assertDisplayed() + } + + fun emailIs(email: String) { + onView(withId(R.id.email) + withText(email)).assertDisplayed() + } + + fun assertInstructureLogoDisplayed() { + onView(withId(R.id.instructureLogo)).scrollTo().assertDisplayed() + } +} \ No newline at end of file diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/DashboardPage.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/DashboardPage.kt index 22b030fad1..a1aff9f7cc 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/DashboardPage.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/DashboardPage.kt @@ -17,19 +17,31 @@ package com.instructure.teacher.ui.pages import android.view.View -import androidx.test.espresso.Espresso import androidx.test.espresso.assertion.ViewAssertions.doesNotExist import androidx.test.espresso.assertion.ViewAssertions.matches -import androidx.test.espresso.matcher.ViewMatchers import androidx.test.espresso.matcher.ViewMatchers.isDisplayed import androidx.test.espresso.matcher.ViewMatchers.withContentDescription -import com.instructure.canvas.espresso.waitForMatcherWithSleeps import com.instructure.canvasapi2.models.Course -import com.instructure.canvasapi2.models.User -import com.instructure.dataseeding.model.CanvasUserApiModel import com.instructure.dataseeding.model.CourseApiModel -import com.instructure.espresso.* -import com.instructure.espresso.page.* +import com.instructure.espresso.OnViewWithId +import com.instructure.espresso.TextViewColorAssertion +import com.instructure.espresso.WaitForViewWithId +import com.instructure.espresso.WaitForViewWithText +import com.instructure.espresso.assertContainsText +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.waitForView +import com.instructure.espresso.page.withAncestor +import com.instructure.espresso.page.withDescendant +import com.instructure.espresso.page.withId +import com.instructure.espresso.page.withParent +import com.instructure.espresso.page.withText +import com.instructure.espresso.replaceText +import com.instructure.espresso.waitForCheck import com.instructure.teacher.R import org.hamcrest.CoreMatchers.allOf import org.hamcrest.Matcher @@ -136,52 +148,10 @@ class DashboardPage : BasePage() { todoTab.click() } - fun openUserSettingsPage() { - onView(hamburgerButtonMatcher).click() - onViewWithId(R.id.navigationDrawerSettings).click() - } - - fun gotoGlobalFiles() { - onView(hamburgerButtonMatcher).click() - onViewWithId(R.id.navigationDrawerItem_files).click() - } - fun assertCourseLabelTextColor(expectedTextColor: String) { onView(withId(R.id.courseLabel)).check(TextViewColorAssertion(expectedTextColor)) } - fun logOut() { - onView(hamburgerButtonMatcher).click() - onViewWithId(R.id.navigationDrawerItem_logout).scrollTo().click() - onViewWithText(android.R.string.yes).click() - // It can potentially take a long time for the sign-out to take effect, especially on - // slow FTL devices. So let's pause for a bit until we see the canvas logo. - waitForMatcherWithSleeps(ViewMatchers.withId(R.id.canvasLogo), 10000).check(matches(isDisplayed())) - } - - fun assertUserLoggedIn(user: CanvasUserApiModel) { - onView(hamburgerButtonMatcher).click() - onViewWithText(user.shortName).assertDisplayed() - Espresso.pressBack() - } - - fun assertUserLoggedIn(user: User) { - onView(hamburgerButtonMatcher).click() - onViewWithText(user.shortName!!).assertDisplayed() - Espresso.pressBack() - } - - fun assertUserLoggedIn(userName: String) { - onView(hamburgerButtonMatcher).click() - onViewWithText(userName).assertDisplayed() - Espresso.pressBack() - } - - fun pressChangeUser() { - onView(hamburgerButtonMatcher).click() - onViewWithId(R.id.navigationDrawerItem_changeUser).scrollTo().click() - } - fun selectCourse(course: CourseApiModel) { assertDisplaysCourse(course) onView(withText(course.name)).click() @@ -202,8 +172,4 @@ class DashboardPage : BasePage() { onView(withText(R.string.ok) + withAncestor(R.id.buttonPanel)).click() } - fun openHelpMenu() { - onView(hamburgerButtonMatcher).click() - onViewWithId(R.id.navigationDrawerItem_help).scrollTo().click() - } } \ No newline at end of file diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/InboxPage.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/InboxPage.kt index a3bd1bd3bc..7a32b23584 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/InboxPage.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/InboxPage.kt @@ -12,8 +12,25 @@ import com.instructure.canvas.espresso.waitForMatcherWithRefreshes import com.instructure.canvas.espresso.withCustomConstraints import com.instructure.canvasapi2.models.Conversation import com.instructure.dataseeding.model.ConversationApiModel -import com.instructure.espresso.* -import com.instructure.espresso.page.* +import com.instructure.espresso.OnViewWithId +import com.instructure.espresso.RecyclerViewItemCountGreaterThanAssertion +import com.instructure.espresso.WaitForViewWithId +import com.instructure.espresso.assertDisplayed +import com.instructure.espresso.assertVisibility +import com.instructure.espresso.click +import com.instructure.espresso.longClick +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.waitForViewWithId +import com.instructure.espresso.page.waitForViewWithText +import com.instructure.espresso.page.withAncestor +import com.instructure.espresso.page.withId +import com.instructure.espresso.page.withText +import com.instructure.espresso.scrollTo +import com.instructure.espresso.swipeLeft +import com.instructure.espresso.swipeRight import com.instructure.teacher.R import com.instructure.teacher.ui.utils.WaitForToolbarTitle import org.hamcrest.Matchers @@ -69,7 +86,6 @@ class InboxPage: BasePage() { } fun filterInbox(filterFor: String) { - refresh() waitForView(withId(R.id.scopeFilterText)) onView(withId(R.id.scopeFilter)).click() waitForViewWithText(filterFor).click() diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/LeftSideNavigationDrawerPage.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/LeftSideNavigationDrawerPage.kt new file mode 100644 index 0000000000..fdb44c0cd2 --- /dev/null +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/LeftSideNavigationDrawerPage.kt @@ -0,0 +1,105 @@ +package com.instructure.teacher.ui.pages + +import android.view.View +import androidx.appcompat.widget.SwitchCompat +import androidx.test.espresso.Espresso +import androidx.test.espresso.UiController +import androidx.test.espresso.ViewAction +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.matcher.ViewMatchers +import com.instructure.canvas.espresso.waitForMatcherWithSleeps +import com.instructure.dataseeding.model.CanvasUserApiModel +import com.instructure.espresso.OnViewWithContentDescription +import com.instructure.espresso.OnViewWithId +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.onViewWithId +import com.instructure.espresso.page.onViewWithText +import com.instructure.espresso.scrollTo +import com.instructure.teacher.R +import org.hamcrest.CoreMatchers +import org.hamcrest.Matcher + +class LeftSideNavigationDrawerPage: BasePage() { + + private val userName by OnViewWithId(R.id.navigationDrawerUserName) + private val userEmail by OnViewWithId(R.id.navigationDrawerUserEmail) + private val logoutButton by OnViewWithId(R.id.navigationDrawerItem_logout) + private val version by OnViewWithId(R.id.navigationDrawerVersion) + private val hamburgerButton by OnViewWithContentDescription(R.string.navigation_drawer_open) + + // Sometimes when we navigate back to the dashboard page, there can be several hamburger buttons + // in the UI stack. We want to choose the one that is displayed. + private val hamburgerButtonMatcher = CoreMatchers.allOf( + ViewMatchers.withContentDescription(R.string.navigation_drawer_open), + ViewMatchers.isDisplayed() + ) + + private fun clickMenu(menuId: Int) { + onView(hamburgerButtonMatcher).click() + onViewWithId(menuId).scrollTo().click() + } + + fun logout() { + onView(hamburgerButtonMatcher).click() + logoutButton.scrollTo().click() + onViewWithText(android.R.string.yes).click() + // It can potentially take a long time for the sign-out to take effect, especially on + // slow FTL devices. So let's pause for a bit until we see the canvas logo. + waitForMatcherWithSleeps(ViewMatchers.withId(R.id.canvasLogo), 20000).check(matches( + ViewMatchers.isDisplayed() + )) + } + + fun clickChangeUserMenu() { + clickMenu(R.id.navigationDrawerItem_changeUser) + } + + fun clickHelpMenu() { + clickMenu(R.id.navigationDrawerItem_help) + } + + fun clickFilesMenu() { + clickMenu(R.id.navigationDrawerItem_files) + } + + fun clickSettingsMenu() { + clickMenu(R.id.navigationDrawerSettings) + } + + fun setColorOverlay(colorOverlay: Boolean) { + hamburgerButton.click() + onViewWithId(R.id.navigationDrawerColorOverlaySwitch).perform(SetSwitchCompat(colorOverlay)) + Espresso.pressBack() + } + + fun assertUserLoggedIn(user: CanvasUserApiModel) { + onView(hamburgerButtonMatcher).click() + onViewWithText(user.shortName).assertDisplayed() + Espresso.pressBack() + } + + /** + * Custom ViewAction to set a SwitchCompat to the desired on/off position + * [position]: true -> "on", false -> "off" + */ + private class SetSwitchCompat(val position: Boolean) : ViewAction { + override fun getDescription(): String { + val desiredPosition = if(position) "On" else "Off" + return "Set SwitchCompat to $desiredPosition" + } + + override fun getConstraints(): Matcher { + return ViewMatchers.isAssignableFrom(SwitchCompat::class.java) + } + + override fun perform(uiController: UiController?, view: View?) { + val switch = view as SwitchCompat + if(switch != null) { + switch.isChecked = position + } + } + } +} diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/SettingsPage.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/SettingsPage.kt index 8cb53c1ad8..025ac01352 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/SettingsPage.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/SettingsPage.kt @@ -22,7 +22,11 @@ import androidx.test.espresso.matcher.ViewMatchers import com.instructure.espresso.OnViewWithId import com.instructure.espresso.TextViewColorAssertion import com.instructure.espresso.click -import com.instructure.espresso.page.* +import com.instructure.espresso.page.BasePage +import com.instructure.espresso.page.onView +import com.instructure.espresso.page.plus +import com.instructure.espresso.page.withParent +import com.instructure.espresso.page.withText import com.instructure.espresso.scrollTo import com.instructure.teacher.R @@ -32,6 +36,7 @@ class SettingsPage : BasePage(R.id.settingsPage) { private val pushNotificationsLabel by OnViewWithId(R.id.notificationPreferenesButton) private val rateAppLabel by OnViewWithId(R.id.rateButton) private val legalLabel by OnViewWithId(R.id.legalButton) + private val aboutLabel by OnViewWithId(R.id.aboutButton) private val featureFlagLabel by OnViewWithId(R.id.featureFlagButton) private val remoteConfigLabel by OnViewWithId(R.id.remoteConfigButton) private val appThemeTitle by OnViewWithId(R.id.appThemeTitle) @@ -53,6 +58,10 @@ class SettingsPage : BasePage(R.id.settingsPage) { legalLabel.scrollTo().click() } + fun openAboutPage() { + aboutLabel.scrollTo().click() + } + fun openFeatureFlagsPage() { featureFlagLabel.scrollTo().click() } diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/utils/TeacherTest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/utils/TeacherTest.kt index 3b190d5ac8..d80b8e4783 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/utils/TeacherTest.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/utils/TeacherTest.kt @@ -27,15 +27,67 @@ import com.instructure.canvas.espresso.CanvasTest import com.instructure.espresso.InstructureActivityTestRule import com.instructure.teacher.BuildConfig import com.instructure.teacher.activities.LoginActivity -import com.instructure.teacher.ui.espresso.TeacherHiltTestApplication import com.instructure.teacher.ui.espresso.TeacherHiltTestApplication_Application -import com.instructure.teacher.ui.pages.* +import com.instructure.teacher.ui.pages.AboutPage +import com.instructure.teacher.ui.pages.AddMessagePage +import com.instructure.teacher.ui.pages.AnnouncementsListPage +import com.instructure.teacher.ui.pages.AssigneeListPage +import com.instructure.teacher.ui.pages.AssignmentDetailsPage +import com.instructure.teacher.ui.pages.AssignmentDueDatesPage +import com.instructure.teacher.ui.pages.AssignmentListPage +import com.instructure.teacher.ui.pages.AssignmentSubmissionListPage +import com.instructure.teacher.ui.pages.CalendarEventPage +import com.instructure.teacher.ui.pages.ChooseRecipientsPage +import com.instructure.teacher.ui.pages.CommentLibraryPage +import com.instructure.teacher.ui.pages.CourseBrowserPage +import com.instructure.teacher.ui.pages.CourseSettingsPage +import com.instructure.teacher.ui.pages.DashboardPage +import com.instructure.teacher.ui.pages.DiscussionsDetailsPage +import com.instructure.teacher.ui.pages.DiscussionsListPage +import com.instructure.teacher.ui.pages.EditAnnouncementPage +import com.instructure.teacher.ui.pages.EditAssignmentDetailsPage +import com.instructure.teacher.ui.pages.EditDashboardPage +import com.instructure.teacher.ui.pages.EditDiscussionsDetailsPage +import com.instructure.teacher.ui.pages.EditPageDetailsPage +import com.instructure.teacher.ui.pages.EditProfileSettingsPage +import com.instructure.teacher.ui.pages.EditQuizDetailsPage +import com.instructure.teacher.ui.pages.EditSyllabusPage +import com.instructure.teacher.ui.pages.FileListPage +import com.instructure.teacher.ui.pages.HelpPage +import com.instructure.teacher.ui.pages.InboxMessagePage +import com.instructure.teacher.ui.pages.InboxPage +import com.instructure.teacher.ui.pages.LeftSideNavigationDrawerPage +import com.instructure.teacher.ui.pages.LegalPage +import com.instructure.teacher.ui.pages.LoginFindSchoolPage +import com.instructure.teacher.ui.pages.LoginLandingPage +import com.instructure.teacher.ui.pages.LoginSignInPage +import com.instructure.teacher.ui.pages.ModulesPage +import com.instructure.teacher.ui.pages.NavDrawerPage +import com.instructure.teacher.ui.pages.NotATeacherPage +import com.instructure.teacher.ui.pages.PageListPage +import com.instructure.teacher.ui.pages.PeopleListPage +import com.instructure.teacher.ui.pages.PersonContextPage +import com.instructure.teacher.ui.pages.PostSettingsPage +import com.instructure.teacher.ui.pages.ProfileSettingsPage +import com.instructure.teacher.ui.pages.QuizDetailsPage +import com.instructure.teacher.ui.pages.QuizListPage +import com.instructure.teacher.ui.pages.QuizSubmissionListPage +import com.instructure.teacher.ui.pages.RemoteConfigSettingsPage +import com.instructure.teacher.ui.pages.SettingsPage +import com.instructure.teacher.ui.pages.SpeedGraderCommentsPage +import com.instructure.teacher.ui.pages.SpeedGraderFilesPage +import com.instructure.teacher.ui.pages.SpeedGraderGradePage +import com.instructure.teacher.ui.pages.SpeedGraderPage +import com.instructure.teacher.ui.pages.SpeedGraderQuizSubmissionPage +import com.instructure.teacher.ui.pages.StudentContextPage +import com.instructure.teacher.ui.pages.SyllabusPage +import com.instructure.teacher.ui.pages.TodoPage +import com.instructure.teacher.ui.pages.WebViewLoginPage import dagger.hilt.android.testing.HiltAndroidRule import instructure.rceditor.RCETextEditor import org.hamcrest.Matcher import org.junit.Before import org.junit.Rule -import java.lang.IllegalStateException import javax.inject.Inject abstract class TeacherTest : CanvasTest() { @@ -82,10 +134,12 @@ abstract class TeacherTest : CanvasTest() { val courseBrowserPage = CourseBrowserPage() val courseSettingsPage = CourseSettingsPage() val dashboardPage = DashboardPage() + val leftSideNavigationDrawerPage = LeftSideNavigationDrawerPage() val editDashboardPage = EditDashboardPage() val settingsPage = SettingsPage() val legalPage = LegalPage() val helpPage = HelpPage() + val aboutPage = AboutPage() val remoteConfigSettingsPage = RemoteConfigSettingsPage() val profileSettingsPage = ProfileSettingsPage() val editProfileSettingsPage = EditProfileSettingsPage() diff --git a/apps/teacher/src/main/AndroidManifest.xml b/apps/teacher/src/main/AndroidManifest.xml index 9c9441cc12..6c80bd89e7 100644 --- a/apps/teacher/src/main/AndroidManifest.xml +++ b/apps/teacher/src/main/AndroidManifest.xml @@ -25,6 +25,7 @@ + + + . + * + */ + +package com.instructure.teacher.features.shareextension + +import com.instructure.pandautils.features.shareextension.ShareExtensionActivity +import com.instructure.teacher.activities.SplashActivity + +class TeacherShareExtensionActivity : ShareExtensionActivity() { + + override fun exitActivity() { + val intent = SplashActivity.createIntent(this, intent?.extras) + startActivity(intent) + finish() + } +} \ No newline at end of file diff --git a/apps/teacher/src/main/java/com/instructure/teacher/features/shareextension/TeacherShareExtensionRouter.kt b/apps/teacher/src/main/java/com/instructure/teacher/features/shareextension/TeacherShareExtensionRouter.kt index 047d235a67..f1148efd6b 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/features/shareextension/TeacherShareExtensionRouter.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/features/shareextension/TeacherShareExtensionRouter.kt @@ -3,11 +3,14 @@ package com.instructure.teacher.features.shareextension import android.content.Context import android.content.Intent import com.instructure.pandautils.features.shareextension.ShareExtensionRouter +import com.instructure.pandautils.features.shareextension.WORKER_ID import java.util.* class TeacherShareExtensionRouter : ShareExtensionRouter { override fun routeToProgressScreen(context: Context, workerId: UUID): Intent { - return Intent() + val intent = Intent(context, TeacherShareExtensionActivity::class.java) + intent.putExtra(WORKER_ID, workerId) + return intent } } \ No newline at end of file diff --git a/apps/teacher/src/main/java/com/instructure/teacher/fragments/AttendanceListFragment.kt b/apps/teacher/src/main/java/com/instructure/teacher/fragments/AttendanceListFragment.kt index 38a10a0099..b47e81151a 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/fragments/AttendanceListFragment.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/fragments/AttendanceListFragment.kt @@ -103,7 +103,7 @@ class AttendanceListFragment : BaseSyncFragment< } private fun setupViews() = with(binding) { - webView.setDarkModeSupport() + webView.enableAlgorithmicDarkening() toolbar.setupMenu(R.menu.menu_attendance) { menuItem -> when(menuItem.itemId) { R.id.menuFilterSections -> { /* Do Nothing */ } diff --git a/apps/teacher/src/main/java/com/instructure/teacher/fragments/InternalWebViewFragment.kt b/apps/teacher/src/main/java/com/instructure/teacher/fragments/InternalWebViewFragment.kt index 18529ed09b..dfe4eb51aa 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/fragments/InternalWebViewFragment.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/fragments/InternalWebViewFragment.kt @@ -124,7 +124,6 @@ open class InternalWebViewFragment : BaseFragment() { setupToolbar(courseBackgroundColor) - canvasWebView.setDarkModeSupport(webThemeDarkeningOnly = true) canvasWebView.settings.loadWithOverviewMode = true canvasWebView.settings.displayZoomControls = false canvasWebView.settings.setSupportZoom(true) diff --git a/apps/teacher/src/main/java/com/instructure/teacher/fragments/QuizPreviewWebviewFragment.kt b/apps/teacher/src/main/java/com/instructure/teacher/fragments/QuizPreviewWebviewFragment.kt index 45a7c84a3b..8e14adebb4 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/fragments/QuizPreviewWebviewFragment.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/fragments/QuizPreviewWebviewFragment.kt @@ -24,7 +24,7 @@ import com.instructure.canvasapi2.utils.ApiPrefs import com.instructure.pandautils.analytics.SCREEN_VIEW_QUIZ_PREVIEW import com.instructure.pandautils.analytics.ScreenView import com.instructure.pandautils.utils.ViewStyler -import com.instructure.pandautils.utils.setDarkModeSupport +import com.instructure.pandautils.utils.enableAlgorithmicDarkening import com.instructure.pandautils.utils.setGone import com.instructure.pandautils.utils.setVisible import com.instructure.pandautils.views.CanvasWebView @@ -56,7 +56,7 @@ class QuizPreviewWebviewFragment : InternalWebViewFragment() { override fun onActivityCreated(savedInstanceState: Bundle?) = with(binding) { setShouldLoadUrl(false) super.onActivityCreated(savedInstanceState) - canvasWebView.setDarkModeSupport() + canvasWebView.enableAlgorithmicDarkening() canvasWebView.canvasWebViewClientCallback = object : CanvasWebView.CanvasWebViewClientCallback { override fun openMediaFromWebView(mime: String, url: String, filename: String) = diff --git a/apps/teacher/src/main/java/com/instructure/teacher/fragments/SettingsFragment.kt b/apps/teacher/src/main/java/com/instructure/teacher/fragments/SettingsFragment.kt index 4ab0234adf..d2baca49ea 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/fragments/SettingsFragment.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/fragments/SettingsFragment.kt @@ -23,6 +23,7 @@ import com.instructure.pandautils.analytics.SCREEN_VIEW_SETTINGS import com.instructure.pandautils.analytics.ScreenView import com.instructure.pandautils.binding.viewBinding import com.instructure.pandautils.dialogs.RatingDialog +import com.instructure.pandautils.features.about.AboutFragment import com.instructure.pandautils.features.notification.preferences.EmailNotificationPreferencesFragment import com.instructure.pandautils.features.notification.preferences.PushNotificationPreferencesFragment import com.instructure.pandautils.fragments.BasePresenterFragment @@ -56,6 +57,7 @@ class SettingsFragment : BasePresenterFragment + + { + fun seedAssignmentSubmission(request: SubmissionSeedRequest) : List { val submissionsList = mutableListOf() with(request) { for (seed in submissionSeedsList) { @@ -157,7 +157,7 @@ object SubmissionsApi { // // https://github.com/instructure/mobile_qa/blob/7f985a08161f457e9b5d60987bd6278d21e2557e/SoSeedy/lib/so_seedy/canvas_models/account_admin.rb#L357-L359 Thread.sleep(1000) - var submission = SubmissionsApi.submitCourseAssignment( + var submission = submitCourseAssignment( submissionType = seed.submissionType, courseId = courseId, assignmentId = assignmentId, @@ -169,7 +169,7 @@ object SubmissionsApi { val maxAttempts = 6 var attempts = 1 while (attempts < maxAttempts) { - val submissionResponse = SubmissionsApi.getSubmission ( + val submissionResponse = getSubmission ( studentToken = studentToken, courseId = courseId, assignmentId = assignmentId, @@ -185,7 +185,7 @@ object SubmissionsApi { submission = commentSeedsList .map { // Create comments with any assigned upload file types - val assignment = SubmissionsApi.commentOnSubmission( + val assignment = commentOnSubmission( studentToken = studentToken, courseId = courseId, assignmentId = assignmentId, diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/MockCanvas.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/MockCanvas.kt index 6bbf5d8b56..5eef33883e 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/MockCanvas.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/MockCanvas.kt @@ -36,6 +36,7 @@ import okhttp3.mockwebserver.MockWebServer import okhttp3.mockwebserver.RecordedRequest import org.threeten.bp.OffsetDateTime import java.util.* +import kotlin.random.Random class MockCanvas { /** Fake domain */ @@ -1086,7 +1087,7 @@ fun MockCanvas.addUser(): User { val name = Randomizer.randomName() val email = Randomizer.randomEmail() val user = User( - id = users.size + 1L, + id = Random.nextLong(), name = name.fullName, shortName = name.firstName, loginId = email, diff --git a/buildSrc/src/main/java/GlobalDependencies.kt b/buildSrc/src/main/java/GlobalDependencies.kt index 784fd11929..ba841a914f 100644 --- a/buildSrc/src/main/java/GlobalDependencies.kt +++ b/buildSrc/src/main/java/GlobalDependencies.kt @@ -4,7 +4,7 @@ object Versions { /* SDK Versions */ const val COMPILE_SDK = 33 const val MIN_SDK = 26 - const val TARGET_SDK = 31 + const val TARGET_SDK = 33 /* Build/tooling */ const val ANDROID_GRADLE_TOOLS = "7.1.3" @@ -160,7 +160,7 @@ object Libs { } object Plugins { - const val FIREBASE_CRASHLYTICS = "com.google.firebase:firebase-crashlytics-gradle:2.9.4" + const val FIREBASE_CRASHLYTICS = "com.google.firebase:firebase-crashlytics-gradle:2.9.2" const val ANDROID_GRADLE_TOOLS = "com.android.tools.build:gradle:${Versions.ANDROID_GRADLE_TOOLS}" const val APOLLO = "com.apollographql.apollo:apollo-gradle-plugin:${Versions.APOLLO}" const val KOTLIN = "org.jetbrains.kotlin:kotlin-gradle-plugin:${Versions.KOTLIN}" diff --git a/libs/DocumentScanner/build.gradle b/libs/DocumentScanner/build.gradle index 81d7ae394a..47145c3f4d 100644 --- a/libs/DocumentScanner/build.gradle +++ b/libs/DocumentScanner/build.gradle @@ -13,12 +13,12 @@ repositories { } android { - compileSdkVersion 29 - buildToolsVersion "29.0.3" + compileSdkVersion Versions.COMPILE_SDK + buildToolsVersion Versions.BUILD_TOOLS defaultConfig { minSdkVersion 21 - targetSdkVersion 29 + targetSdkVersion Versions.TARGET_SDK versionCode libraryVersionCode versionName libraryVersionName diff --git a/libs/DocumentScanner/src/main/AndroidManifest.xml b/libs/DocumentScanner/src/main/AndroidManifest.xml index 5b46a8f9ba..0934d4cb6f 100644 --- a/libs/DocumentScanner/src/main/AndroidManifest.xml +++ b/libs/DocumentScanner/src/main/AndroidManifest.xml @@ -3,7 +3,7 @@ package="com.zynksoftware.documentscanner"> - + (F private fun checkForStoragePermissions() { RxPermissions(this) - .requestEach(Manifest.permission.READ_EXTERNAL_STORAGE) + .requestEach(Manifest.permission.READ_MEDIA_IMAGES) .subscribe { permission -> when { permission.granted -> { diff --git a/libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/ui/imagecrop/ImageCropFragment.kt b/libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/ui/imagecrop/ImageCropFragment.kt index ae5543c4b4..8ab1ef6633 100644 --- a/libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/ui/imagecrop/ImageCropFragment.kt +++ b/libs/DocumentScanner/src/main/java/com/zynksoftware/documentscanner/ui/imagecrop/ImageCropFragment.kt @@ -61,7 +61,7 @@ internal class ImageCropFragment : BaseFragment(Fragme } } binding.holderImageView.post { - initializeCropping() + if (this.view != null) initializeCropping() } initListeners() diff --git a/libs/annotations/src/main/res/values-b+da+DK+instk12/strings.xml b/libs/annotations/src/main/res/values-b+da+DK+instk12/strings.xml index eb8b89980f..89cba7fc67 100644 --- a/libs/annotations/src/main/res/values-b+da+DK+instk12/strings.xml +++ b/libs/annotations/src/main/res/values-b+da+DK+instk12/strings.xml @@ -1,4 +1,4 @@ - + + + + + + + + + + + + + + + + + + diff --git a/libs/pandares/src/main/res/values-ar/strings.xml b/libs/pandares/src/main/res/values-ar/strings.xml index 4236bb56fd..57545a0bc7 100644 --- a/libs/pandares/src/main/res/values-ar/strings.xml +++ b/libs/pandares/src/main/res/values-ar/strings.xml @@ -1359,6 +1359,8 @@ جارٍ التحميل إلى الملفات جارٍ تحميل الإرسال إلى \"%s\" جارٍ تحميل الإرسال + نجح الإرسال + فشل الإرسال جارٍ تحميل الملفات إلغاء الإرسال سيؤدي هذا إلى إلغاء وحذف إرسالك. @@ -1440,4 +1442,6 @@ تم إلغاء تحديد المحادثة الخروج من وضع التحديد تم إلغاء تنشيط وضع التحديد. + صورة رمزية %s + تراجع 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 2d506f6072..d563f1aaa1 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 @@ -1,4 +1,4 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/libs/pandautils/src/main/res/layout/fragment_share_extension_progress_dialog.xml b/libs/pandautils/src/main/res/layout/fragment_share_extension_progress_dialog.xml index 7901df4c05..96dd4e1d9a 100644 --- a/libs/pandautils/src/main/res/layout/fragment_share_extension_progress_dialog.xml +++ b/libs/pandautils/src/main/res/layout/fragment_share_extension_progress_dialog.xml @@ -22,6 +22,8 @@ + + @@ -72,9 +74,8 @@ android:id="@+id/subtitle" android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_marginStart="16dp" + android:layout_marginHorizontal="16dp" android:layout_marginTop="16dp" - android:layout_marginEnd="16dp" android:text="@{viewModel.data.subtitle}" app:layout_constraintTop_toBottomOf="@id/divider" tools:text="Uploading submission for Assignment Name" /> @@ -84,12 +85,12 @@ style="?android:attr/progressBarStyleHorizontal" android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_marginStart="16dp" + android:layout_marginHorizontal="16dp" android:layout_marginTop="12dp" - android:layout_marginEnd="16dp" android:max="100" android:progress="@{viewModel.data.progressInt}" android:progressTint="@color/backgroundInfo" + android:visibility="@{viewModel.data.failed ? View.GONE : View.VISIBLE}" app:layout_constraintTop_toBottomOf="@id/subtitle" tools:progress="25" /> @@ -101,41 +102,94 @@ android:text="@{viewModel.data.percentage}" android:textColor="@color/textDarkest" android:textSize="12sp" + android:visibility="@{viewModel.data.failed ? View.GONE : View.VISIBLE}" app:layout_constraintStart_toStartOf="@id/progressBar" app:layout_constraintTop_toBottomOf="@id/progressBar" tools:text="32.7%" /> + + + + + + + app:layout_constraintTop_toBottomOf="@id/cancel" /> - + android:layout_height="0dp" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintHeight_default="wrap" + app:layout_constraintTop_toBottomOf="@id/itemsDivider"> + + + + + \ No newline at end of file diff --git a/libs/pandautils/src/main/res/layout/fragment_share_extension_status_dialog.xml b/libs/pandautils/src/main/res/layout/fragment_share_extension_status_dialog.xml index a2e08f6f22..7df43629cc 100644 --- a/libs/pandautils/src/main/res/layout/fragment_share_extension_status_dialog.xml +++ b/libs/pandautils/src/main/res/layout/fragment_share_extension_status_dialog.xml @@ -112,6 +112,7 @@ android:textAllCaps="true" android:textColor="@color/backgroundInfo" android:textSize="12sp" + android:textStyle="bold" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toBottomOf="@id/description" /> diff --git a/libs/pandautils/src/main/res/layout/item_dashboard_upload.xml b/libs/pandautils/src/main/res/layout/item_dashboard_upload.xml index 18825a34c5..cce05d263a 100644 --- a/libs/pandautils/src/main/res/layout/item_dashboard_upload.xml +++ b/libs/pandautils/src/main/res/layout/item_dashboard_upload.xml @@ -117,6 +117,7 @@ android:contentDescription="@string/dismiss" android:onClick="@{() -> itemViewModel.remove()}" android:src="@drawable/ic_close" + android:visibility="@{itemViewModel.data.uploading ? View.GONE : View.VISIBLE}" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toTopOf="parent" app:tint="@color/textDarkest" /> diff --git a/libs/pandautils/src/main/res/layout/item_file_progress.xml b/libs/pandautils/src/main/res/layout/item_file_progress.xml index adf7e2796c..8781c7e0d3 100644 --- a/libs/pandautils/src/main/res/layout/item_file_progress.xml +++ b/libs/pandautils/src/main/res/layout/item_file_progress.xml @@ -6,6 +6,8 @@ + + @@ -28,7 +30,7 @@ app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" - app:tint="@color/textDarkest" + app:tint="@{itemViewModel.data.iconTint}" tools:src="@drawable/ic_media" /> + android:visibility="@{itemViewModel.data.status == FileProgressStatus.IN_PROGRESS ? View.VISIBLE : View.GONE}" /> diff --git a/libs/pandautils/src/main/res/layout/viewholder_edit_dashboard_group.xml b/libs/pandautils/src/main/res/layout/viewholder_edit_dashboard_group.xml index 207ab456c7..39331c6200 100644 --- a/libs/pandautils/src/main/res/layout/viewholder_edit_dashboard_group.xml +++ b/libs/pandautils/src/main/res/layout/viewholder_edit_dashboard_group.xml @@ -64,6 +64,8 @@ app:layout_constraintEnd_toStartOf="@+id/openButton" app:layout_constraintStart_toEndOf="@id/favoriteButton" app:layout_constraintTop_toTopOf="parent" + app:layout_constraintBottom_toTopOf="@id/subtitle" + app:layout_constraintVertical_chainStyle="packed" tools:text="Course name" /> + + Offline Content + Manage Offline Content + Storage + %s of %s Used + Other Apps + Canvas Student + Remaining + All Courses + Sync + %d Selected + Select All + Deselect All + An error occurred while loading the content. diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/binding/BindableRecyclerViewAdapter.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/binding/BindableRecyclerViewAdapter.kt index dffc3eafb6..50e130995d 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/binding/BindableRecyclerViewAdapter.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/binding/BindableRecyclerViewAdapter.kt @@ -37,15 +37,15 @@ open class BindableRecyclerViewAdapter : RecyclerView.Adapter?, useDiffUtil: Boolean = false) { + val allItems = mutableListOf() + items?.forEach { + allItems.add(it) + if (it is GroupItemViewModel && !it.collapsed) { + allItems.addAll(it.getAllVisibleItems()) + } + } + if (useDiffUtil) { - val diffResult = DiffUtil.calculateDiff(DiffUtilCallback(itemViewModels, items ?: emptyList()), false) - itemViewModels = items.orEmpty().toMutableList() + val diffResult = DiffUtil.calculateDiff(DiffUtilCallback(itemViewModels, allItems), false) + itemViewModels = allItems.toMutableList() diffResult.dispatchUpdatesTo(this) } else { - itemViewModels = items.orEmpty().toMutableList() + itemViewModels = allItems.toMutableList() notifyDataSetChanged() } @@ -81,23 +89,19 @@ open class BindableRecyclerViewAdapter : RecyclerView.Adapter()) - } } } private fun toggleGroup(group: GroupItemViewModel) { val position = itemViewModels.indexOf(group) + val items = group.getAllVisibleItems() if (group.collapsed) { - itemViewModels.removeAll(group.items) - notifyItemRangeRemoved(position + 1, group.items.size) + itemViewModels.removeAll(items) + notifyItemRangeRemoved(position + 1, items.size) } else { - itemViewModels.addAll(position + 1, group.items) + itemViewModels.addAll(position + 1, items) setupGroups(group.items.filterIsInstance()) - notifyItemRangeInserted(position + 1, group.items.size) + notifyItemRangeInserted(position + 1, items.size) } } @@ -110,7 +114,6 @@ open class BindableRecyclerViewAdapter : RecyclerView.Adapter ) : ItemViewModel, BaseObservable() { @@ -31,4 +31,15 @@ abstract class GroupItemViewModel( collapsed = !collapsed notifyPropertyChanged(BR.collapsed) } + + fun getAllVisibleItems(): List { + val result = mutableListOf() + items.forEach { + result += it + if (it is GroupItemViewModel && !it.collapsed) { + result += it.getAllVisibleItems() + } + } + return result + } } \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/di/ApplicationModule.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/di/ApplicationModule.kt index bd947ba060..968d0df9c6 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/di/ApplicationModule.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/di/ApplicationModule.kt @@ -27,6 +27,7 @@ import com.instructure.canvasapi2.managers.OAuthManager import com.instructure.pandautils.typeface.TypefaceBehavior import com.instructure.pandautils.utils.ColorKeeper import com.instructure.pandautils.utils.HtmlContentFormatter +import com.instructure.pandautils.utils.StorageUtils import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -91,4 +92,10 @@ class ApplicationModule { fun provideCookieManager(): CookieManager { return CookieManager.getInstance() } + + @Provides + @Singleton + fun provideStorageUtils(@ApplicationContext context: Context): StorageUtils { + return StorageUtils(context) + } } \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/di/OfflineContentModule.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/di/OfflineContentModule.kt new file mode 100644 index 0000000000..daa9bb5c79 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/di/OfflineContentModule.kt @@ -0,0 +1,39 @@ +/* + * 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.pandautils.di + +import com.instructure.canvasapi2.apis.CourseAPI +import com.instructure.canvasapi2.apis.FileFolderAPI +import com.instructure.pandautils.features.offline.OfflineContentRepository +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.components.ViewModelComponent + +@Module +@InstallIn(ViewModelComponent::class) +class OfflineContentModule { + + @Provides + fun provideOfflineContentRepository( + coursesApi: CourseAPI.CoursesInterface, + filesFoldersInterface: FileFolderAPI.FilesFoldersInterface + ): OfflineContentRepository { + return OfflineContentRepository(coursesApi, filesFoldersInterface) + } +} diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/OfflineContentFragment.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/OfflineContentFragment.kt new file mode 100644 index 0000000000..fff81d5920 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/OfflineContentFragment.kt @@ -0,0 +1,95 @@ +/* + * 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.pandautils.features.offline + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import androidx.fragment.app.viewModels +import com.instructure.canvasapi2.models.CanvasContext +import com.instructure.interactions.FragmentInteractions +import com.instructure.interactions.Navigation +import com.instructure.interactions.router.Route +import com.instructure.pandautils.R +import com.instructure.pandautils.databinding.FragmentOfflineContentBinding +import com.instructure.pandautils.utils.* +import dagger.hilt.android.AndroidEntryPoint + +@AndroidEntryPoint +class OfflineContentFragment : Fragment(), FragmentInteractions { + + private val viewModel: OfflineContentViewModel by viewModels() + + private var canvasContext: CanvasContext? by NullableParcelableArg(key = Const.CANVAS_CONTEXT) + + private lateinit var binding: FragmentOfflineContentBinding + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { + binding = FragmentOfflineContentBinding.inflate(inflater, container, false).apply { + lifecycleOwner = this@OfflineContentFragment + viewModel = this@OfflineContentFragment.viewModel + } + + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + applyTheme() + + viewModel.data.observe(viewLifecycleOwner) { data -> + updateMenuText(data.selectedCount) + } + } + + override val navigation: Navigation? + get() = activity as? Navigation + + override fun title(): String = getString(R.string.offline_content_toolbar_title) + + override fun applyTheme() { + ViewStyler.themeToolbarColored(requireActivity(), binding.toolbar, ThemePrefs.primaryColor, ThemePrefs.primaryTextColor) + binding.toolbar.apply { + subtitle = canvasContext?.name ?: getString(R.string.offline_content_all_courses) + setBackgroundColor(ThemePrefs.primaryColor) + setupAsBackButton(this@OfflineContentFragment) + setMenu(R.menu.menu_offline_content) { + viewModel.toggleSelection() + } + } + } + + override fun getFragment(): Fragment = this + + private fun updateMenuText(selectedCount: Int) { + binding.toolbar.menu.items.firstOrNull()?.title = getString( + if (selectedCount > 0) R.string.offline_content_deselect_all else R.string.offline_content_select_all + ) + } + + companion object { + + fun makeRoute(canvasContext: CanvasContext? = null) = Route(OfflineContentFragment::class.java, canvasContext) + + private fun validRoute(route: Route) = route.primaryClass == OfflineContentFragment::class.java + + fun newInstance(route: Route) = if (validRoute(route)) OfflineContentFragment().withArgs(route.argsWithContext) else null + } +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/OfflineContentRepository.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/OfflineContentRepository.kt new file mode 100644 index 0000000000..a030ad3e31 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/OfflineContentRepository.kt @@ -0,0 +1,89 @@ +/* + * 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.pandautils.features.offline + +import com.instructure.canvasapi2.apis.CourseAPI +import com.instructure.canvasapi2.apis.FileFolderAPI +import com.instructure.canvasapi2.builders.RestParams +import com.instructure.canvasapi2.models.CanvasContext +import com.instructure.canvasapi2.models.Course +import com.instructure.canvasapi2.models.FileFolder +import com.instructure.canvasapi2.utils.depaginate +import com.instructure.canvasapi2.utils.hasActiveEnrollment +import com.instructure.canvasapi2.utils.isValidTerm + +class OfflineContentRepository( + private val coursesApi: CourseAPI.CoursesInterface, + private val fileFolderApi: FileFolderAPI.FilesFoldersInterface +) { + suspend fun getCourse(courseId: Long): Course { + val params = RestParams() + val courseResult = coursesApi.getCourse(courseId, params) + + return courseResult.dataOrThrow + } + + suspend fun getCourses(): List { + val params = RestParams(usePerPageQueryParam = true) + val coursesResult = coursesApi.getFirstPageCourses(params).depaginate { nextUrl -> coursesApi.next(nextUrl, params) } + + return coursesResult.dataOrThrow.filter { it.isValidTerm() && it.hasActiveEnrollment() } + } + + suspend fun getCourseFiles(courseId: Long): List { + val params = RestParams() + val rootFolderResult = fileFolderApi.getRootFolderForContext(courseId, CanvasContext.Type.COURSE.apiString, params) + + if (rootFolderResult.isFail) return emptyList() + + return getAllFiles(rootFolderResult.dataOrThrow) + } + + private suspend fun getAllFiles(folder: FileFolder): List { + val result = mutableListOf() + val subFolders = getFolders(folder) + + val currentFolderFiles = getFiles(folder) + result.addAll(currentFolderFiles) + + for (subFolder in subFolders) { + val subFolderFiles = getAllFiles(subFolder) + result.addAll(subFolderFiles) + } + + return result.filter { !it.isHidden && !it.isLocked && !it.isHiddenForUser && !it.isLockedForUser } + } + + private suspend fun getFolders(folder: FileFolder): List { + val params = RestParams() + val foldersResult = fileFolderApi.getFirstPageFolders(folder.id, params).depaginate { nextUrl -> + fileFolderApi.getNextPageFileFoldersList(nextUrl, params) + } + + return foldersResult.dataOrNull.orEmpty() + } + + private suspend fun getFiles(folder: FileFolder): List { + val params = RestParams() + val filesResult = fileFolderApi.getFirstPageFiles(folder.id, params).depaginate { nextUrl -> + fileFolderApi.getNextPageFileFoldersList(nextUrl, params) + } + + return filesResult.dataOrNull.orEmpty() + } +} diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/OfflineContentViewData.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/OfflineContentViewData.kt new file mode 100644 index 0000000000..3906fe16f7 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/OfflineContentViewData.kt @@ -0,0 +1,44 @@ +package com.instructure.pandautils.features.offline + +import androidx.databinding.BaseObservable +import com.instructure.pandautils.features.offline.itemviewmodels.CourseItemViewModel +import com.instructure.pandautils.features.offline.itemviewmodels.CourseTabViewModel +import com.instructure.pandautils.features.offline.itemviewmodels.FileViewModel + +data class OfflineContentViewData( + val storageInfo: StorageInfo, + val courseItems: List, + val selectedCount: Int +) : BaseObservable() + +data class StorageInfo(val otherAppsReservedPercent: Int, val allAppsReservedPercent: Int, val storageInfoText: String) + +data class CourseItemViewData( + val checked: Boolean, + val title: String, + val size: String, + val tabs: List +) : BaseObservable() + +data class CourseTabViewData( + val checked: Boolean, + val title: String, + val size: String, + val files: List +) : BaseObservable() + +data class FileViewData( + val checked: Boolean, + val title: String, + val size: String +) : BaseObservable() + +enum class OfflineItemViewModelType(val viewType: Int) { + COURSE(1), + COURSE_TAB(2), + FILE(3) +} + +sealed class OfflineContentAction { + +} diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/OfflineContentViewModel.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/OfflineContentViewModel.kt new file mode 100644 index 0000000000..6e7611ac01 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/OfflineContentViewModel.kt @@ -0,0 +1,194 @@ +/* + * 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.pandautils.features.offline + +import android.content.Context +import android.text.format.Formatter +import androidx.lifecycle.* +import com.instructure.canvasapi2.models.Course +import com.instructure.canvasapi2.models.FileFolder +import com.instructure.canvasapi2.models.Tab +import com.instructure.pandautils.R +import com.instructure.pandautils.features.offline.itemviewmodels.CourseItemViewModel +import com.instructure.pandautils.features.offline.itemviewmodels.CourseTabViewModel +import com.instructure.pandautils.features.offline.itemviewmodels.FileViewModel +import com.instructure.pandautils.mvvm.Event +import com.instructure.pandautils.mvvm.ViewState +import com.instructure.pandautils.utils.Const +import com.instructure.pandautils.utils.StorageUtils +import com.instructure.pandautils.utils.orDefault +import dagger.hilt.android.lifecycle.HiltViewModel +import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.launch +import javax.inject.Inject + +private val ALLOWED_TAB_IDS = listOf(Tab.ASSIGNMENTS_ID, Tab.PAGES_ID, Tab.FILES_ID) + +@HiltViewModel +class OfflineContentViewModel @Inject constructor( + savedStateHandle: SavedStateHandle, + @ApplicationContext private val context: Context, + private val offlineContentRepository: OfflineContentRepository, + private val storageUtils: StorageUtils +) : ViewModel() { + + val course = savedStateHandle.get(Const.CANVAS_CONTEXT) + + val state: LiveData + get() = _state + private val _state = MutableLiveData() + + val data: LiveData + get() = _data + private val _data = MutableLiveData() + + val events: LiveData> + get() = _events + private val _events = MutableLiveData>() + + init { + loadData() + } + + private fun loadData() { + _state.postValue(ViewState.Loading) + viewModelScope.launch { + try { + val storageInfo = getStorageInfo() + val coursesData = getCoursesData(course?.id) + val data = OfflineContentViewData(storageInfo, coursesData, 0) + _data.postValue(data) + _state.postValue(ViewState.Success) + } catch (ex: Exception) { + _state.postValue(ViewState.Error(context.getString(R.string.offline_content_loading_error))) + } + } + } + + private suspend fun getCoursesData(courseId: Long? = null): List { + val courses = if (courseId == null) { + offlineContentRepository.getCourses() + } else { + listOf(offlineContentRepository.getCourse(courseId)) + } + + return courses.map { course -> + val tabs = course.tabs?.filter { it.tabId in ALLOWED_TAB_IDS }.orEmpty() + val files = offlineContentRepository.getCourseFiles(course.id) + val size = Formatter.formatShortFileSize(context, files.sumOf { it.size }) + createCourseItemViewModel(course, size, tabs, files) + } + } + + private fun createCourseItemViewModel(course: Course, size: String, tabs: List, files: List) = CourseItemViewModel( + CourseItemViewData(false, course.name, size, tabs.map { tab -> + val isFilesTab = tab.tabId == Tab.FILES_ID + createTabViewModel(course.id, tab, if (isFilesTab) size else "", if (isFilesTab) files else emptyList()) + }), course.id, this.course == null + ) { checked, item -> + val courseViewModel = data.value?.courseItems?.find { it == item } ?: return@CourseItemViewModel + val newTabs = courseViewModel.data.tabs.map { tab -> + val newFiles = tab.data.files.map { it.copy(data = it.data.copy(checked = checked)) } + tab.copy(data = tab.data.copy(checked = checked, files = newFiles)) + } + val newCourseViewModel = courseViewModel.copy(data = item.data.copy(checked = checked, tabs = newTabs)) + val newList = _data.value?.courseItems?.map { if (it == item) newCourseViewModel else it }.orEmpty() + val selectedCount = getSelectedItemCount(newList) + _data.value = _data.value?.copy(courseItems = newList, selectedCount = selectedCount) + } + + private fun createTabViewModel(courseId: Long, tab: Tab, size: String, files: List) = CourseTabViewModel( + CourseTabViewData(false, tab.label.orEmpty(), size, files.map { fileFolder -> + createFileViewModel(fileFolder, courseId, tab.tabId) + }), courseId, tab.tabId, false + ) { checked, item -> + val courseViewModel = data.value?.courseItems?.find { it.courseId == item.courseId } ?: return@CourseTabViewModel + val tabViewModel = courseViewModel.data.tabs.find { it == item } ?: return@CourseTabViewModel + val newFiles = tabViewModel.data.files.map { it.copy(data = it.data.copy(checked = checked)) } + val newTabViewModel = tabViewModel.copy(data = tabViewModel.data.copy(checked = checked, files = newFiles)) + val newTabs = courseViewModel.data.tabs.map { if (it == item) newTabViewModel else it } + val newCourseViewModel = courseViewModel.copy(data = courseViewModel.data.copy(checked = newTabs.all { it.data.checked }, tabs = newTabs)) + val newList = _data.value?.courseItems?.map { if (it == courseViewModel) newCourseViewModel else it }.orEmpty() + val selectedCount = getSelectedItemCount(newList) + _data.value = _data.value?.copy(courseItems = newList, selectedCount = selectedCount) + } + + private fun createFileViewModel(fileFolder: FileFolder, courseId: Long, tabId: String): FileViewModel { + val fileSize = Formatter.formatShortFileSize(context, fileFolder.size) + return FileViewModel(FileViewData(false, fileFolder.displayName.orEmpty(), fileSize), courseId, tabId) { checked, item -> + val courseViewModel = data.value?.courseItems?.find { it.courseId == item.courseId } ?: return@FileViewModel + val tabViewModel = courseViewModel.data.tabs.find { it.tabId == item.tabId } ?: return@FileViewModel + val fileViewModel = tabViewModel.data.files.find { it == item } ?: return@FileViewModel + val newFile = fileViewModel.copy(data = fileViewModel.data.copy(checked = checked)) + val newFiles = tabViewModel.data.files.map { if (it == item) newFile else it } + val newTabViewModel = tabViewModel.copy(data = tabViewModel.data.copy(checked = newFiles.all { it.data.checked }, files = newFiles)) + val newTabs = courseViewModel.data.tabs.map { if (it.tabId == item.tabId) newTabViewModel else it } + val newCourseViewModel = courseViewModel.copy(data = courseViewModel.data.copy(checked = newTabs.all { it.data.checked }, tabs = newTabs)) + val newList = _data.value?.courseItems?.map { if (it == courseViewModel) newCourseViewModel else it }.orEmpty() + val selectedCount = getSelectedItemCount(newList) + _data.value = _data.value?.copy(courseItems = newList, selectedCount = selectedCount) + } + } + + private fun getSelectedItemCount(courses: List): Int { + val selectedTabs = courses.flatMap { it.data.tabs }.count { it.data.checked && it.tabId != Tab.FILES_ID } + val selectedFiles = courses.flatMap { it.data.tabs }.flatMap { it.data.files }.count { it.data.checked } + return selectedTabs + selectedFiles + } + + fun toggleSelection() { + val shouldCheck = _data.value?.selectedCount.orDefault() == 0 + val newList = _data.value?.courseItems?.map { courseItemViewModel -> + courseItemViewModel.copy(data = courseItemViewModel.data.copy( + checked = shouldCheck, + tabs = courseItemViewModel.data.tabs.map { tab -> + tab.copy(data = tab.data.copy( + checked = shouldCheck, + files = tab.data.files.map { it.copy(data = it.data.copy(checked = shouldCheck)) } + )) + } + )) + }.orEmpty() + val selectedCount = getSelectedItemCount(newList) + _data.value = _data.value?.copy(courseItems = newList, selectedCount = selectedCount) + } + + fun onSyncClicked() { + + } + + fun onRefresh() { + loadData() + } + + private fun getStorageInfo(): StorageInfo { + val appSize = storageUtils.getAppSize() + val totalSpace = storageUtils.getTotalSpace() + val usedSpace = totalSpace - storageUtils.getFreeSpace() + val otherAppsSpace = usedSpace - appSize + val otherPercent = if (totalSpace > 0) (otherAppsSpace.toFloat() / totalSpace * 100).toInt() else 0 + val canvasPercent = if (totalSpace > 0) (appSize.toFloat() / totalSpace * 100).toInt().coerceAtLeast(1) + otherPercent else 0 + val storageInfoText = context.getString( + R.string.offline_content_storage_info, + Formatter.formatShortFileSize(context, usedSpace), + Formatter.formatShortFileSize(context, totalSpace), + ) + + return StorageInfo(otherPercent, canvasPercent, storageInfoText) + } +} diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/itemviewmodels/CourseItemViewModel.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/itemviewmodels/CourseItemViewModel.kt new file mode 100644 index 0000000000..c9c1f2ef99 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/itemviewmodels/CourseItemViewModel.kt @@ -0,0 +1,50 @@ +/* + * 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.pandautils.features.offline.itemviewmodels + +import android.widget.CompoundButton.OnCheckedChangeListener +import androidx.databinding.Bindable +import com.instructure.pandautils.R +import com.instructure.pandautils.binding.GroupItemViewModel +import com.instructure.pandautils.features.offline.CourseItemViewData +import com.instructure.pandautils.features.offline.OfflineItemViewModelType +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) { + override val layoutId = R.layout.item_offline_course + override val viewType = OfflineItemViewModelType.COURSE.viewType + + val onCheckChanged = OnCheckedChangeListener { cb, checked -> + if (cb.isPressed) onCheckedChanged(checked, this) + } + + override fun areContentsTheSame(other: ItemViewModel): Boolean { + return other is CourseItemViewModel + && other.courseId == this.courseId + && other.data == this.data + } + + override fun areItemsTheSame(other: ItemViewModel): Boolean { + return other is CourseItemViewModel && other.courseId == this.courseId + } +} diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/itemviewmodels/CourseTabViewModel.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/itemviewmodels/CourseTabViewModel.kt new file mode 100644 index 0000000000..f482992bc5 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/itemviewmodels/CourseTabViewModel.kt @@ -0,0 +1,54 @@ +/* + * 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.pandautils.features.offline.itemviewmodels + +import android.widget.CompoundButton +import androidx.databinding.Bindable +import com.instructure.pandautils.R +import com.instructure.pandautils.binding.GroupItemViewModel +import com.instructure.pandautils.features.offline.CourseTabViewData +import com.instructure.pandautils.features.offline.OfflineItemViewModelType +import com.instructure.pandautils.mvvm.ItemViewModel + +data class CourseTabViewModel( + val data: CourseTabViewData, + val courseId: Long, + val tabId: String, + @get:Bindable override var collapsed: Boolean, + val onCheckedChanged: (Boolean, CourseTabViewModel) -> Unit +) : GroupItemViewModel(collapsable = true, items = data.files) { + override val layoutId = R.layout.item_offline_tab + override val viewType = OfflineItemViewModelType.COURSE_TAB.viewType + + val onCheckChanged = CompoundButton.OnCheckedChangeListener { cb, checked -> + if (cb.isPressed) onCheckedChanged(checked, this) + } + + override fun areContentsTheSame(other: ItemViewModel): Boolean { + return other is CourseTabViewModel + && other.courseId == this.courseId + && other.tabId == this.tabId + && other.data == this.data + } + + override fun areItemsTheSame(other: ItemViewModel): Boolean { + return other is CourseTabViewModel + && other.courseId == this.courseId + && other.tabId == this.tabId + } +} diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/itemviewmodels/FileViewModel.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/itemviewmodels/FileViewModel.kt new file mode 100644 index 0000000000..8597614b8a --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/itemviewmodels/FileViewModel.kt @@ -0,0 +1,51 @@ +/* + * 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.pandautils.features.offline.itemviewmodels + +import android.widget.CompoundButton +import com.instructure.pandautils.R +import com.instructure.pandautils.features.offline.FileViewData +import com.instructure.pandautils.features.offline.OfflineItemViewModelType +import com.instructure.pandautils.mvvm.ItemViewModel + +data class FileViewModel( + val data: FileViewData, + val courseId: Long, + val tabId: String, + val onCheckedChanged: (Boolean, FileViewModel) -> Unit +) : ItemViewModel { + override val layoutId = R.layout.item_offline_file + override val viewType = OfflineItemViewModelType.FILE.viewType + + val onCheckChanged = CompoundButton.OnCheckedChangeListener { cb, checked -> + if (cb.isPressed) onCheckedChanged(checked, this) + } + + override fun areContentsTheSame(other: ItemViewModel): Boolean { + return other is FileViewModel + && other.courseId == this.courseId + && other.tabId == this.tabId + && other.data == this.data + } + + override fun areItemsTheSame(other: ItemViewModel): Boolean { + return other is FileViewModel + && other.courseId == this.courseId + && other.tabId == this.tabId + } +} diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/utils/StorageUtils.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/utils/StorageUtils.kt new file mode 100644 index 0000000000..5f8119ef56 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/utils/StorageUtils.kt @@ -0,0 +1,55 @@ +/* + * 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.pandautils.utils + +import android.content.Context +import android.os.Environment +import java.io.File + +class StorageUtils(private val context: Context) { + + fun getTotalSpace(): Long { + val externalStorageDirectory = Environment.getExternalStorageDirectory() + return externalStorageDirectory.totalSpace + } + + fun getFreeSpace(): Long { + val externalStorageDirectory = Environment.getExternalStorageDirectory() + return externalStorageDirectory.freeSpace + } + + fun getAppSize(): Long { + val appInfo = context.applicationInfo + val appSize = File(appInfo.publicSourceDir).length() + val dataDirSize = getDirSize(File(appInfo.dataDir)) + val cacheDirSize = getDirSize(context.cacheDir) + val externalCacheDirSize = getDirSize(context.externalCacheDir) + val filesDirSize = getDirSize(context.filesDir) + val externalFilesDirSize = getDirSize(context.getExternalFilesDir(null)) + + return appSize + dataDirSize + cacheDirSize + externalCacheDirSize + filesDirSize + externalFilesDirSize + } + + private fun getDirSize(directory: File?): Long { + if (directory == null || !directory.isDirectory) return 0 + var size: Long = 0 + val files = directory.listFiles() ?: return 0 + for (file in files) size += if (file.isDirectory) getDirSize(file) else file.length() + return size + } +} diff --git a/libs/pandautils/src/main/res/drawable/storage_progress_bar_background.xml b/libs/pandautils/src/main/res/drawable/storage_progress_bar_background.xml new file mode 100644 index 0000000000..a4a4b5decc --- /dev/null +++ b/libs/pandautils/src/main/res/drawable/storage_progress_bar_background.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/libs/pandautils/src/main/res/layout/fragment_offline_content.xml b/libs/pandautils/src/main/res/layout/fragment_offline_content.xml new file mode 100644 index 0000000000..304bc64a0d --- /dev/null +++ b/libs/pandautils/src/main/res/layout/fragment_offline_content.xml @@ -0,0 +1,243 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +