From cd66243a29ba3786160fdd393e8174dbc3c7a57f Mon Sep 17 00:00:00 2001 From: "andras.maczak" Date: Mon, 4 Nov 2024 16:06:05 +0100 Subject: [PATCH 01/31] Release Student 7.7.0 (269) --- apps/student/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/student/build.gradle b/apps/student/build.gradle index 451ec4425e..772a40885a 100644 --- a/apps/student/build.gradle +++ b/apps/student/build.gradle @@ -40,8 +40,8 @@ android { applicationId "com.instructure.candroid" minSdkVersion Versions.MIN_SDK targetSdkVersion Versions.TARGET_SDK - versionCode = 268 - versionName = '7.6.1' + versionCode = 269 + versionName = '7.7.0' vectorDrawables.useSupportLibrary = true testInstrumentationRunner 'com.instructure.student.espresso.StudentHiltTestRunner' From d5a0696e38a1918b2fd20d15cffea14b6d8b1457 Mon Sep 17 00:00:00 2001 From: "andras.maczak" Date: Tue, 5 Nov 2024 09:57:10 +0100 Subject: [PATCH 02/31] Release Teacher 1.35.0 (72) --- apps/teacher/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/teacher/build.gradle b/apps/teacher/build.gradle index 8e42c1c34f..124a64779f 100644 --- a/apps/teacher/build.gradle +++ b/apps/teacher/build.gradle @@ -39,8 +39,8 @@ android { defaultConfig { minSdkVersion Versions.MIN_SDK targetSdkVersion Versions.TARGET_SDK - versionCode = 71 - versionName = '1.34.0' + versionCode = 72 + versionName = '1.35.0' vectorDrawables.useSupportLibrary = true testInstrumentationRunner 'com.instructure.teacher.ui.espresso.TeacherHiltTestRunner' testInstrumentationRunnerArguments disableAnalytics: 'true' From 031edba1148f986db3c4a17b5c917e024aee0d72 Mon Sep 17 00:00:00 2001 From: Tamas Kozmer <72397075+tamaskozmer@users.noreply.github.com> Date: Tue, 5 Nov 2024 10:22:08 +0100 Subject: [PATCH 03/31] [MBL-17962][Student][Teacher] New Quiz LTI Labeling (#2622) refs: MBL-17962 affects: Student, Teacher release note: none * Change icons of new quiz assignments * Changed strings depending on the type of LTI tool. * Unit test. --- .../modules/list/adapter/ModuleViewHolder.kt | 1 + .../SubmissionDetailsModels.kt | 2 +- .../SubmissionDetailsUpdate.kt | 4 ++-- .../content/LtiSubmissionViewFragment.kt | 10 +++++++++ .../layout/fragment_lti_submission_view.xml | 2 ++ .../details/AssignmentDetailsFragment.kt | 2 +- .../modules/list/ModuleListPresenter.kt | 2 +- .../teacher/holders/AssignmentViewHolder.kt | 4 ++-- .../teacher/holders/ToDoViewHolder.kt | 2 +- .../teacher/utils/AssignmentExtensions.kt | 6 ----- .../modules/list/ModuleListPresenterTest.kt | 22 +++++++++++++++++++ .../canvasapi2/models/Assignment.kt | 22 +++++++++++++------ .../canvasapi2/models/ModuleItem.kt | 2 ++ libs/pandares/src/main/res/values/strings.xml | 3 +++ .../details/AssignmentDetailsViewModel.kt | 3 ++- .../pandautils/utils/AssignmentExtensions.kt | 1 + 16 files changed, 66 insertions(+), 22 deletions(-) diff --git a/apps/student/src/main/java/com/instructure/student/features/modules/list/adapter/ModuleViewHolder.kt b/apps/student/src/main/java/com/instructure/student/features/modules/list/adapter/ModuleViewHolder.kt index f95fdb3ce7..8282540c4b 100644 --- a/apps/student/src/main/java/com/instructure/student/features/modules/list/adapter/ModuleViewHolder.kt +++ b/apps/student/src/main/java/com/instructure/student/features/modules/list/adapter/ModuleViewHolder.kt @@ -109,6 +109,7 @@ class ModuleViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { // Icon val drawableResource: Int = when { + moduleItem.quizLti -> R.drawable.ic_quiz ModuleItem.Type.Assignment.toString() .equals(moduleItem.type, ignoreCase = true) -> R.drawable.ic_assignment ModuleItem.Type.Discussion.toString() diff --git a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/SubmissionDetailsModels.kt b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/SubmissionDetailsModels.kt index 1538308c69..2e543cc714 100644 --- a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/SubmissionDetailsModels.kt +++ b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/SubmissionDetailsModels.kt @@ -93,7 +93,7 @@ sealed class SubmissionDetailsContentType { data class NoSubmissionContent(val canvasContext: CanvasContext, val assignment: Assignment, val isStudioEnabled: Boolean, val quiz: Quiz? = null, val studioLTITool: LTITool? = null, val isObserver: Boolean = false, val ltiTool: LTITool? = null) : SubmissionDetailsContentType() object NoneContent : SubmissionDetailsContentType() - data class ExternalToolContent(val canvasContext: CanvasContext, val url: String) : SubmissionDetailsContentType() + data class ExternalToolContent(val canvasContext: CanvasContext, val url: String, val newQuizLti: Boolean = false) : SubmissionDetailsContentType() object OnPaperContent : SubmissionDetailsContentType() data class UnsupportedContent(val assignmentId: Long) : SubmissionDetailsContentType() data class OtherAttachmentContent(val attachment: Attachment) : SubmissionDetailsContentType() diff --git a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/SubmissionDetailsUpdate.kt b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/SubmissionDetailsUpdate.kt index ccbdb0847b..920857c7b0 100644 --- a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/SubmissionDetailsUpdate.kt +++ b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/SubmissionDetailsUpdate.kt @@ -166,7 +166,7 @@ class SubmissionDetailsUpdate : UpdateInit SubmissionDetailsContentType.OnPaperContent Assignment.SubmissionType.EXTERNAL_TOOL.apiString in assignment?.submissionTypesRaw.orEmpty() -> { if (assignment?.isAllowedToSubmit == true) - SubmissionDetailsContentType.ExternalToolContent(canvasContext, ltiUrl?.url ?: "") + SubmissionDetailsContentType.ExternalToolContent(canvasContext, ltiUrl?.url ?: "", assignment.isNewQuizLti()) else SubmissionDetailsContentType.LockedContent } submission?.submissionType == null -> SubmissionDetailsContentType.NoSubmissionContent(canvasContext, assignment!!, isStudioEnabled!!, quiz, studioLTITool, isObserver, ltiUrl) @@ -176,7 +176,7 @@ class SubmissionDetailsUpdate : UpdateInit SubmissionDetailsContentType.ExternalToolContent( canvasContext, - submission.previewUrl.validOrNull() ?: assignment?.url?.validOrNull() ?: assignment?.htmlUrl ?: "" + submission.previewUrl.validOrNull() ?: assignment?.url?.validOrNull() ?: assignment?.htmlUrl ?: "", false ) // Text submission diff --git a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/content/LtiSubmissionViewFragment.kt b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/content/LtiSubmissionViewFragment.kt index 755c40efd6..7363e9c3f8 100644 --- a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/content/LtiSubmissionViewFragment.kt +++ b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/content/LtiSubmissionViewFragment.kt @@ -23,6 +23,7 @@ import android.view.ViewGroup import androidx.fragment.app.Fragment import com.instructure.canvasapi2.models.CanvasContext import com.instructure.pandautils.binding.viewBinding +import com.instructure.pandautils.utils.BooleanArg import com.instructure.pandautils.utils.ParcelableArg import com.instructure.pandautils.utils.StringArg import com.instructure.pandautils.utils.ViewStyler @@ -38,6 +39,7 @@ class LtiSubmissionViewFragment : Fragment() { private val binding by viewBinding(FragmentLtiSubmissionViewBinding::bind) private var canvasContext: CanvasContext by ParcelableArg() private var url: String by StringArg() + private var newQuizLti: Boolean by BooleanArg() override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { return inflater.inflate(R.layout.fragment_lti_submission_view, container, false) @@ -46,16 +48,24 @@ class LtiSubmissionViewFragment : Fragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) ViewStyler.themeButton(binding.viewLtiButton) + setUpViews() binding.viewLtiButton.onClickWithRequireNetwork { val route = LtiLaunchFragment.makeRoute(canvasContext = canvasContext, url = url) RouteMatcher.route(requireActivity(), route) } } + private fun setUpViews() { + binding.viewLtiButton.text = if (newQuizLti) getString(R.string.openTheQuizButton) else getString(R.string.openTool) + binding.ltiSubmissionTitle.text = if (newQuizLti) getString(R.string.newQuizSubmissionTitle) else getString(R.string.commentSubmissionTypeExternalTool) + binding.ltiSubmissionSubtitle.text = if (newQuizLti) getString(R.string.newQuizSubmissionSubtitle) else getString(R.string.speedGraderExternalToolMessage) + } + companion object { fun newInstance(data: ExternalToolContent) = LtiSubmissionViewFragment().apply { canvasContext = data.canvasContext url = data.url + newQuizLti = data.newQuizLti } } } diff --git a/apps/student/src/main/res/layout/fragment_lti_submission_view.xml b/apps/student/src/main/res/layout/fragment_lti_submission_view.xml index 5fdaa035c6..60157e789f 100644 --- a/apps/student/src/main/res/layout/fragment_lti_submission_view.xml +++ b/apps/student/src/main/res/layout/fragment_lti_submission_view.xml @@ -35,6 +35,7 @@ app:srcCompat="@drawable/ic_panda_online_submissions" /> { pointsPossible?.let { context.resources.getQuantityString(R.plurals.moduleItemPoints, it.toInt(), it) } val iconRes: Int? = when (tryOrNull { ModuleItem.Type.valueOf(item.type.orEmpty()) }) { - ModuleItem.Type.Assignment -> R.drawable.ic_assignment + ModuleItem.Type.Assignment -> if (item.quizLti) R.drawable.ic_quiz else R.drawable.ic_assignment ModuleItem.Type.Discussion -> R.drawable.ic_discussion ModuleItem.Type.File -> R.drawable.ic_attachment ModuleItem.Type.Page -> R.drawable.ic_pages diff --git a/apps/teacher/src/main/java/com/instructure/teacher/holders/AssignmentViewHolder.kt b/apps/teacher/src/main/java/com/instructure/teacher/holders/AssignmentViewHolder.kt index fa9492aab4..76b0a4c0bc 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/holders/AssignmentViewHolder.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/holders/AssignmentViewHolder.kt @@ -25,12 +25,12 @@ import com.instructure.canvasapi2.models.Assignment import com.instructure.canvasapi2.utils.DateHelper import com.instructure.canvasapi2.utils.NumberHelper import com.instructure.pandautils.utils.ThemePrefs +import com.instructure.pandautils.utils.getAssignmentIcon import com.instructure.pandautils.utils.setGone import com.instructure.pandautils.utils.setVisible import com.instructure.teacher.R import com.instructure.teacher.databinding.AdapterAssignmentBinding -import com.instructure.teacher.utils.getAssignmentIcon -import java.util.* +import java.util.Date class AssignmentViewHolder(private val binding: AdapterAssignmentBinding) : RecyclerView.ViewHolder(binding.root) { init { diff --git a/apps/teacher/src/main/java/com/instructure/teacher/holders/ToDoViewHolder.kt b/apps/teacher/src/main/java/com/instructure/teacher/holders/ToDoViewHolder.kt index f2be0754bf..89dba65fee 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/holders/ToDoViewHolder.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/holders/ToDoViewHolder.kt @@ -27,12 +27,12 @@ import com.instructure.canvasapi2.utils.DateHelper import com.instructure.canvasapi2.utils.NumberHelper import com.instructure.pandautils.utils.ThemePrefs import com.instructure.pandautils.utils.color +import com.instructure.pandautils.utils.getAssignmentIcon import com.instructure.pandautils.utils.setGone import com.instructure.pandautils.utils.setVisible import com.instructure.teacher.R import com.instructure.teacher.databinding.AdapterTodoBinding import com.instructure.teacher.interfaces.AdapterToFragmentCallback -import com.instructure.teacher.utils.getAssignmentIcon import java.util.Date class ToDoViewHolder(private val binding: AdapterTodoBinding) : RecyclerView.ViewHolder(binding.root) { diff --git a/apps/teacher/src/main/java/com/instructure/teacher/utils/AssignmentExtensions.kt b/apps/teacher/src/main/java/com/instructure/teacher/utils/AssignmentExtensions.kt index ac5fdae538..53b6c91d93 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/utils/AssignmentExtensions.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/utils/AssignmentExtensions.kt @@ -47,12 +47,6 @@ import com.instructure.teacher.models.DueDateGroup import java.util.ArrayList import java.util.Calendar -fun Assignment.getAssignmentIcon() = when { - Assignment.SubmissionType.ONLINE_QUIZ.apiString in submissionTypesRaw -> R.drawable.ic_quiz - Assignment.SubmissionType.DISCUSSION_TOPIC.apiString in submissionTypesRaw -> R.drawable.ic_discussion - else -> R.drawable.ic_assignment -} - fun List?.getAssignmentIcon() = when { this == null -> R.drawable.ic_assignment SubmissionType.ONLINE_QUIZ in this -> R.drawable.ic_quiz diff --git a/apps/teacher/src/test/java/com/instructure/teacher/unit/modules/list/ModuleListPresenterTest.kt b/apps/teacher/src/test/java/com/instructure/teacher/unit/modules/list/ModuleListPresenterTest.kt index 5e8e5b0d83..269e2f8b74 100644 --- a/apps/teacher/src/test/java/com/instructure/teacher/unit/modules/list/ModuleListPresenterTest.kt +++ b/apps/teacher/src/test/java/com/instructure/teacher/unit/modules/list/ModuleListPresenterTest.kt @@ -330,6 +330,28 @@ class ModuleListPresenterTest : Assert() { assertEquals(expectedState, itemState) } + @Test + fun `Returns correct state for New Quiz module item`() { + val item = moduleItemTemplate.copy( + title = "New Quiz", + type = "Assignment", + quizLti = true + ) + val model = modelTemplate.copy( + modules = listOf( + moduleTemplate.copy(items = listOf(item)) + ) + ) + val expectedState = moduleItemDataTemplate.copy( + title = item.title, + iconResId = R.drawable.ic_quiz, + type = ModuleItem.Type.Assignment + ) + val viewState = ModuleListPresenter.present(model, context) + val itemState = (viewState.items[0] as ModuleListItemData.ModuleData).moduleItems.first() + assertEquals(expectedState, itemState) + } + @Test fun `Returns correct state for SubHeader`() { val item = moduleItemTemplate.copy( diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/models/Assignment.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/models/Assignment.kt index f513af1c69..c7a0f186fc 100644 --- a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/models/Assignment.kt +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/models/Assignment.kt @@ -239,6 +239,10 @@ data class Assignment( return (submission?.grade != null && submission?.workflowState != "pending_review" && submission?.postedAt != null) } + fun isNewQuizLti(): Boolean { + return submissionTypesRaw.contains(SubmissionType.EXTERNAL_TOOL.apiString) && externalToolAttributes?.url?.contains("quiz-lti") == true + } + companion object { const val PASS_FAIL_TYPE = "pass_fail" @@ -294,14 +298,11 @@ data class Assignment( ) ?: "" - fun submissionTypeStringToPrettyPrintString(submissionType: String?, context: Context): String? = - submissionTypeToPrettyPrintString(getSubmissionTypeFromAPIString(submissionType), context) - - fun submissionTypeToPrettyPrintString(submissionType: SubmissionType?, context: Context): String? { - return submissionTypeToPrettyPrintString(submissionType, context.resources) + fun submissionTypeToPrettyPrintString(submissionType: SubmissionType?, context: Context, newQuizLti: Boolean = false): String? { + return submissionTypeToPrettyPrintString(submissionType, context.resources, newQuizLti) } - fun submissionTypeToPrettyPrintString(submissionType: SubmissionType?, resources: Resources): String? { + fun submissionTypeToPrettyPrintString(submissionType: SubmissionType?, resources: Resources, newQuizLti: Boolean = false): String? { submissionType ?: return null return when (submissionType) { @@ -309,7 +310,14 @@ data class Assignment( SubmissionType.NONE -> resources.getString(R.string.canvasAPI_none) SubmissionType.ON_PAPER -> resources.getString(R.string.canvasAPI_onPaper) SubmissionType.DISCUSSION_TOPIC -> resources.getString(R.string.canvasAPI_discussionTopic) - SubmissionType.EXTERNAL_TOOL, SubmissionType.BASIC_LTI_LAUNCH -> resources.getString(R.string.canvasAPI_externalTool) + SubmissionType.EXTERNAL_TOOL -> { + if (newQuizLti) { + resources.getString(R.string.canvasAPI_quiz) + } else { + resources.getString(R.string.canvasAPI_externalTool) + } + } + SubmissionType.BASIC_LTI_LAUNCH -> resources.getString(R.string.canvasAPI_externalTool) SubmissionType.ONLINE_UPLOAD -> resources.getString(R.string.canvasAPI_onlineUpload) SubmissionType.ONLINE_TEXT_ENTRY -> resources.getString(R.string.canvasAPI_onlineTextEntry) SubmissionType.ONLINE_URL -> resources.getString(R.string.canvasAPI_onlineURL) diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/models/ModuleItem.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/models/ModuleItem.kt index 51439c03c4..8254cc505a 100644 --- a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/models/ModuleItem.kt +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/models/ModuleItem.kt @@ -46,6 +46,8 @@ data class ModuleItem( val unpublishable: Boolean = true, @SerializedName("mastery_paths") var masteryPaths: MasteryPath? = null, + @SerializedName("quiz_lti") + var quizLti: Boolean = false, // When we display the "Choose Assignment Group" when an assignment uses Mastery Paths we create a new row to display. // We still need the module item id to select the assignment group that we want, but if we use the same id as the root // module item both items wouldn't display (because they would have the same id at that point). diff --git a/libs/pandares/src/main/res/values/strings.xml b/libs/pandares/src/main/res/values/strings.xml index 864739e258..df28ab9326 100644 --- a/libs/pandares/src/main/res/values/strings.xml +++ b/libs/pandares/src/main/res/values/strings.xml @@ -1894,4 +1894,7 @@ There was an error loading this announcement Announcement Global Announcement + Open the Quiz + Quiz Submission + This quiz opens in a web browser. Select "Open The Quiz" to proceed. diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/assignments/details/AssignmentDetailsViewModel.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/assignments/details/AssignmentDetailsViewModel.kt index 63d3b05a10..6291a3b23e 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/assignments/details/AssignmentDetailsViewModel.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/assignments/details/AssignmentDetailsViewModel.kt @@ -317,7 +317,7 @@ class AssignmentDetailsViewModel @Inject constructor( }.orEmpty() val submissionTypes = assignment.getSubmissionTypes() - .map { Assignment.submissionTypeToPrettyPrintString(it, resources) } + .map { Assignment.submissionTypeToPrettyPrintString(it, resources, assignment.isNewQuizLti()) } .joinToString() val allowedFileTypes = assignment.allowedExtensions.joinToString().takeIf { @@ -335,6 +335,7 @@ class AssignmentDetailsViewModel @Inject constructor( val submitButtonText = resources.getString( when { !submitEnabled -> R.string.noAttemptsLeft + assignment.isNewQuizLti() -> R.string.openTheQuizButton assignment.turnInType == Assignment.TurnInType.QUIZ -> R.string.viewQuiz assignment.turnInType == Assignment.TurnInType.DISCUSSION -> R.string.viewDiscussion assignment.turnInType == Assignment.TurnInType.EXTERNAL_TOOL -> R.string.launchExternalTool diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/utils/AssignmentExtensions.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/utils/AssignmentExtensions.kt index fcd93ee9d5..0b83b8d2a4 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/utils/AssignmentExtensions.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/utils/AssignmentExtensions.kt @@ -32,6 +32,7 @@ private const val NO_GRADE_INDICATOR = "-" fun Assignment.getAssignmentIcon() = when { Assignment.SubmissionType.ONLINE_QUIZ.apiString in submissionTypesRaw -> R.drawable.ic_quiz Assignment.SubmissionType.DISCUSSION_TOPIC.apiString in submissionTypesRaw -> R.drawable.ic_discussion + isNewQuizLti() -> R.drawable.ic_quiz else -> R.drawable.ic_assignment } From 22b078a7db6b1bc2248beb54b00a028932e4457a Mon Sep 17 00:00:00 2001 From: Akos Hermann <72087159+hermannakos@users.noreply.github.com> Date: Tue, 5 Nov 2024 11:33:52 +0100 Subject: [PATCH 04/31] [MBL-17948][All] Common settings code + new theme selector (#2588) Test plan: Check the settings page in all apps. refs: MBL-17948 affects: Student, Teacher, Parent release note: none --- .../parentapp/di/feature/SettingsModule.kt | 5 +- .../settings/ParentSettingsBehaviour.kt | 10 +- .../features/settings/ParentSettingsRouter.kt | 18 +- .../settings/ParentSettingsBehaviourTest.kt | 10 +- .../ui/e2e/PushNotificationsE2ETest.kt | 6 +- .../student/ui/e2e/SettingsE2ETest.kt | 44 +- .../e2e/offline/OfflineSyncProgressE2ETest.kt | 2 + .../e2e/offline/OfflineSyncSettingsE2ETest.kt | 12 +- .../OfflineContentInteractionTest.kt | 27 +- .../ProfileSettingsInteractionTest.kt | 32 +- .../ui/interaction/SettingsInteractionTest.kt | 11 +- .../SyncSettingsInteractionTest.kt | 5 +- .../student/ui/pages/SettingsPage.kt | 125 ------ .../student/ui/utils/StudentComposeTest.kt | 2 + .../student/ui/utils/StudentTest.kt | 4 +- apps/student/src/main/AndroidManifest.xml | 4 - .../student/activity/NavigationActivity.kt | 7 +- .../student/activity/SettingsActivity.kt | 86 ---- .../student/di/feature/SettingsModule.kt | 27 +- .../settings/StudentSettingsBehaviour.kt | 56 +++ .../settings/StudentSettingsRouter.kt | 97 +++++ .../fragment/ApplicationSettingsFragment.kt | 235 ---------- .../student/router/RouteResolver.kt | 8 + .../src/main/res/layout/activity_settings.xml | 48 --- .../layout/fragment_application_settings.xml | 347 --------------- .../ui/e2e/PushNotificationsE2ETest.kt | 6 +- .../teacher/ui/e2e/SettingsE2ETest.kt | 47 +- .../teacher/ui/pages/SettingsPage.kt | 146 ------- .../teacher/ui/utils/TeacherComposeTest.kt | 2 + .../teacher/ui/utils/TeacherTest.kt | 2 - .../teacher/activities/InitActivity.kt | 2 +- .../instructure/teacher/di/SettingsModule.kt | 23 +- ...ProfileSettingsFragmentPresenterFactory.kt | 26 -- .../settings/TeacherSettingsBehaviour.kt | 46 ++ .../settings/TeacherSettingsRouter.kt | 71 +++ .../teacher/fragments/SettingsFragment.kt | 141 ------ .../ProfileSettingsFragmentPresenter.kt | 28 -- .../teacher/router/RouteMatcher.kt | 4 +- .../teacher/router/RouteResolver.kt | 4 +- .../ProfileSettingsFragmentView.kt | 21 - .../src/main/res/layout/fragment_settings.xml | 229 ---------- .../interaction/SettingsInteractionTest.kt | 12 - .../common/pages/compose/SettingsPage.kt | 133 +++++- .../main/res/drawable-night/ic_panda_dark.xml | 214 ++++++++++ .../res/drawable-night/ic_panda_light.xml | 183 ++++++++ .../res/drawable-night/ic_panda_system.xml | 403 ++++++++++++++++++ .../src/main/res/drawable/ic_panda_dark.xml | 214 ++++++++++ .../src/main/res/drawable/ic_panda_light.xml | 183 ++++++++ .../src/main/res/drawable/ic_panda_system.xml | 403 ++++++++++++++++++ libs/pandares/src/main/res/values/strings.xml | 1 + .../features/settings/SettingsScreenTest.kt | 52 ++- .../compose/composables/LabelValueSwitch.kt | 101 +++++ .../features/settings/SettingsBehaviour.kt | 6 +- .../features/settings/SettingsFragment.kt | 160 ++++++- .../features/settings/SettingsRouter.kt | 18 +- .../features/settings/SettingsScreen.kt | 229 ++++++++-- .../features/settings/SettingsUiState.kt | 17 +- .../features/settings/SettingsViewModel.kt | 96 ++++- .../src/main/res/layout/fragment_settings.xml | 33 ++ .../settings/SettingsViewModelTest.kt | 63 ++- 60 files changed, 2889 insertions(+), 1658 deletions(-) delete mode 100644 apps/student/src/main/java/com/instructure/student/activity/SettingsActivity.kt create mode 100644 apps/student/src/main/java/com/instructure/student/features/settings/StudentSettingsBehaviour.kt create mode 100644 apps/student/src/main/java/com/instructure/student/features/settings/StudentSettingsRouter.kt delete mode 100644 apps/student/src/main/java/com/instructure/student/fragment/ApplicationSettingsFragment.kt delete mode 100644 apps/student/src/main/res/layout/activity_settings.xml delete mode 100644 apps/student/src/main/res/layout/fragment_application_settings.xml delete mode 100644 apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/SettingsPage.kt delete mode 100644 apps/teacher/src/main/java/com/instructure/teacher/factory/ProfileSettingsFragmentPresenterFactory.kt create mode 100644 apps/teacher/src/main/java/com/instructure/teacher/features/settings/TeacherSettingsBehaviour.kt create mode 100644 apps/teacher/src/main/java/com/instructure/teacher/features/settings/TeacherSettingsRouter.kt delete mode 100644 apps/teacher/src/main/java/com/instructure/teacher/fragments/SettingsFragment.kt delete mode 100644 apps/teacher/src/main/java/com/instructure/teacher/presenters/ProfileSettingsFragmentPresenter.kt delete mode 100644 apps/teacher/src/main/java/com/instructure/teacher/viewinterface/ProfileSettingsFragmentView.kt delete mode 100644 apps/teacher/src/main/res/layout/fragment_settings.xml create mode 100644 libs/pandares/src/main/res/drawable-night/ic_panda_dark.xml create mode 100644 libs/pandares/src/main/res/drawable-night/ic_panda_light.xml create mode 100644 libs/pandares/src/main/res/drawable-night/ic_panda_system.xml create mode 100644 libs/pandares/src/main/res/drawable/ic_panda_dark.xml create mode 100644 libs/pandares/src/main/res/drawable/ic_panda_light.xml create mode 100644 libs/pandares/src/main/res/drawable/ic_panda_system.xml create mode 100644 libs/pandautils/src/main/java/com/instructure/pandautils/compose/composables/LabelValueSwitch.kt create mode 100644 libs/pandautils/src/main/res/layout/fragment_settings.xml diff --git a/apps/parent/src/main/java/com/instructure/parentapp/di/feature/SettingsModule.kt b/apps/parent/src/main/java/com/instructure/parentapp/di/feature/SettingsModule.kt index 0c256eac71..b68ef90bb6 100644 --- a/apps/parent/src/main/java/com/instructure/parentapp/di/feature/SettingsModule.kt +++ b/apps/parent/src/main/java/com/instructure/parentapp/di/feature/SettingsModule.kt @@ -18,6 +18,7 @@ package com.instructure.parentapp.di.feature import com.instructure.pandautils.features.settings.SettingsBehaviour import com.instructure.pandautils.features.settings.SettingsRouter +import com.instructure.parentapp.features.dashboard.SelectedStudentHolder import com.instructure.parentapp.features.settings.ParentSettingsBehaviour import com.instructure.parentapp.features.settings.ParentSettingsRouter import dagger.Module @@ -30,8 +31,8 @@ import dagger.hilt.components.SingletonComponent class SettingsModule { @Provides - fun provideSettingsBehaviour(): SettingsBehaviour { - return ParentSettingsBehaviour() + fun provideSettingsBehaviour(selectedStudentHolder: SelectedStudentHolder): SettingsBehaviour { + return ParentSettingsBehaviour(selectedStudentHolder) } @Provides diff --git a/apps/parent/src/main/java/com/instructure/parentapp/features/settings/ParentSettingsBehaviour.kt b/apps/parent/src/main/java/com/instructure/parentapp/features/settings/ParentSettingsBehaviour.kt index bba51876eb..7546e05b80 100644 --- a/apps/parent/src/main/java/com/instructure/parentapp/features/settings/ParentSettingsBehaviour.kt +++ b/apps/parent/src/main/java/com/instructure/parentapp/features/settings/ParentSettingsBehaviour.kt @@ -13,16 +13,22 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . * - */ package com.instructure.parentapp.features.settings + */ +package com.instructure.parentapp.features.settings import com.instructure.pandautils.features.settings.SettingsBehaviour import com.instructure.pandautils.features.settings.SettingsItem import com.instructure.parentapp.R +import com.instructure.parentapp.features.dashboard.SelectedStudentHolder -class ParentSettingsBehaviour : SettingsBehaviour { +class ParentSettingsBehaviour(private val selectedStudentHolder: SelectedStudentHolder) : SettingsBehaviour { override val settingsItems: Map> get() = mapOf( R.string.preferences to listOf(SettingsItem.APP_THEME), R.string.legal to listOf(SettingsItem.ABOUT, SettingsItem.LEGAL) ) + + override suspend fun applyAppSpecificColorSettings() { + selectedStudentHolder.selectedStudentColorChanged() + } } \ No newline at end of file diff --git a/apps/parent/src/main/java/com/instructure/parentapp/features/settings/ParentSettingsRouter.kt b/apps/parent/src/main/java/com/instructure/parentapp/features/settings/ParentSettingsRouter.kt index aff1ffda29..ba84050d74 100644 --- a/apps/parent/src/main/java/com/instructure/parentapp/features/settings/ParentSettingsRouter.kt +++ b/apps/parent/src/main/java/com/instructure/parentapp/features/settings/ParentSettingsRouter.kt @@ -18,20 +18,4 @@ package com.instructure.parentapp.features.settings import com.instructure.pandautils.features.settings.SettingsRouter -class ParentSettingsRouter : SettingsRouter { - override fun navigateToProfileSettings() { - throw IllegalStateException("Profile settings item not available") - } - - override fun navigateToPushNotificationsSettings() { - throw IllegalStateException("Push settings item not available") - } - - override fun navigateToEmailNotificationsSettings() { - throw IllegalStateException("Email settings item not available") - } - - override fun navigateToPairWithObserver() { - throw IllegalStateException("Pair with observer item not available") - } -} \ No newline at end of file +class ParentSettingsRouter : SettingsRouter \ No newline at end of file diff --git a/apps/parent/src/test/java/com/instructure/parentapp/features/settings/ParentSettingsBehaviourTest.kt b/apps/parent/src/test/java/com/instructure/parentapp/features/settings/ParentSettingsBehaviourTest.kt index a7deac8c56..4eca4b6ceb 100644 --- a/apps/parent/src/test/java/com/instructure/parentapp/features/settings/ParentSettingsBehaviourTest.kt +++ b/apps/parent/src/test/java/com/instructure/parentapp/features/settings/ParentSettingsBehaviourTest.kt @@ -16,16 +16,22 @@ */ package com.instructure.parentapp.features.settings +import com.instructure.canvasapi2.models.User import com.instructure.pandautils.features.settings.SettingsItem -import org.junit.Test import com.instructure.parentapp.R +import com.instructure.parentapp.features.dashboard.TestSelectStudentHolder import junit.framework.TestCase.assertEquals +import kotlinx.coroutines.flow.MutableStateFlow +import org.junit.Test class ParentSettingsBehaviourTest { + private val selectedStudentFlow = MutableStateFlow(null) + private val selectedStudentHolder = TestSelectStudentHolder(selectedStudentFlow) + @Test fun `Settings behaviour has the correct items`() { - val settingsBehaviour = ParentSettingsBehaviour() + val settingsBehaviour = ParentSettingsBehaviour(selectedStudentHolder) val expected = mapOf( R.string.preferences to listOf(SettingsItem.APP_THEME), diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/PushNotificationsE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/PushNotificationsE2ETest.kt index 4503ae6bb8..90472a9c3b 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/PushNotificationsE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/PushNotificationsE2ETest.kt @@ -7,12 +7,12 @@ import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData import com.instructure.student.BuildConfig -import com.instructure.student.ui.utils.StudentTest +import com.instructure.student.ui.utils.StudentComposeTest import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test @HiltAndroidTest -class PushNotificationsE2ETest : StudentTest() { +class PushNotificationsE2ETest : StudentComposeTest() { override fun displaysPageObjects() = Unit @@ -40,7 +40,7 @@ class PushNotificationsE2ETest : StudentTest() { leftSideNavigationDrawerPage.clickSettingsMenu() Log.d(STEP_TAG, "Open Push Notifications Page.") - settingsPage.openPushNotificationsPage() + settingsPage.clickOnSettingsItem("Push Notifications") Log.d(ASSERTION_TAG, "Assert that the toolbar title is 'Push Notifications' on the Push Notifications Page.") pushNotificationsPage.assertToolbarTitle() 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 0417003bff..c701efcb60 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 @@ -18,6 +18,8 @@ package com.instructure.student.ui.e2e import android.content.Intent import android.util.Log +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick import androidx.test.espresso.Espresso import androidx.test.espresso.intent.Intents import androidx.test.espresso.intent.Intents.intended @@ -32,22 +34,29 @@ import com.instructure.dataseeding.api.ConversationsApi import com.instructure.dataseeding.api.CoursesApi import com.instructure.dataseeding.api.EnrollmentsApi import com.instructure.espresso.ViewUtils +import com.instructure.pandautils.utils.AppTheme import com.instructure.student.BuildConfig import com.instructure.student.R import com.instructure.student.ui.utils.IntentActionMatcher -import com.instructure.student.ui.utils.StudentTest +import com.instructure.student.ui.utils.StudentComposeTest import com.instructure.student.ui.utils.seedData import com.instructure.student.ui.utils.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest +import org.junit.After import org.junit.Assert import org.junit.Test @HiltAndroidTest -class SettingsE2ETest : StudentTest() { +class SettingsE2ETest : StudentComposeTest() { override fun displaysPageObjects() = Unit override fun enableAndConfigureAccessibilityChecks() = Unit + @After + fun tearDown() { + Intents.release() + } + @E2E @Test @TestMetaData(Priority.MANDATORY, FeatureCategory.SETTINGS, TestCategory.E2E) @@ -66,7 +75,7 @@ class SettingsE2ETest : StudentTest() { settingsPage.assertPageObjects() Log.d(STEP_TAG, "Open Profile Settings Page.") - settingsPage.openProfileSettings() + settingsPage.clickOnSettingsItem("Profile Settings") profileSettingsPage.assertPageObjects() val newUserName = "John Doe" @@ -82,7 +91,7 @@ class SettingsE2ETest : StudentTest() { Log.d(STEP_TAG, "Navigate to Settings Page again and open Panda Avatar Creator.") leftSideNavigationDrawerPage.clickSettingsMenu() settingsPage.assertPageObjects() - settingsPage.openProfileSettings() + settingsPage.clickOnSettingsItem("Profile Settings") profileSettingsPage.assertPageObjects() profileSettingsPage.launchPandaAvatarCreator() @@ -126,13 +135,9 @@ class SettingsE2ETest : StudentTest() { leftSideNavigationDrawerPage.clickSettingsMenu() settingsPage.assertPageObjects() - Log.d(STEP_TAG,"Navigate to Settings Page and open App Theme Settings.") - settingsPage.openAppThemeSettings() Log.d(STEP_TAG,"Select Dark App Theme and assert that the App Theme Title and Status has the proper text color (which is used in Dark mode).") - settingsPage.selectAppTheme("Dark") - settingsPage.assertAppThemeTitleTextColor("#FFFFFFFF") //Currently, this color is used in the Dark mode for the AppTheme Title text. - settingsPage.assertAppThemeStatusTextColor("#FF919CA8") //Currently, this color is used in the Dark mode for the AppTheme Status text. + settingsPage.selectAppTheme(AppTheme.DARK) Log.d(STEP_TAG,"Navigate back to Dashboard. Assert that the 'Courses' label has the proper text color (which is used in Dark mode).") Espresso.pressBack() @@ -146,12 +151,9 @@ class SettingsE2ETest : StudentTest() { Log.d(STEP_TAG,"Navigate to Settings Page and open App Theme Settings again.") Espresso.pressBack() 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).") - settingsPage.selectAppTheme("Light") - settingsPage.assertAppThemeTitleTextColor("#FF273540") //Currently, this color is used in the Light mode for the AppTheme Title texts. - settingsPage.assertAppThemeStatusTextColor("#FF6A7883") //Currently, this color is used in the Light mode for the AppTheme Status text. + settingsPage.selectAppTheme(AppTheme.LIGHT) Log.d(STEP_TAG,"Navigate back to Dashboard. Assert that the 'Courses' label has the proper text color (which is used in Light mode).") Espresso.pressBack() @@ -176,7 +178,7 @@ class SettingsE2ETest : StudentTest() { settingsPage.assertPageObjects() Log.d(STEP_TAG, "Click on 'Legal' link to open Legal Page. Assert that Legal Page has opened.") - settingsPage.openLegalPage() + settingsPage.clickOnSettingsItem("Legal") legalPage.assertPageObjects() } @@ -198,7 +200,7 @@ class SettingsE2ETest : StudentTest() { settingsPage.assertPageObjects() Log.d(STEP_TAG, "Click on 'About' link to open About Page. Assert that About Page has opened.") - settingsPage.openAboutPage() + settingsPage.clickOnSettingsItem("About") aboutPage.assertPageObjects() Log.d(STEP_TAG,"Check that domain is equal to: ${student.domain} (student's domain).") @@ -236,7 +238,7 @@ class SettingsE2ETest : StudentTest() { RemoteConfigParam.values().forEach {param -> initialValues.put(param.rc_name, RemoteConfigUtils.getString(param))} Log.d(STEP_TAG, "Navigate to Remote Config Settings Page.") - settingsPage.openRemoteConfigParams() + settingsPage.clickOnSettingsItem("Remote Config Params") RemoteConfigParam.values().forEach { param -> @@ -256,7 +258,7 @@ class SettingsE2ETest : StudentTest() { Espresso.pressBack() Log.d(STEP_TAG, "Navigate to Remote Config Settings Page.") - settingsPage.openRemoteConfigParams() + settingsPage.clickOnSettingsItem("Remote Config Params") Log.d(STEP_TAG, "Assert that all fields have maintained their initial value.") RemoteConfigParam.values().forEach { param -> @@ -282,21 +284,17 @@ class SettingsE2ETest : StudentTest() { Log.d(STEP_TAG, "Navigate to User Settings Page.") leftSideNavigationDrawerPage.clickSettingsMenu() - settingsPage.assertPageObjects() Log.d(STEP_TAG, "Click on 'Subscribe to Calendar'.") - settingsPage.openSubscribeToCalendar() + settingsPage.clickOnSettingsItem("Subscribe to Calendar Feed") Log.d(STEP_TAG, "Click on the 'SUBSCRIBE' button of the pop-up dialog.") - settingsPage.clickOnSubscribe() + settingsPage.clickOnSubscribeButton() Log.d(STEP_TAG, "Assert that the proper intents has launched, so the NavigationActivity has been launched with an Intent from SettingsActivity.") val calendarDataMatcherString = "https://calendar.google.com/calendar/r?cid=webcal://" val intentActionMatcher = IntentActionMatcher(Intent.ACTION_VIEW, calendarDataMatcherString) intended(intentActionMatcher) - - Log.d(PREPARATION_TAG, "Release Intents.") - Intents.release() } @E2E diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineSyncProgressE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineSyncProgressE2ETest.kt index 9bb2940fa9..ae2b88bb63 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineSyncProgressE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineSyncProgressE2ETest.kt @@ -23,6 +23,7 @@ import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.OfflineE2E import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.SecondaryFeatureCategory +import com.instructure.canvas.espresso.Stub import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData import com.instructure.student.ui.e2e.offline.utils.OfflineTestUtils @@ -40,6 +41,7 @@ class OfflineSyncProgressE2ETest : StudentTest() { override fun enableAndConfigureAccessibilityChecks() = Unit + @Stub @OfflineE2E @Test @TestMetaData(Priority.MANDATORY, FeatureCategory.SYNC_PROGRESS, TestCategory.E2E, SecondaryFeatureCategory.OFFLINE_MODE) diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineSyncSettingsE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineSyncSettingsE2ETest.kt index 6efa56db19..550911fe82 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineSyncSettingsE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineSyncSettingsE2ETest.kt @@ -24,6 +24,7 @@ import com.instructure.canvas.espresso.SecondaryFeatureCategory import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData import com.instructure.pandautils.R +import com.instructure.student.ui.utils.StudentComposeTest import com.instructure.student.ui.utils.StudentTest import com.instructure.student.ui.utils.ViewUtils import com.instructure.student.ui.utils.seedData @@ -33,7 +34,7 @@ import org.junit.After import org.junit.Test @HiltAndroidTest -class OfflineSyncSettingsE2ETest : StudentTest() { +class OfflineSyncSettingsE2ETest : StudentComposeTest() { override fun displaysPageObjects() = Unit @@ -57,11 +58,10 @@ class OfflineSyncSettingsE2ETest : StudentTest() { Log.d(STEP_TAG, "Assert that the Offline Sync Settings related information is displayed properly on the Settings Page ('Daily' is the default status).") settingsPage.assertOfflineContentDisplayed() - settingsPage.assertOfflineContentTitle() - settingsPage.assertOfflineSyncSettingsStatus(R.string.daily) + settingsPage.assertOfflineSyncSettingsStatus("Daily") Log.d(STEP_TAG, "Open Offline Sync Settings page and wait for it to be loaded.") - settingsPage.openOfflineSyncSettingsPage() + settingsPage.clickOnSyncSettingsItem() offlineSyncSettingsPage.assertPageObjects() Log.d(STEP_TAG, "Assert that further settings, such as the toolbar title is displayed and correct, and both the Auto Content Sync and Wi-Fi Only Sync toggles are displayed and checked by default.") @@ -126,10 +126,10 @@ class OfflineSyncSettingsE2ETest : StudentTest() { leftSideNavigationDrawerPage.clickSettingsMenu() Log.d(STEP_TAG, "Assert that the Offline Sync Settings frequency text is 'Weekly' (because we set it previously).") - settingsPage.assertOfflineSyncSettingsStatus(R.string.weekly) + settingsPage.assertOfflineSyncSettingsStatus("Weekly") Log.d(STEP_TAG, "Open Offline Sync Settings page and wait for it to be loaded.") - settingsPage.openOfflineSyncSettingsPage() + settingsPage.clickOnSyncSettingsItem() offlineSyncSettingsPage.assertPageObjects() Log.d(STEP_TAG, "Assert that the Offline Sync Settings frequency text is 'Weekly' (because we set it previously) and the 'Sync Content Wi-Fi Only' switch is switched off.") diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/OfflineContentInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/OfflineContentInteractionTest.kt index d429d3b65d..e71b9f2b54 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/OfflineContentInteractionTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/OfflineContentInteractionTest.kt @@ -18,7 +18,11 @@ package com.instructure.student.ui.interaction import android.text.format.Formatter +import androidx.compose.ui.platform.ComposeView import androidx.test.espresso.Espresso +import androidx.test.espresso.matcher.ViewMatchers +import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckResultUtils +import com.google.android.apps.common.testing.accessibility.framework.checks.SpeakableTextPresentCheck import com.google.android.material.checkbox.MaterialCheckBox import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority @@ -32,14 +36,16 @@ import com.instructure.canvasapi2.models.Tab import com.instructure.dataseeding.util.Randomizer import com.instructure.pandautils.R import com.instructure.pandautils.utils.StorageUtils +import com.instructure.student.ui.utils.StudentComposeTest import com.instructure.student.ui.utils.StudentTest import com.instructure.student.ui.utils.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest +import org.hamcrest.Matchers import org.junit.Test import javax.inject.Inject @HiltAndroidTest -class OfflineContentInteractionTest : StudentTest() { +class OfflineContentInteractionTest : StudentComposeTest() { @Inject lateinit var storageUtils: StorageUtils @@ -367,11 +373,28 @@ class OfflineContentInteractionTest : StudentTest() { tokenLogin(data.domain, token, student) dashboardPage.waitForRender() leftSideNavigationDrawerPage.clickSettingsMenu() - settingsPage.openOfflineSyncSettingsPage() + settingsPage.clickOnSyncSettingsItem() offlineSyncSettingsPage.clickWifiOnlySwitch() offlineSyncSettingsPage.clickTurnOff() Espresso.pressBack() Espresso.pressBack() dashboardPage.openGlobalManageOfflineContentPage() } + + override fun enableAndConfigureAccessibilityChecks() { + extraAccessibilitySupressions = Matchers.allOf( + AccessibilityCheckResultUtils.matchesCheck( + SpeakableTextPresentCheck::class.java + ), + AccessibilityCheckResultUtils.matchesViews( + ViewMatchers.withParent( + ViewMatchers.withClassName( + Matchers.equalTo(ComposeView::class.java.name) + ) + ) + ) + ) + + super.enableAndConfigureAccessibilityChecks() + } } diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ProfileSettingsInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ProfileSettingsInteractionTest.kt index cb58b48fd2..b2e05bcc1a 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ProfileSettingsInteractionTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ProfileSettingsInteractionTest.kt @@ -1,7 +1,11 @@ package com.instructure.student.ui.interaction +import androidx.compose.ui.platform.ComposeView import androidx.test.espresso.Espresso +import androidx.test.espresso.matcher.ViewMatchers import androidx.test.rule.GrantPermissionRule +import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckResultUtils +import com.google.android.apps.common.testing.accessibility.framework.checks.SpeakableTextPresentCheck import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.TestCategory @@ -10,15 +14,16 @@ import com.instructure.canvas.espresso.mockCanvas.MockCanvas import com.instructure.canvas.espresso.mockCanvas.addUserPermissions import com.instructure.canvas.espresso.mockCanvas.init import com.instructure.student.R -import com.instructure.student.ui.utils.StudentTest +import com.instructure.student.ui.utils.StudentComposeTest import com.instructure.student.ui.utils.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest +import org.hamcrest.Matchers import org.junit.Assert.assertTrue import org.junit.Rule import org.junit.Test @HiltAndroidTest -class ProfileSettingsInteractionTest : StudentTest() { +class ProfileSettingsInteractionTest : StudentComposeTest() { override fun displaysPageObjects() = Unit // Not used for interaction tests @@ -43,7 +48,7 @@ class ProfileSettingsInteractionTest : StudentTest() { tokenLogin(data.domain, token, student) leftSideNavigationDrawerPage.clickSettingsMenu() - settingsPage.openProfileSettings() + settingsPage.clickOnSettingsItem("Profile Settings") profileSettingsPage.changeUserNameTo(newUserName) Espresso.pressBack() // to settings page @@ -62,7 +67,7 @@ class ProfileSettingsInteractionTest : StudentTest() { tokenLogin(data.domain, token, student) leftSideNavigationDrawerPage.clickSettingsMenu() - settingsPage.openProfileSettings() + settingsPage.clickOnSettingsItem("Profile Settings") profileSettingsPage.assertSettingsDisabled() // No permissions granted } @@ -86,7 +91,7 @@ class ProfileSettingsInteractionTest : StudentTest() { // Navigate to avatar creation page leftSideNavigationDrawerPage.clickSettingsMenu() - settingsPage.openProfileSettings() + settingsPage.clickOnSettingsItem("Profile Settings") profileSettingsPage.launchPandaAvatarCreator() // Select head @@ -115,4 +120,21 @@ class ProfileSettingsInteractionTest : StudentTest() { ) } + + override fun enableAndConfigureAccessibilityChecks() { + extraAccessibilitySupressions = Matchers.allOf( + AccessibilityCheckResultUtils.matchesCheck( + SpeakableTextPresentCheck::class.java + ), + AccessibilityCheckResultUtils.matchesViews( + ViewMatchers.withParent( + ViewMatchers.withClassName( + Matchers.equalTo(ComposeView::class.java.name) + ) + ) + ) + ) + + super.enableAndConfigureAccessibilityChecks() + } } \ No newline at end of file diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/SettingsInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/SettingsInteractionTest.kt index 762fb310e1..451f701398 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/SettingsInteractionTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/SettingsInteractionTest.kt @@ -29,6 +29,7 @@ import com.instructure.canvas.espresso.mockCanvas.MockCanvas import com.instructure.canvas.espresso.mockCanvas.init import com.instructure.canvasapi2.models.Course import com.instructure.canvasapi2.utils.ApiPrefs +import com.instructure.student.ui.utils.StudentComposeTest import com.instructure.student.ui.utils.StudentTest import com.instructure.student.ui.utils.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest @@ -37,7 +38,7 @@ import org.junit.Before import org.junit.Test @HiltAndroidTest -class SettingsInteractionTest : StudentTest() { +class SettingsInteractionTest : StudentComposeTest() { override fun displaysPageObjects() = Unit // Not used for interaction tests private lateinit var course: Course @@ -59,7 +60,7 @@ class SettingsInteractionTest : StudentTest() { setUpAndSignIn() leftSideNavigationDrawerPage.clickSettingsMenu() - settingsPage.openLegalPage() + settingsPage.clickOnSettingsItem("Legal") Intents.init() try { @@ -80,7 +81,7 @@ class SettingsInteractionTest : StudentTest() { setUpAndSignIn() leftSideNavigationDrawerPage.clickSettingsMenu() - settingsPage.openLegalPage() + settingsPage.clickOnSettingsItem("Legal") legalPage.openTermsOfUse() legalPage.assertTermsOfUseDisplayed() } @@ -93,7 +94,7 @@ class SettingsInteractionTest : StudentTest() { setUpAndSignIn() leftSideNavigationDrawerPage.clickSettingsMenu() - settingsPage.openLegalPage() + settingsPage.clickOnSettingsItem("Legal") legalPage.openPrivacyPolicy() canvasWebViewPage.acceptCookiePolicyIfNecessary() canvasWebViewPage.checkWebViewURL("https://www.instructure.com/policies/product-privacy-policy") @@ -109,7 +110,7 @@ class SettingsInteractionTest : StudentTest() { ApiPrefs.canGeneratePairingCode = true leftSideNavigationDrawerPage.clickSettingsMenu() - settingsPage.openPairObserverPage() + settingsPage.clickOnSettingsItem("Pair with Observer") pairObserverPage.hasCode("1") pairObserverPage.refresh() diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/SyncSettingsInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/SyncSettingsInteractionTest.kt index 2a5c54cd5f..83f3fc40a1 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/SyncSettingsInteractionTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/SyncSettingsInteractionTest.kt @@ -24,13 +24,14 @@ import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData import com.instructure.pandautils.R +import com.instructure.student.ui.utils.StudentComposeTest import com.instructure.student.ui.utils.StudentTest import com.instructure.student.ui.utils.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test @HiltAndroidTest -class SyncSettingsInteractionTest : StudentTest() { +class SyncSettingsInteractionTest : StudentComposeTest() { override fun displaysPageObjects() = Unit @@ -104,6 +105,6 @@ class SyncSettingsInteractionTest : StudentTest() { tokenLogin(data.domain, token, student) dashboardPage.waitForRender() leftSideNavigationDrawerPage.clickSettingsMenu() - settingsPage.openOfflineSyncSettingsPage() + settingsPage.clickOnSyncSettingsItem() } } 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 512939d935..e69de29bb2 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 @@ -1,125 +0,0 @@ -/* - * Copyright (C) 2019 - present Instructure, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package com.instructure.student.ui.pages - -import androidx.test.espresso.matcher.ViewMatchers.hasSibling -import com.instructure.espresso.OnViewWithId -import com.instructure.espresso.TextViewColorAssertion -import com.instructure.espresso.assertDisplayed -import com.instructure.espresso.assertNotDisplayed -import com.instructure.espresso.click -import com.instructure.espresso.page.BasePage -import com.instructure.espresso.page.onView -import com.instructure.espresso.page.plus -import com.instructure.espresso.page.withId -import com.instructure.espresso.page.withParent -import com.instructure.espresso.page.withText -import com.instructure.espresso.scrollTo -import com.instructure.student.R - -class SettingsPage : BasePage(R.id.settingsFragment) { - private val profileSettingLabel by OnViewWithId(R.id.profileSettings) - private val accountPreferencesLabel by OnViewWithId(R.id.accountPreferences) - private val pushNotificationsLabel by OnViewWithId(R.id.pushNotifications) - - // The pairObserverLabel may not be present if the corresponding remote-config flag is disabled. - private val pairObserverLabel by OnViewWithId(R.id.pairObserver, autoAssert = false) - private val aboutLabel by OnViewWithId(R.id.about) - private val legalLabel by OnViewWithId(R.id.legal) - private val subscribeCalendarLabel by OnViewWithId(R.id.subscribeToCalendar, autoAssert = false) - private val remoteConfigLabel by OnViewWithId(R.id.remoteConfigParams) - private val appThemeTitle by OnViewWithId(R.id.appThemeTitle) - private val appThemeStatus by OnViewWithId(R.id.appThemeStatus) - private val offlineContent by OnViewWithId(R.id.offlineSyncSettingsContainer) - - fun openAboutPage() { - aboutLabel.scrollTo().click() - } - - fun openLegalPage() { - legalLabel.scrollTo().click() - } - - fun openRemoteConfigParams() { - remoteConfigLabel.scrollTo().click() - } - - fun openPairObserverPage() { - pairObserverLabel.scrollTo().click() - } - - fun openProfileSettings() { - profileSettingLabel.scrollTo().click() - } - - fun openPushNotificationsPage() { - pushNotificationsLabel.scrollTo().click() - } - - - fun openSubscribeToCalendar() { - subscribeCalendarLabel.scrollTo().click() - } - - fun clickOnSubscribe() { - onView(withText(R.string.subscribeButton)).click() - } - - fun openAppThemeSettings() { - appThemeTitle.scrollTo().click() - } - - fun selectAppTheme(appTheme: String) - { - onView(withText(appTheme) + withParent(R.id.select_dialog_listview)).click() - } - - fun assertAppThemeTitleTextColor(expectedTextColor: String) { - appThemeTitle.check(TextViewColorAssertion(expectedTextColor)) - } - - fun assertAppThemeStatusTextColor(expectedTextColor: String) { - appThemeStatus.check(TextViewColorAssertion(expectedTextColor)) - } - - //OfflineMethod - fun openOfflineSyncSettingsPage() { - offlineContent.scrollTo().click() - } - - //OfflineMethod - fun assertOfflineContentDisplayed() { - offlineContent.scrollTo().assertDisplayed() - } - - //OfflineMethod - fun assertOfflineContentNotDisplayed() { - offlineContent.assertNotDisplayed() - } - - //OfflineMethod - fun assertOfflineContentTitle() { - onView(withId(R.id.offlineContentTitle) + withText(R.string.offlineContent)).assertDisplayed() - } - - //OfflineMethod - fun assertOfflineSyncSettingsStatus(expectedStatus: Int) { - onView(withId(R.id.offlineSyncSettingsStatus) + withText(expectedStatus) + withParent(R.id.offlineSyncSettingsContainer) + - hasSibling(withId(R.id.offlineSyncSettingsTitle) + withText(R.string.offlineSyncSettingsTitle))).assertDisplayed() - } - -} diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/utils/StudentComposeTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/utils/StudentComposeTest.kt index b4b871784e..e573e56490 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/utils/StudentComposeTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/utils/StudentComposeTest.kt @@ -25,6 +25,7 @@ import com.instructure.canvas.espresso.common.pages.compose.CalendarFilterPage import com.instructure.canvas.espresso.common.pages.compose.CalendarScreenPage import com.instructure.canvas.espresso.common.pages.compose.CalendarToDoCreateUpdatePage import com.instructure.canvas.espresso.common.pages.compose.CalendarToDoDetailsPage +import com.instructure.canvas.espresso.common.pages.compose.SettingsPage import com.instructure.student.activity.LoginActivity import org.junit.Rule @@ -39,4 +40,5 @@ abstract class StudentComposeTest : StudentTest() { val calendarToDoCreateUpdatePage = CalendarToDoCreateUpdatePage(composeTestRule) val calendarToDoDetailsPage = CalendarToDoDetailsPage(composeTestRule) val calendarFilterPage = CalendarFilterPage(composeTestRule) + val settingsPage = SettingsPage(composeTestRule) } \ No newline at end of file diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/utils/StudentTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/utils/StudentTest.kt index efdc4c2a85..52ad282662 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/utils/StudentTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/utils/StudentTest.kt @@ -89,7 +89,6 @@ import com.instructure.student.ui.pages.QuizTakingPage import com.instructure.student.ui.pages.RemoteConfigSettingsPage import com.instructure.student.ui.pages.ResourcesPage import com.instructure.student.ui.pages.SchedulePage -import com.instructure.student.ui.pages.SettingsPage import com.instructure.student.ui.pages.ShareExtensionStatusPage import com.instructure.student.ui.pages.ShareExtensionTargetPage import com.instructure.student.ui.pages.StudentAssignmentDetailsPage @@ -164,7 +163,6 @@ abstract class StudentTest : CanvasTest() { val quizTakingPage = QuizTakingPage() val goToQuizPage = GoToQuizPage(ModuleItemInteractions(R.id.moduleName, R.id.next_item, R.id.prev_item)) val remoteConfigSettingsPage = RemoteConfigSettingsPage() - val settingsPage = SettingsPage() val pushNotificationsPage = PushNotificationsPage() val submissionDetailsPage = SubmissionDetailsPage() val textSubmissionUploadPage = TextSubmissionUploadPage() @@ -185,7 +183,7 @@ abstract class StudentTest : CanvasTest() { // A no-op interaction to afford us an easy, harmless way to get a11y checking to trigger. fun meaninglessSwipe() { - Espresso.onView(ViewMatchers.withId(R.id.action_bar_root)).swipeRight(); + Espresso.onView(ViewMatchers.withId(R.id.action_bar_root)).swipeRight() } // Get the number of files/avatars in our panda avatars folder diff --git a/apps/student/src/main/AndroidManifest.xml b/apps/student/src/main/AndroidManifest.xml index 360bcc3e0e..ec123b370f 100644 --- a/apps/student/src/main/AndroidManifest.xml +++ b/apps/student/src/main/AndroidManifest.xml @@ -117,10 +117,6 @@ android:theme="@style/PSPDFKitTheme" android:windowSoftInputMode="adjustNothing" /> - - diff --git a/apps/student/src/main/java/com/instructure/student/activity/NavigationActivity.kt b/apps/student/src/main/java/com/instructure/student/activity/NavigationActivity.kt index 709f6b6fb0..a9f3721247 100644 --- a/apps/student/src/main/java/com/instructure/student/activity/NavigationActivity.kt +++ b/apps/student/src/main/java/com/instructure/student/activity/NavigationActivity.kt @@ -87,6 +87,7 @@ import com.instructure.pandautils.features.help.HelpDialogFragment import com.instructure.pandautils.features.inbox.list.InboxFragment import com.instructure.pandautils.features.notification.preferences.PushNotificationPreferencesFragment import com.instructure.pandautils.features.offline.sync.OfflineSyncHelper +import com.instructure.pandautils.features.settings.SettingsFragment import com.instructure.pandautils.features.themeselector.ThemeSelectorBottomSheet import com.instructure.pandautils.interfaces.NavigationCallbacks import com.instructure.pandautils.models.PushNotification @@ -281,7 +282,11 @@ class NavigationActivity : BaseRouterActivity(), Navigation, MasqueradingDialog. R.id.navigationDrawerItem_stopMasquerading -> { MasqueradeHelper.stopMasquerading(startActivityClass) } - R.id.navigationDrawerSettings -> startActivity(SettingsActivity.createIntent(applicationContext, featureFlagProvider.offlineEnabled())) + R.id.navigationDrawerSettings -> { + val route = SettingsFragment.makeRoute(featureFlagProvider.offlineEnabled()) + val fragment = SettingsFragment.newInstance(route) + addFragment(fragment, route) + } } } } diff --git a/apps/student/src/main/java/com/instructure/student/activity/SettingsActivity.kt b/apps/student/src/main/java/com/instructure/student/activity/SettingsActivity.kt deleted file mode 100644 index edaed771de..0000000000 --- a/apps/student/src/main/java/com/instructure/student/activity/SettingsActivity.kt +++ /dev/null @@ -1,86 +0,0 @@ -/* - * Copyright (C) 2016 - 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.activity - -import android.content.Context -import android.content.Intent -import android.content.res.Configuration -import android.os.Bundle -import androidx.appcompat.app.AppCompatActivity -import androidx.fragment.app.Fragment -import com.instructure.interactions.FragmentInteractions -import com.instructure.pandautils.analytics.SCREEN_VIEW_SETTINGS -import com.instructure.pandautils.analytics.ScreenView -import com.instructure.pandautils.binding.viewBinding -import com.instructure.pandautils.utils.NetworkStateProvider -import com.instructure.pandautils.utils.setVisible -import com.instructure.student.R -import com.instructure.student.databinding.ActivitySettingsBinding -import dagger.hilt.android.AndroidEntryPoint -import javax.inject.Inject - -private const val OFFLINE_ENABLED = "offlineEnabled" - -@ScreenView(SCREEN_VIEW_SETTINGS) -@AndroidEntryPoint -class SettingsActivity : AppCompatActivity(){ - - @Inject - lateinit var networkStateProvider: NetworkStateProvider - - private val binding by viewBinding(ActivitySettingsBinding::inflate) - - var offlineEnabled: Boolean = false - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - offlineEnabled = intent.getBooleanExtra(OFFLINE_ENABLED, false) - setContentView(binding.root) - networkStateProvider.isOnlineLiveData.observe(this) { isOnline -> - binding.offlineIndicator.root.setVisible(!isOnline) - } - } - - private val currentFragment: Fragment? get() = supportFragmentManager.fragments.last() - - override fun onConfigurationChanged(newConfig: Configuration) { - super.onConfigurationChanged(newConfig) - applyThemeForAllFragments() - } - - private fun applyThemeForAllFragments() { - supportFragmentManager.fragments.forEach { - (it as? FragmentInteractions)?.applyTheme() - } - } - - fun addFragment(fragment: Fragment) { - val ft = supportFragmentManager.beginTransaction() - currentFragment?.let { ft.hide(it) } - ft.add(R.id.fragmentContainer, fragment, fragment.javaClass.name) - ft.addToBackStack(fragment.javaClass.name) - ft.commitAllowingStateLoss() - } - - companion object { - fun createIntent(context: Context, offlineEnabled: Boolean): Intent { - return Intent(context, SettingsActivity::class.java).apply { - putExtra(OFFLINE_ENABLED, offlineEnabled) - } - } - } -} diff --git a/apps/student/src/main/java/com/instructure/student/di/feature/SettingsModule.kt b/apps/student/src/main/java/com/instructure/student/di/feature/SettingsModule.kt index ec2906477d..b2337c95be 100644 --- a/apps/student/src/main/java/com/instructure/student/di/feature/SettingsModule.kt +++ b/apps/student/src/main/java/com/instructure/student/di/feature/SettingsModule.kt @@ -15,24 +15,37 @@ */ package com.instructure.student.di.feature +import android.content.Context +import androidx.fragment.app.FragmentActivity +import com.instructure.canvasapi2.utils.ApiPrefs import com.instructure.pandautils.features.settings.SettingsBehaviour import com.instructure.pandautils.features.settings.SettingsRouter +import com.instructure.student.features.settings.StudentSettingsBehaviour +import com.instructure.student.features.settings.StudentSettingsRouter import dagger.Module import dagger.Provides import dagger.hilt.InstallIn +import dagger.hilt.android.components.ActivityComponent +import dagger.hilt.android.components.FragmentComponent +import dagger.hilt.android.components.ViewModelComponent +import dagger.hilt.android.qualifiers.ActivityContext import dagger.hilt.components.SingletonComponent @Module -@InstallIn(SingletonComponent::class) -class SettingsModule { - +@InstallIn(FragmentComponent::class) +class SettingsRouterModule{ @Provides - fun provideSettingsRouter(): SettingsRouter { - throw NotImplementedError("Not implemented") + fun provideSettingsRouter(activity: FragmentActivity): SettingsRouter { + return StudentSettingsRouter(activity) } +} + +@Module +@InstallIn(ViewModelComponent::class) +class SettingsModule { @Provides - fun provideSettingsBehavior(): SettingsBehaviour { - throw NotImplementedError("Not implemented") + fun provideSettingsBehavior(apiPrefs: ApiPrefs): SettingsBehaviour { + return StudentSettingsBehaviour(apiPrefs) } } \ No newline at end of file diff --git a/apps/student/src/main/java/com/instructure/student/features/settings/StudentSettingsBehaviour.kt b/apps/student/src/main/java/com/instructure/student/features/settings/StudentSettingsBehaviour.kt new file mode 100644 index 0000000000..60ac269d6b --- /dev/null +++ b/apps/student/src/main/java/com/instructure/student/features/settings/StudentSettingsBehaviour.kt @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2024 - 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.settings + +import com.instructure.canvasapi2.utils.ApiPrefs +import com.instructure.pandautils.features.settings.SettingsBehaviour +import com.instructure.pandautils.features.settings.SettingsItem +import com.instructure.student.BuildConfig +import com.instructure.student.R + +class StudentSettingsBehaviour( + private val apiPrefs: ApiPrefs, +) : SettingsBehaviour { + override val settingsItems: Map> + get() { + val preferencesList = mutableListOf( + SettingsItem.APP_THEME, + SettingsItem.PROFILE_SETTINGS, + SettingsItem.PUSH_NOTIFICATIONS, + SettingsItem.EMAIL_NOTIFICATIONS + ) + if (apiPrefs.canGeneratePairingCode == true) { + preferencesList.add(SettingsItem.PAIR_WITH_OBSERVER) + } + if (apiPrefs.user?.calendar?.ics != null) { + preferencesList.add(SettingsItem.SUBSCRIBE_TO_CALENDAR) + } + if (apiPrefs.canvasForElementary) { + preferencesList.add(1, SettingsItem.HOMEROOM_VIEW) + } + if (BuildConfig.DEBUG) { + preferencesList.add(SettingsItem.ACCOUNT_PREFERENCES) + preferencesList.add(SettingsItem.FEATURE_FLAGS) + preferencesList.add(SettingsItem.REMOTE_CONFIG) + } + + return mapOf( + R.string.preferences to preferencesList, + R.string.offlineContent to listOf(SettingsItem.OFFLINE_SYNCHRONIZATION), + R.string.legal to listOf(SettingsItem.ABOUT, SettingsItem.LEGAL) + ) + } +} \ No newline at end of file diff --git a/apps/student/src/main/java/com/instructure/student/features/settings/StudentSettingsRouter.kt b/apps/student/src/main/java/com/instructure/student/features/settings/StudentSettingsRouter.kt new file mode 100644 index 0000000000..b4f4f28369 --- /dev/null +++ b/apps/student/src/main/java/com/instructure/student/features/settings/StudentSettingsRouter.kt @@ -0,0 +1,97 @@ +/* + * Copyright (C) 2024 - 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.settings + +import androidx.fragment.app.FragmentActivity +import com.instructure.canvasapi2.utils.ApiPrefs +import com.instructure.interactions.router.Route +import com.instructure.pandautils.features.notification.preferences.EmailNotificationPreferencesFragment +import com.instructure.pandautils.features.notification.preferences.PushNotificationPreferencesFragment +import com.instructure.pandautils.features.offline.sync.settings.SyncSettingsFragment +import com.instructure.pandautils.features.settings.SettingsRouter +import com.instructure.pandautils.fragments.RemoteConfigParamsFragment +import com.instructure.student.activity.NothingToSeeHereFragment +import com.instructure.student.fragment.AccountPreferencesFragment +import com.instructure.student.fragment.FeatureFlagsFragment +import com.instructure.student.fragment.ProfileSettingsFragment +import com.instructure.student.mobius.settings.pairobserver.ui.PairObserverFragment +import com.instructure.student.router.RouteMatcher + +class StudentSettingsRouter( + private val activity: FragmentActivity +) : SettingsRouter { + override fun navigateToProfileSettings() { + val fragment = if (ApiPrefs.isStudentView) { + NothingToSeeHereFragment::class.java + } else { + ProfileSettingsFragment::class.java + } + RouteMatcher.route( + activity, + Route(null, fragment) + ) + } + + override fun navigateToPushNotificationsSettings() { + RouteMatcher.route( + activity, + Route(null, PushNotificationPreferencesFragment::class.java) + ) + + } + + override fun navigateToEmailNotificationsSettings() { + RouteMatcher.route( + activity, + Route(null, EmailNotificationPreferencesFragment::class.java) + ) + } + + override fun navigateToPairWithObserver() { + RouteMatcher.route( + activity, + Route(null, PairObserverFragment::class.java) + ) + } + + override fun navigateToSyncSettings() { + RouteMatcher.route( + activity, + Route(null, SyncSettingsFragment::class.java) + ) + } + + override fun navigateToAccountPreferences() { + RouteMatcher.route( + activity, + Route(null, AccountPreferencesFragment::class.java) + ) + } + + override fun navigateToRemoteConfig() { + RouteMatcher.route( + activity, + Route(null, RemoteConfigParamsFragment::class.java) + ) + } + + override fun navigateToFeatureFlags() { + RouteMatcher.route( + activity, + Route(null, FeatureFlagsFragment::class.java) + ) + } +} \ No newline at end of file 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 deleted file mode 100644 index 8a90f88748..0000000000 --- a/apps/student/src/main/java/com/instructure/student/fragment/ApplicationSettingsFragment.kt +++ /dev/null @@ -1,235 +0,0 @@ -/* - * Copyright (C) 2017 - 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.fragment - -import android.annotation.SuppressLint -import android.content.Intent -import android.net.Uri -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.appcompat.app.AlertDialog -import androidx.fragment.app.Fragment -import androidx.lifecycle.lifecycleScope -import com.instructure.canvasapi2.utils.* -import com.instructure.canvasapi2.utils.pageview.PageView -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.legal.LegalDialogFragment -import com.instructure.pandautils.features.notification.preferences.EmailNotificationPreferencesFragment -import com.instructure.pandautils.features.notification.preferences.PushNotificationPreferencesFragment -import com.instructure.pandautils.features.offline.sync.settings.SyncSettingsFragment -import com.instructure.pandautils.fragments.RemoteConfigParamsFragment -import com.instructure.pandautils.room.offline.facade.SyncSettingsFacade -import com.instructure.pandautils.utils.* -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.FragmentApplicationSettingsBinding -import com.instructure.student.mobius.settings.pairobserver.ui.PairObserverFragment -import dagger.hilt.android.AndroidEntryPoint -import kotlinx.coroutines.launch -import javax.inject.Inject - -@ScreenView(SCREEN_VIEW_APPLICATION_SETTINGS) -@PageView(url = "profile/settings") -@AndroidEntryPoint -class ApplicationSettingsFragment : ParentFragment() { - - @Inject - lateinit var syncSettingsFacade: SyncSettingsFacade - - @Inject - lateinit var featureFlagProvider: FeatureFlagProvider - - @Inject - lateinit var networkStateProvider: NetworkStateProvider - - private val binding by viewBinding(FragmentApplicationSettingsBinding::bind) - - override fun title(): String = getString(R.string.settings) - - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? = - inflater.inflate(R.layout.fragment_application_settings, container, false) - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - applyTheme() - setupViews() - - networkStateProvider.isOnlineLiveData.observe(this) { isOnline -> - handleOnlineState(isOnline == true) - } - } - - private fun handleOnlineState(online: Boolean) = with(binding) { - profileSettings.alpha = if (online) 1f else 0.5f - pushNotifications.alpha = if (online) 1f else 0.5f - emailNotifications.alpha = if (online) 1f else 0.5f - pairObserver.alpha = if (online) 1f else 0.5f - legal.alpha = if (online) 1f else 0.5f - } - - override fun applyTheme() = with(binding) { - toolbar.setupAsBackButton(this@ApplicationSettingsFragment) - ViewStyler.themeToolbarColored(requireActivity(), toolbar, ThemePrefs.primaryColor, ThemePrefs.primaryTextColor) - } - - @SuppressLint("SetTextI18n") - private fun setupViews() = with(binding) { - profileSettings.onClickWithRequireNetwork { - val frag = if (ApiPrefs.isStudentView) { - // Profile settings not available in Student View - NothingToSeeHereFragment.newInstance() - } else { - ProfileSettingsFragment.newInstance() - } - - addFragment(frag) - } - - // Account Preferences currently only contains the debug language selector, so we'll hide it in prod - if (BuildConfig.DEBUG) { - accountPreferences.setVisible() - accountPreferences.onClick { addFragment(AccountPreferencesFragment.newInstance()) } - } - - legal.onClickWithRequireNetwork { LegalDialogFragment().show(requireFragmentManager(), null) } - pinAndFingerprint.setGone() // TODO: Wire up once implemented - - if (ApiPrefs.canGeneratePairingCode == true) { - pairObserver.setVisible() - pairObserver.onClickWithRequireNetwork { - addFragment(PairObserverFragment.newInstance()) - } - } - - pushNotifications.onClickWithRequireNetwork { - addFragment(PushNotificationPreferencesFragment.newInstance()) - } - - emailNotifications.onClickWithRequireNetwork { - addFragment(EmailNotificationPreferencesFragment.newInstance()) - } - - about.onClick { - AboutFragment.newInstance().show(childFragmentManager, null) - } - - setUpSyncSettings() - - if (ApiPrefs.canvasForElementary) { - elementaryViewSwitch.isChecked = ApiPrefs.elementaryDashboardEnabledOverride - elementaryViewLayout.setVisible() - ViewStyler.themeSwitch(requireContext(), elementaryViewSwitch, ThemePrefs.brandColor) - elementaryViewSwitch.setOnCheckedChangeListener { _, isChecked -> - ApiPrefs.elementaryDashboardEnabledOverride = isChecked - - val analyticsBundle = Bundle().apply { - putBoolean(AnalyticsParamConstants.MANUAL_C4E_STATE, isChecked) - } - Analytics.logEvent(AnalyticsEventConstants.CHANGED_C4E_MODE, analyticsBundle) - } - } - - setUpAppThemeSelector() - setUpSubscribeToCalendarFeed() - - if (BuildConfig.DEBUG) { - featureFlags.setVisible() - featureFlags.onClick { - addFragment(FeatureFlagsFragment()) - } - - remoteConfigParams.setVisible() - remoteConfigParams.onClick { - addFragment(RemoteConfigParamsFragment()) - } - } - } - - private fun addFragment(fragment: Fragment) { - (activity as? SettingsActivity)?.addFragment(fragment) - } - - private fun setUpAppThemeSelector() { - val initialAppTheme = AppTheme.fromIndex(ThemePrefs.appTheme) - binding.appThemeStatus.setText(initialAppTheme.themeNameRes) - - binding.appThemeContainer.onClick { - AppThemeSelector.showAppThemeSelectorDialog(requireContext(), binding.appThemeStatus) - } - } - - private fun setUpSyncSettings() { - lifecycleScope.launch { - val offlineEnabled = (activity as? SettingsActivity)?.offlineEnabled ?: false - if (offlineEnabled) { - syncSettingsFacade.getSyncSettingsListenable().observe(viewLifecycleOwner) { syncSettings -> - if (syncSettings == null) { - binding.offlineSyncSettingsContainer.setGone() - } else { - binding.offlineSyncSettingsStatus.text = if (syncSettings.autoSyncEnabled) { - getString(syncSettings.syncFrequency.readable) - } else { - getString(R.string.syncSettings_manualDescription) - } - } - } - - binding.offlineSyncSettingsContainer.onClick { - addFragment(SyncSettingsFragment.newInstance()) - } - } else { - binding.offlineContentDivider.setGone() - binding.offlineContentTitle.setGone() - binding.offlineSyncSettingsContainer.setGone() - } - } - } - - private fun setUpSubscribeToCalendarFeed() { - val calendarFeed = ApiPrefs.user?.calendar?.ics - if (!calendarFeed.isNullOrEmpty()) { - binding.subscribeToCalendar.apply { - setVisible() - onClick { - AlertDialog.Builder(requireContext()) - .setMessage(R.string.subscribeToCalendarMessage) - .setPositiveButton(R.string.subscribeButton) { dialog, _ -> - dialog.dismiss() - openCalendarLink(calendarFeed) - } - .setNegativeButton(R.string.cancel, { dialog, _ -> dialog.dismiss() }) - .showThemed() - } - } - } - } - - private fun openCalendarLink(calendarLink: String) { - val webcalLink = calendarLink.replace("https://", "webcal://") - val googleCalendarLink = "https://calendar.google.com/calendar/r?cid=$webcalLink" - val intent = Intent(Intent.ACTION_VIEW) - intent.setData(Uri.parse(googleCalendarLink)) - startActivity(intent) - } -} diff --git a/apps/student/src/main/java/com/instructure/student/router/RouteResolver.kt b/apps/student/src/main/java/com/instructure/student/router/RouteResolver.kt index 77ddbd1324..36bcad986b 100644 --- a/apps/student/src/main/java/com/instructure/student/router/RouteResolver.kt +++ b/apps/student/src/main/java/com/instructure/student/router/RouteResolver.kt @@ -16,6 +16,8 @@ import com.instructure.pandautils.features.notification.preferences.EmailNotific import com.instructure.pandautils.features.notification.preferences.PushNotificationPreferencesFragment import com.instructure.pandautils.features.offline.offlinecontent.OfflineContentFragment import com.instructure.pandautils.features.offline.sync.progress.SyncProgressFragment +import com.instructure.pandautils.features.offline.sync.settings.SyncSettingsFragment +import com.instructure.pandautils.fragments.RemoteConfigParamsFragment import com.instructure.pandautils.utils.Const import com.instructure.student.AnnotationComments.AnnotationCommentListFragment import com.instructure.student.activity.NothingToSeeHereFragment @@ -48,6 +50,7 @@ import com.instructure.student.fragment.DashboardFragment import com.instructure.student.fragment.DiscussionsReplyFragment import com.instructure.student.fragment.DiscussionsUpdateFragment import com.instructure.student.fragment.EditPageDetailsFragment +import com.instructure.student.fragment.FeatureFlagsFragment import com.instructure.student.fragment.InboxComposeMessageFragment import com.instructure.student.fragment.InboxConversationFragment import com.instructure.student.fragment.InboxRecipientsFragment @@ -73,6 +76,7 @@ import com.instructure.student.mobius.assignmentDetails.submissionDetails.ui.Sub import com.instructure.student.mobius.conferences.conference_details.ui.ConferenceDetailsRepositoryFragment import com.instructure.student.mobius.conferences.conference_list.ui.ConferenceListRepositoryFragment import com.instructure.student.mobius.elementary.ElementaryDashboardFragment +import com.instructure.student.mobius.settings.pairobserver.ui.PairObserverFragment import com.instructure.student.mobius.syllabus.ui.SyllabusRepositoryFragment object RouteResolver { @@ -178,6 +182,10 @@ object RouteResolver { cls.isA() -> DiscussionRouterFragment.newInstance(route.canvasContext!!, route) cls.isA() -> OfflineContentFragment.newInstance(route) cls.isA() -> SyncProgressFragment.newInstance() + cls.isA() -> PairObserverFragment.newInstance() + cls.isA() -> SyncSettingsFragment.newInstance() + cls.isA() -> FeatureFlagsFragment() + cls.isA() -> RemoteConfigParamsFragment() cls.isA() -> InternalWebviewFragment.newInstance(route) // Keep this at the end else -> null } diff --git a/apps/student/src/main/res/layout/activity_settings.xml b/apps/student/src/main/res/layout/activity_settings.xml deleted file mode 100644 index 7a115143d5..0000000000 --- a/apps/student/src/main/res/layout/activity_settings.xml +++ /dev/null @@ -1,48 +0,0 @@ - - - - - - - - - - - - - diff --git a/apps/student/src/main/res/layout/fragment_application_settings.xml b/apps/student/src/main/res/layout/fragment_application_settings.xml deleted file mode 100644 index cfc219aaae..0000000000 --- a/apps/student/src/main/res/layout/fragment_application_settings.xml +++ /dev/null @@ -1,347 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/PushNotificationsE2ETest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/PushNotificationsE2ETest.kt index d1ea4269f2..6d81618d9d 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/PushNotificationsE2ETest.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/PushNotificationsE2ETest.kt @@ -7,12 +7,12 @@ import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData import com.instructure.teacher.BuildConfig -import com.instructure.teacher.ui.utils.TeacherTest +import com.instructure.teacher.ui.utils.TeacherComposeTest import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test @HiltAndroidTest -class PushNotificationsE2ETest : TeacherTest() { +class PushNotificationsE2ETest : TeacherComposeTest() { override fun displaysPageObjects() = Unit @@ -41,7 +41,7 @@ class PushNotificationsE2ETest : TeacherTest() { settingsPage.assertPageObjects() Log.d(STEP_TAG, "Open Push Notifications Page.") - settingsPage.openPushNotificationsPage() + settingsPage.clickOnSettingsItem("Push Notifications") Log.d(ASSERTION_TAG, "Assert that the toolbar title is 'Push Notifications' on the Push Notifications Page.") pushNotificationsPage.assertToolbarTitle() 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 bcd86e014d..52b07e2286 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 @@ -30,9 +30,10 @@ import com.instructure.dataseeding.api.ConversationsApi import com.instructure.dataseeding.api.CoursesApi import com.instructure.dataseeding.api.EnrollmentsApi import com.instructure.espresso.ViewUtils +import com.instructure.pandautils.utils.AppTheme import com.instructure.teacher.BuildConfig import com.instructure.teacher.ui.pages.PersonContextPage -import com.instructure.teacher.ui.utils.TeacherTest +import com.instructure.teacher.ui.utils.TeacherComposeTest import com.instructure.teacher.ui.utils.openLeftSideMenu import com.instructure.teacher.ui.utils.seedData import com.instructure.teacher.ui.utils.tokenLogin @@ -40,7 +41,7 @@ import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test @HiltAndroidTest -class SettingsE2ETest : TeacherTest() { +class SettingsE2ETest : TeacherComposeTest() { override fun displaysPageObjects() = Unit @@ -85,10 +86,9 @@ class SettingsE2ETest : TeacherTest() { Log.d(STEP_TAG, "Navigate to User Settings Page.") leftSideNavigationDrawerPage.clickSettingsMenu() - settingsPage.assertPageObjects() Log.d(STEP_TAG, "Open Profile Settings Page.") - settingsPage.openProfileSettingsPage() + settingsPage.clickOnSettingsItem("Profile Settings") profileSettingsPage.assertPageObjects() Log.d(STEP_TAG, "Assert that the '$testPronoun' pronouns are displayed on the Profile Settings Page.") @@ -141,10 +141,9 @@ class SettingsE2ETest : TeacherTest() { Log.d(STEP_TAG, "Navigate to User Settings Page.") leftSideNavigationDrawerPage.clickSettingsMenu() - settingsPage.assertPageObjects() Log.d(STEP_TAG, "Open Profile Settings Page.") - settingsPage.openProfileSettingsPage() + settingsPage.clickOnSettingsItem("Profile Settings") profileSettingsPage.assertPageObjects() Log.d(STEP_TAG, "Click on Edit Pencil Icon on the toolbar.") @@ -159,9 +158,8 @@ class SettingsE2ETest : TeacherTest() { try { Log.d(STEP_TAG, "Check if the user has landed on Settings Page. If yes, navigate back to Profile Settings Page.") //Sometimes in Bitrise it's working different than locally, because in Bitrise sometimes the user has been navigated to Settings Page after saving a new name, - settingsPage.assertPageObjects() - settingsPage.openProfileSettingsPage() - } catch (e: NoMatchingViewException) { + settingsPage.clickOnSettingsItem("Profile Settings") + } catch (e: IllegalStateException) { Log.d(STEP_TAG, "Did not throw the user back to the Settings Page, so the scenario can be continued.") } @@ -208,15 +206,11 @@ class SettingsE2ETest : TeacherTest() { Log.d(STEP_TAG, "Navigate to User Settings Page.") leftSideNavigationDrawerPage.clickSettingsMenu() - settingsPage.assertPageObjects() - - Log.d(STEP_TAG,"Navigate to Settings Page and open App Theme Settings.") - settingsPage.openAppThemeSettings() Log.d(STEP_TAG,"Select Dark App Theme and assert that the App Theme Title and Status has the proper text color (which is used in Dark mode).") - settingsPage.selectAppTheme("Dark") - settingsPage.assertAppThemeTitleTextColor("#FFFFFFFF") //Currently, this color is used in the Dark mode for the AppTheme Title text. - settingsPage.assertAppThemeStatusTextColor("#FF919CA8") //Currently, this color is used in the Dark mode for the AppTheme Status text. + settingsPage.selectAppTheme(AppTheme.DARK) + //settingsPage.assertAppThemeTitleTextColor("#FFFFFFFF") //Currently, this color is used in the Dark mode for the AppTheme Title text. + //settingsPage.assertAppThemeStatusTextColor("#FF919CA8") //Currently, this color is used in the Dark mode for the AppTheme Status text. Log.d(STEP_TAG,"Navigate back to Dashboard. Assert that the 'Courses' label has the proper text color (which is used in Dark mode).") Espresso.pressBack() @@ -230,12 +224,11 @@ class SettingsE2ETest : TeacherTest() { Log.d(STEP_TAG,"Navigate to Settings Page and open App Theme Settings again.") Espresso.pressBack() 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).") - settingsPage.selectAppTheme("Light") - settingsPage.assertAppThemeTitleTextColor("#FF273540") //Currently, this color is used in the Light mode for the AppTheme Title texts. - settingsPage.assertAppThemeStatusTextColor("#FF6A7883") //Currently, this color is used in the Light mode for the AppTheme Status text. + settingsPage.selectAppTheme(AppTheme.LIGHT) + //settingsPage.assertAppThemeTitleTextColor("#FF273540") //Currently, this color is used in the Light mode for the AppTheme Title texts. + //settingsPage.assertAppThemeStatusTextColor("#FF6A7883") //Currently, this color is used in the Light mode for the AppTheme Status text. Log.d(STEP_TAG,"Navigate back to Dashboard. Assert that the 'Courses' label has the proper text color (which is used in Light mode).") Espresso.pressBack() @@ -257,10 +250,9 @@ class SettingsE2ETest : TeacherTest() { Log.d(STEP_TAG,"Navigate to User Settings Page.") leftSideNavigationDrawerPage.clickSettingsMenu() - settingsPage.assertPageObjects() Log.d(STEP_TAG,"Open Legal Page and assert that all the corresponding buttons are displayed.") - settingsPage.openLegalPage() + settingsPage.clickOnSettingsItem("Legal") legalPage.assertPageObjects() } @@ -279,10 +271,9 @@ class SettingsE2ETest : TeacherTest() { 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() + settingsPage.clickOnSettingsItem("About") aboutPage.assertPageObjects() Log.d(STEP_TAG,"Check that domain is equal to: '${teacher.domain}' (teacher's domain).") @@ -313,10 +304,9 @@ class SettingsE2ETest : TeacherTest() { Log.d(STEP_TAG,"Navigate to User Settings Page.") leftSideNavigationDrawerPage.clickSettingsMenu() - settingsPage.assertPageObjects() Log.d(STEP_TAG,"Open Legal Page and assert that all the corresponding buttons are displayed.") - settingsPage.openRateAppDialog() + settingsPage.clickOnSettingsItem("Rate on the Play Store") Log.d(STEP_TAG,"Assert that the five starts are displayed.") settingsPage.assertFiveStarRatingDisplayed() @@ -344,7 +334,7 @@ class SettingsE2ETest : TeacherTest() { RemoteConfigParam.values().forEach { param -> initialValues[param.rc_name] = RemoteConfigUtils.getString(param) } Log.d(STEP_TAG,"Navigate to Remote Config Params Page.") - settingsPage.openRemoteConfigParamsPage() + settingsPage.clickOnSettingsItem("Remote Config Params") Log.d(STEP_TAG,"Click on each EditText, which brings up the soft keyboard, then dismiss it.") RemoteConfigParam.values().forEach { param -> @@ -359,10 +349,9 @@ class SettingsE2ETest : TeacherTest() { Log.d(STEP_TAG,"Navigate back to Settings Page.") Espresso.pressBack() - settingsPage.assertPageObjects() Log.d(STEP_TAG,"Navigate to Remote Config Params page again.") - settingsPage.openRemoteConfigParamsPage() + settingsPage.clickOnSettingsItem("Remote Config Params") Log.d(STEP_TAG,"Assert that all fields have maintained their initial value.") RemoteConfigParam.values().forEach { param -> 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 deleted file mode 100644 index fe53e7e9c8..0000000000 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/SettingsPage.kt +++ /dev/null @@ -1,146 +0,0 @@ -/* - * Copyright (C) 2019 - 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 -import androidx.test.espresso.assertion.ViewAssertions -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.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 - -/** - * Represents the Settings page. - * - * This page provides functionality for interacting with the elements on the Settings page. It contains methods - * for opening various settings pages such as profile settings, push notifications, rate app dialog, legal page, about page, - * feature flags page, and remote config parameters page. It also includes methods for asserting the display of a - * five-star rating, opening the app theme settings, selecting an app theme, and asserting the text color of the app theme title - * and status. This page extends the BasePage class. - */ -class SettingsPage : BasePage(R.id.settingsPage) { - private val toolbar by OnViewWithId(R.id.toolbar) - private val profileSettingLabel by OnViewWithId(R.id.profileButton) - 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) - private val appThemeStatus by OnViewWithId(R.id.appThemeStatus) - - /** - * Opens the profile settings page. - */ - fun openProfileSettingsPage() { - profileSettingLabel.scrollTo().click() - } - - /** - * Opens the push notifications page. - */ - fun openPushNotificationsPage() { - pushNotificationsLabel.scrollTo().click() - } - - /** - * Opens the rate app dialog. - */ - fun openRateAppDialog() { - rateAppLabel.scrollTo().click() - } - - /** - * Opens the legal page. - */ - fun openLegalPage() { - legalLabel.scrollTo().click() - } - - /** - * Opens the about page. - */ - fun openAboutPage() { - aboutLabel.scrollTo().click() - } - - /** - * Opens the feature flags page. - */ - fun openFeatureFlagsPage() { - featureFlagLabel.scrollTo().click() - } - - /** - * Opens the remote config parameters page. - */ - fun openRemoteConfigParamsPage() { - remoteConfigLabel.scrollTo().click() - } - - /** - * Asserts the display of a five-star rating. - */ - fun assertFiveStarRatingDisplayed() { - for (i in 1 until 6) { - Espresso.onView(ViewMatchers.withId(R.id.star + i)) - .check(ViewAssertions.matches(ViewMatchers.isDisplayed())) - } - } - - /** - * Opens the app theme settings. - */ - fun openAppThemeSettings() { - appThemeTitle.scrollTo().click() - } - - /** - * Selects the specified app theme. - * - * @param appTheme The app theme to select. - */ - fun selectAppTheme(appTheme: String) { - onView(withText(appTheme) + withParent(R.id.select_dialog_listview)).click() - } - - /** - * Asserts the text color of the app theme title. - * - * @param expectedTextColor The expected text color of the app theme title. - */ - fun assertAppThemeTitleTextColor(expectedTextColor: String) { - appThemeTitle.check(TextViewColorAssertion(expectedTextColor)) - } - - /** - * Asserts the text color of the app theme status. - * - * @param expectedTextColor The expected text color of the app theme status. - */ - fun assertAppThemeStatusTextColor(expectedTextColor: String) { - appThemeStatus.check(TextViewColorAssertion(expectedTextColor)) - } -} diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/utils/TeacherComposeTest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/utils/TeacherComposeTest.kt index e132120147..a8e76a5dc9 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/utils/TeacherComposeTest.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/utils/TeacherComposeTest.kt @@ -25,6 +25,7 @@ import com.instructure.canvas.espresso.common.pages.compose.CalendarFilterPage import com.instructure.canvas.espresso.common.pages.compose.CalendarScreenPage import com.instructure.canvas.espresso.common.pages.compose.CalendarToDoCreateUpdatePage import com.instructure.canvas.espresso.common.pages.compose.CalendarToDoDetailsPage +import com.instructure.canvas.espresso.common.pages.compose.SettingsPage import com.instructure.teacher.activities.LoginActivity import com.instructure.teacher.ui.pages.ProgressPage @@ -42,4 +43,5 @@ abstract class TeacherComposeTest : TeacherTest() { val calendarToDoDetailsPage = CalendarToDoDetailsPage(composeTestRule) val calendarFilterPage = CalendarFilterPage(composeTestRule) val progressPage = ProgressPage(composeTestRule) + val settingsPage = SettingsPage(composeTestRule) } \ No newline at end of file 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 5e9ed54ad6..5d5b75987c 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 @@ -75,7 +75,6 @@ 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 @@ -116,7 +115,6 @@ abstract class TeacherTest : CanvasTest() { val dashboardPage = DashboardPage() val leftSideNavigationDrawerPage = LeftSideNavigationDrawerPage() val editDashboardPage = EditDashboardPage() - val settingsPage = SettingsPage() val pushNotificationsPage = PushNotificationsPage() val legalPage = LegalPage() val helpPage = HelpPage() diff --git a/apps/teacher/src/main/java/com/instructure/teacher/activities/InitActivity.kt b/apps/teacher/src/main/java/com/instructure/teacher/activities/InitActivity.kt index 04db1e8df1..020acb7ca3 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/activities/InitActivity.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/activities/InitActivity.kt @@ -74,6 +74,7 @@ import com.instructure.pandautils.features.calendar.CalendarFragment import com.instructure.pandautils.features.help.HelpDialogFragment import com.instructure.pandautils.features.inbox.list.InboxFragment import com.instructure.pandautils.features.inbox.list.OnUnreadCountInvalidated +import com.instructure.pandautils.features.settings.SettingsFragment import com.instructure.pandautils.features.themeselector.ThemeSelectorBottomSheet import com.instructure.pandautils.interfaces.NavigationCallbacks import com.instructure.pandautils.models.PushNotification @@ -107,7 +108,6 @@ import com.instructure.teacher.fragments.DashboardFragment import com.instructure.teacher.fragments.EmptyFragment import com.instructure.teacher.fragments.FileListFragment import com.instructure.teacher.fragments.LtiLaunchFragment -import com.instructure.teacher.fragments.SettingsFragment import com.instructure.teacher.fragments.ToDoFragment import com.instructure.teacher.presenters.InitActivityPresenter import com.instructure.teacher.router.RouteMatcher diff --git a/apps/teacher/src/main/java/com/instructure/teacher/di/SettingsModule.kt b/apps/teacher/src/main/java/com/instructure/teacher/di/SettingsModule.kt index caf1706bbf..839edfc27b 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/di/SettingsModule.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/di/SettingsModule.kt @@ -16,24 +16,35 @@ */ package com.instructure.teacher.di +import android.content.Context +import androidx.fragment.app.FragmentActivity import com.instructure.pandautils.features.settings.SettingsBehaviour import com.instructure.pandautils.features.settings.SettingsRouter +import com.instructure.teacher.features.settings.TeacherSettingsBehaviour +import com.instructure.teacher.features.settings.TeacherSettingsRouter import dagger.Module import dagger.Provides import dagger.hilt.InstallIn +import dagger.hilt.android.components.ActivityComponent +import dagger.hilt.android.components.FragmentComponent +import dagger.hilt.android.components.ViewModelComponent +import dagger.hilt.android.qualifiers.ActivityContext import dagger.hilt.components.SingletonComponent @Module -@InstallIn(SingletonComponent::class) -class SettingsModule { - +@InstallIn(FragmentComponent::class) +class SettingsRouterModule { @Provides - fun provideSettingsRouter(): SettingsRouter { - throw NotImplementedError("Not implemented") + fun provideSettingsRouter(activity: FragmentActivity): SettingsRouter { + return TeacherSettingsRouter(activity) } +} +@Module +@InstallIn(ViewModelComponent::class) +class SettingsModule { @Provides fun provideSettingsBehavior(): SettingsBehaviour { - throw NotImplementedError("Not implemented") + return TeacherSettingsBehaviour() } } \ No newline at end of file diff --git a/apps/teacher/src/main/java/com/instructure/teacher/factory/ProfileSettingsFragmentPresenterFactory.kt b/apps/teacher/src/main/java/com/instructure/teacher/factory/ProfileSettingsFragmentPresenterFactory.kt deleted file mode 100644 index bacfa6d671..0000000000 --- a/apps/teacher/src/main/java/com/instructure/teacher/factory/ProfileSettingsFragmentPresenterFactory.kt +++ /dev/null @@ -1,26 +0,0 @@ -/* -* Copyright (C) 2017 - 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.teacher.factory - -import com.instructure.teacher.presenters.ProfileSettingsFragmentPresenter -import com.instructure.teacher.viewinterface.ProfileSettingsFragmentView -import instructure.androidblueprint.PresenterFactory - - -class ProfileSettingsFragmentPresenterFactory : PresenterFactory { - override fun create() = ProfileSettingsFragmentPresenter() -} diff --git a/apps/teacher/src/main/java/com/instructure/teacher/features/settings/TeacherSettingsBehaviour.kt b/apps/teacher/src/main/java/com/instructure/teacher/features/settings/TeacherSettingsBehaviour.kt new file mode 100644 index 0000000000..321845d680 --- /dev/null +++ b/apps/teacher/src/main/java/com/instructure/teacher/features/settings/TeacherSettingsBehaviour.kt @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2024 - 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.teacher.features.settings + +import com.instructure.teacher.BuildConfig +import com.instructure.pandautils.features.settings.SettingsBehaviour +import com.instructure.pandautils.features.settings.SettingsItem +import com.instructure.teacher.R + +class TeacherSettingsBehaviour : SettingsBehaviour { + override val settingsItems: Map> + get() { + val preferencesList = mutableListOf( + SettingsItem.APP_THEME, + SettingsItem.PROFILE_SETTINGS, + SettingsItem.PUSH_NOTIFICATIONS, + SettingsItem.EMAIL_NOTIFICATIONS, + SettingsItem.RATE_APP + ) + if (BuildConfig.DEBUG) { + preferencesList.add(SettingsItem.FEATURE_FLAGS) + preferencesList.add(SettingsItem.REMOTE_CONFIG) + } + return mapOf( + R.string.preferences to preferencesList, + R.string.legal to listOf( + SettingsItem.ABOUT, + SettingsItem.LEGAL + ) + ) + } +} \ No newline at end of file diff --git a/apps/teacher/src/main/java/com/instructure/teacher/features/settings/TeacherSettingsRouter.kt b/apps/teacher/src/main/java/com/instructure/teacher/features/settings/TeacherSettingsRouter.kt new file mode 100644 index 0000000000..f71e7003d1 --- /dev/null +++ b/apps/teacher/src/main/java/com/instructure/teacher/features/settings/TeacherSettingsRouter.kt @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2024 - 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.teacher.features.settings + +import androidx.fragment.app.FragmentActivity +import com.instructure.interactions.router.Route +import com.instructure.pandautils.dialogs.RatingDialog +import com.instructure.pandautils.features.notification.preferences.EmailNotificationPreferencesFragment +import com.instructure.pandautils.features.notification.preferences.PushNotificationPreferencesFragment +import com.instructure.pandautils.features.settings.SettingsRouter +import com.instructure.pandautils.fragments.RemoteConfigParamsFragment +import com.instructure.pandautils.utils.AppType +import com.instructure.teacher.fragments.FeatureFlagsFragment +import com.instructure.teacher.fragments.ProfileFragment +import com.instructure.teacher.router.RouteMatcher + +class TeacherSettingsRouter(private val activity: FragmentActivity) : SettingsRouter { + + override fun navigateToProfileSettings() { + RouteMatcher.route( + activity, + Route(null, ProfileFragment::class.java) + ) + } + + override fun navigateToPushNotificationsSettings() { + RouteMatcher.route( + activity, + Route(null, PushNotificationPreferencesFragment::class.java) + ) + } + + override fun navigateToEmailNotificationsSettings() { + RouteMatcher.route( + activity, + Route(null, EmailNotificationPreferencesFragment::class.java) + ) + } + + override fun navigateToRemoteConfig() { + RouteMatcher.route( + activity, + Route(null, RemoteConfigParamsFragment::class.java) + ) + } + + override fun navigateToFeatureFlags() { + RouteMatcher.route( + activity, + Route(null, FeatureFlagsFragment::class.java) + ) + } + + override fun navigateToRateApp() { + RatingDialog.showRateDialog(activity, AppType.TEACHER) + } +} \ No newline at end of file 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 deleted file mode 100644 index 5fb497aa7e..0000000000 --- a/apps/teacher/src/main/java/com/instructure/teacher/fragments/SettingsFragment.kt +++ /dev/null @@ -1,141 +0,0 @@ -/* -* Copyright (C) 2017 - 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.teacher.fragments - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import com.instructure.canvasapi2.models.CanvasContext -import com.instructure.interactions.router.Route -import com.instructure.pandautils.analytics.SCREEN_VIEW_SETTINGS -import com.instructure.pandautils.analytics.ScreenView -import com.instructure.pandautils.dialogs.RatingDialog -import com.instructure.pandautils.features.about.AboutFragment -import com.instructure.pandautils.features.legal.LegalDialogFragment -import com.instructure.pandautils.features.notification.preferences.EmailNotificationPreferencesFragment -import com.instructure.pandautils.features.notification.preferences.PushNotificationPreferencesFragment -import com.instructure.pandautils.fragments.BasePresenterFragment -import com.instructure.pandautils.fragments.RemoteConfigParamsFragment -import com.instructure.pandautils.utils.AppTheme -import com.instructure.pandautils.utils.AppThemeSelector -import com.instructure.pandautils.utils.AppType -import com.instructure.pandautils.utils.Const -import com.instructure.pandautils.utils.NullableParcelableArg -import com.instructure.pandautils.utils.ThemePrefs -import com.instructure.pandautils.utils.ViewStyler -import com.instructure.pandautils.utils.makeBundle -import com.instructure.pandautils.utils.onClick -import com.instructure.pandautils.utils.setVisible -import com.instructure.teacher.BuildConfig -import com.instructure.teacher.R -import com.instructure.teacher.databinding.FragmentSettingsBinding -import com.instructure.teacher.factory.ProfileSettingsFragmentPresenterFactory -import com.instructure.teacher.presenters.ProfileSettingsFragmentPresenter -import com.instructure.teacher.router.RouteMatcher -import com.instructure.teacher.utils.setupBackButton -import com.instructure.teacher.viewinterface.ProfileSettingsFragmentView - -@ScreenView(SCREEN_VIEW_SETTINGS) -class SettingsFragment : BasePresenterFragment< - ProfileSettingsFragmentPresenter, - ProfileSettingsFragmentView, - FragmentSettingsBinding>(), - ProfileSettingsFragmentView { - - private var canvasContext: CanvasContext? by NullableParcelableArg(key = Const.CANVAS_CONTEXT) - - override val bindingInflater: (layoutInflater: LayoutInflater) -> FragmentSettingsBinding = FragmentSettingsBinding::inflate - - override fun onActivityCreated(savedInstanceState: Bundle?) = with(binding) { - super.onActivityCreated(savedInstanceState) - versionTextView.text = getString(R.string.fullVersion, BuildConfig.VERSION_NAME, BuildConfig.VERSION_CODE) - profileButton.onClick { - RouteMatcher.route( - requireActivity(), - Route(null, ProfileFragment::class.java, canvasContext, canvasContext?.makeBundle() ?: Bundle()) - ) - } - rateButton.onClick { RatingDialog.showRateDialog(requireActivity(), AppType.TEACHER) } - aboutButton.onClick { AboutFragment.newInstance().show(childFragmentManager, null) } - legalButton.onClick { LegalDialogFragment().show(requireFragmentManager(), null) } - notificationPreferenesButton.onClick { - RouteMatcher.route( - requireActivity(), - Route(null, PushNotificationPreferencesFragment::class.java, canvasContext, canvasContext?.makeBundle() ?: Bundle()) - ) - } - emailNotifications.onClick { - RouteMatcher.route( - requireActivity(), - Route(null, EmailNotificationPreferencesFragment::class.java, canvasContext, canvasContext?.makeBundle() ?: Bundle()) - ) - } - if (BuildConfig.DEBUG) { - featureFlagButton.setVisible() - featureFlagButton.onClick { - RouteMatcher.route( - requireActivity(), - Route(null, FeatureFlagsFragment::class.java, canvasContext, canvasContext?.makeBundle() ?: Bundle()) - ) - } - - remoteConfigButton.setVisible() - remoteConfigButton.onClick { - RouteMatcher.route( - requireActivity(), - Route(null, RemoteConfigParamsFragment::class.java, canvasContext, canvasContext?.makeBundle() ?: Bundle()) - ) - } - } - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - setUpAppThemeSelector() - } - - private fun setUpAppThemeSelector() = with(binding) { - val initialAppTheme = AppTheme.fromIndex(ThemePrefs.appTheme) - appThemeStatus.setText(initialAppTheme.themeNameRes) - - appThemeContainer.onClick { - AppThemeSelector.showAppThemeSelectorDialog(requireContext(), appThemeStatus) - } - } - - override fun getPresenterFactory() = ProfileSettingsFragmentPresenterFactory() - - override fun onReadySetGo(presenter: ProfileSettingsFragmentPresenter) { - setupToolbar() - } - - fun setupToolbar() = with(binding) { - toolbar.setupBackButton(this@SettingsFragment) - toolbar.title = getString(R.string.settings) - ViewStyler.themeToolbarColored(requireActivity(), toolbar, ThemePrefs.primaryColor, ThemePrefs.primaryTextColor) - } - - override fun onRefreshStarted() {} - - override fun onRefreshFinished() {} - - override fun onPresenterPrepared(presenter: ProfileSettingsFragmentPresenter) {} - - companion object { - fun newInstance(args: Bundle) = SettingsFragment().apply { arguments = args } - } -} diff --git a/apps/teacher/src/main/java/com/instructure/teacher/presenters/ProfileSettingsFragmentPresenter.kt b/apps/teacher/src/main/java/com/instructure/teacher/presenters/ProfileSettingsFragmentPresenter.kt deleted file mode 100644 index c9f9af2a1e..0000000000 --- a/apps/teacher/src/main/java/com/instructure/teacher/presenters/ProfileSettingsFragmentPresenter.kt +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright (C) 2017 - 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.teacher.presenters - - -import com.instructure.teacher.viewinterface.ProfileSettingsFragmentView -import instructure.androidblueprint.FragmentPresenter - -class ProfileSettingsFragmentPresenter : FragmentPresenter() { - - override fun refresh(forceNetwork: Boolean) {} - - override fun loadData(forceNetwork: Boolean) {} -} diff --git a/apps/teacher/src/main/java/com/instructure/teacher/router/RouteMatcher.kt b/apps/teacher/src/main/java/com/instructure/teacher/router/RouteMatcher.kt index 42938cdbb6..b53a6b2ee5 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/router/RouteMatcher.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/router/RouteMatcher.kt @@ -50,6 +50,7 @@ import com.instructure.pandautils.features.dashboard.edit.EditDashboardFragment import com.instructure.pandautils.features.discussion.details.DiscussionDetailsWebViewFragment import com.instructure.pandautils.features.discussion.router.DiscussionRouterFragment import com.instructure.pandautils.features.inbox.list.InboxFragment +import com.instructure.pandautils.features.settings.SettingsFragment import com.instructure.pandautils.fragments.HtmlContentFragment import com.instructure.pandautils.loaders.OpenMediaAsyncTaskLoader import com.instructure.pandautils.utils.Const @@ -105,7 +106,6 @@ import com.instructure.teacher.fragments.ProfileFragment import com.instructure.teacher.fragments.QuizDetailsFragment import com.instructure.teacher.fragments.QuizListFragment import com.instructure.teacher.fragments.QuizPreviewWebviewFragment -import com.instructure.teacher.fragments.SettingsFragment import com.instructure.teacher.fragments.SpeedGraderQuizWebViewFragment import com.instructure.teacher.fragments.ViewHtmlFragment import com.instructure.teacher.fragments.ViewImageFragment @@ -510,7 +510,7 @@ object RouteMatcher : BaseRouteMatcher() { CreateDiscussionFragment::class.java.isAssignableFrom(cls) -> fragment = CreateDiscussionFragment.newInstance(route.arguments) CreateOrEditAnnouncementFragment::class.java.isAssignableFrom(cls) -> fragment = CreateOrEditAnnouncementFragment .newInstance(route.arguments) - SettingsFragment::class.java.isAssignableFrom(cls) -> fragment = SettingsFragment.newInstance(route.arguments) + SettingsFragment::class.java.isAssignableFrom(cls) -> fragment = SettingsFragment.newInstance(route) ProfileEditFragment::class.java.isAssignableFrom(cls) -> fragment = ProfileEditFragment.newInstance(route.arguments) LtiLaunchFragment::class.java.isAssignableFrom(cls) -> fragment = LtiLaunchFragment.newInstance(route.arguments) PeopleListFragment::class.java.isAssignableFrom(cls) -> fragment = PeopleListFragment.newInstance(canvasContext!!) diff --git a/apps/teacher/src/main/java/com/instructure/teacher/router/RouteResolver.kt b/apps/teacher/src/main/java/com/instructure/teacher/router/RouteResolver.kt index 725dcd94a5..2e707076f8 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/router/RouteResolver.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/router/RouteResolver.kt @@ -15,6 +15,7 @@ import com.instructure.pandautils.features.discussion.router.DiscussionRouterFra import com.instructure.pandautils.features.inbox.list.InboxFragment import com.instructure.pandautils.features.notification.preferences.EmailNotificationPreferencesFragment import com.instructure.pandautils.features.notification.preferences.PushNotificationPreferencesFragment +import com.instructure.pandautils.features.settings.SettingsFragment import com.instructure.pandautils.fragments.HtmlContentFragment import com.instructure.pandautils.fragments.RemoteConfigParamsFragment import com.instructure.pandautils.utils.Const @@ -62,7 +63,6 @@ import com.instructure.teacher.fragments.ProfileFragment import com.instructure.teacher.fragments.QuizDetailsFragment import com.instructure.teacher.fragments.QuizListFragment import com.instructure.teacher.fragments.QuizPreviewWebviewFragment -import com.instructure.teacher.fragments.SettingsFragment import com.instructure.teacher.fragments.SpeedGraderQuizWebViewFragment import com.instructure.teacher.fragments.ViewHtmlFragment import com.instructure.teacher.fragments.ViewImageFragment @@ -189,7 +189,7 @@ object RouteResolver { } else if (CreateOrEditAnnouncementFragment::class.java.isAssignableFrom(cls)) { fragment = CreateOrEditAnnouncementFragment.newInstance(route.arguments) } else if (SettingsFragment::class.java.isAssignableFrom(cls)) { - fragment = SettingsFragment.newInstance(route.arguments) + fragment = SettingsFragment.newInstance(route) } else if (ProfileEditFragment::class.java.isAssignableFrom(cls)) { fragment = ProfileEditFragment.newInstance(route.arguments) } else if (FeatureFlagsFragment::class.java.isAssignableFrom(cls)) { diff --git a/apps/teacher/src/main/java/com/instructure/teacher/viewinterface/ProfileSettingsFragmentView.kt b/apps/teacher/src/main/java/com/instructure/teacher/viewinterface/ProfileSettingsFragmentView.kt deleted file mode 100644 index 81848bb3ac..0000000000 --- a/apps/teacher/src/main/java/com/instructure/teacher/viewinterface/ProfileSettingsFragmentView.kt +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Copyright (C) 2017 - 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.teacher.viewinterface - -import instructure.androidblueprint.FragmentViewInterface - -interface ProfileSettingsFragmentView : FragmentViewInterface \ No newline at end of file diff --git a/apps/teacher/src/main/res/layout/fragment_settings.xml b/apps/teacher/src/main/res/layout/fragment_settings.xml deleted file mode 100644 index d6ca01c912..0000000000 --- a/apps/teacher/src/main/res/layout/fragment_settings.xml +++ /dev/null @@ -1,229 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/interaction/SettingsInteractionTest.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/interaction/SettingsInteractionTest.kt index 5566d10995..24fb85ea8d 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/interaction/SettingsInteractionTest.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/interaction/SettingsInteractionTest.kt @@ -23,18 +23,6 @@ abstract class SettingsInteractionTest : CanvasComposeTest() { private val settingsPage = SettingsPage(composeTestRule) - @Test - fun openAppThemeSelector() { - val data = initData() - goToSettings(data) - - composeTestRule.waitForIdle() - - settingsPage.assertSettingsItemDisplayed("App Theme") - settingsPage.clickOnSettingsItem("App Theme") - settingsPage.assertThemeSelectorOpened() - } - @Test fun openAboutScreen() { val data = initData() diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/pages/compose/SettingsPage.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/pages/compose/SettingsPage.kt index 64c0707c31..40c9c0ee9f 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/pages/compose/SettingsPage.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/pages/compose/SettingsPage.kt @@ -16,14 +16,26 @@ package com.instructure.canvas.espresso.common.pages.compose import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.assertIsNotDisplayed import androidx.compose.ui.test.hasAnyDescendant import androidx.compose.ui.test.hasTestTag import androidx.compose.ui.test.hasText import androidx.compose.ui.test.junit4.ComposeTestRule +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.performScrollTo +import androidx.test.espresso.matcher.RootMatchers +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.uiautomator.UiDevice 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.onViewWithText +import com.instructure.espresso.page.withText +import com.instructure.espresso.retry +import com.instructure.pandautils.utils.AppTheme class SettingsPage(private val composeTestRule: ComposeTestRule) : BasePage() { @@ -36,15 +48,24 @@ class SettingsPage(private val composeTestRule: ComposeTestRule) : BasePage() { } fun clickOnSettingsItem(title: String) { - composeTestRule.onNode( - hasTestTag("settingsItem").and(hasAnyDescendant(hasText(title))), - useUnmergedTree = true - ) - .performClick() - } + val nodeMatcher = hasTestTag("settingsItem").and(hasAnyDescendant(hasText(title))) + retry(catchBlock = { + val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) + val y = device.displayHeight / 2 + val x = device.displayWidth / 2 + device.swipe( + x, + y, + x, + 0, + 10 + ) + }) { + composeTestRule.onNode(nodeMatcher, useUnmergedTree = true) + .assertIsDisplayed() + .performClick() + } - fun assertThemeSelectorOpened() { - onViewWithText("Select app theme").assertDisplayed() } fun assertAboutDialogOpened() { @@ -54,4 +75,100 @@ class SettingsPage(private val composeTestRule: ComposeTestRule) : BasePage() { fun assertLegalDialogOpened() { onViewWithText("Legal").assertDisplayed() } + + fun assertFiveStarRatingDisplayed() { + onView(withText("How are we doing?")) + .inRoot(RootMatchers.isDialog()) + .assertDisplayed() + } + + fun selectAppTheme(appTheme: AppTheme) { + val testTag = when (appTheme) { + AppTheme.LIGHT -> { + "lightThemeButton" + } + + AppTheme.DARK -> { + "darkThemeButton" + } + + AppTheme.SYSTEM -> { + "systemThemeButton" + } + } + + composeTestRule + .onNodeWithTag(testTag) + .performScrollTo() + .performClick() + } + + fun clickOnSyncSettingsItem() { + retry(catchBlock = { + val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) + val y = device.displayHeight / 2 + val x = device.displayWidth / 2 + device.swipe( + x, + y, + x, + 0, + 10 + ) + }) { + composeTestRule.onNode( + hasTestTag("syncSettingsItem"), + useUnmergedTree = true + ).assertIsDisplayed() + .performClick() + } + } + + fun clickOnSubscribeButton() { + onViewWithText("Subscribe").click() + } + + fun assertOfflineContentDisplayed() { + retry(catchBlock = { + val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) + val y = device.displayHeight / 2 + val x = device.displayWidth / 2 + device.swipe( + x, + y, + x, + 0, + 10 + ) + }) { + composeTestRule.onNode( + hasTestTag("syncSettingsItem"), + useUnmergedTree = true + ).assertIsDisplayed() + } + } + + fun assertOfflineSyncSettingsStatus(status: String) { + retry(catchBlock = { + val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) + val y = device.displayHeight / 2 + val x = device.displayWidth / 2 + device.swipe( + x, + y, + x, + 0, + 10 + ) + }) { + composeTestRule.onNodeWithText(status, useUnmergedTree = true).assertIsDisplayed() + } + } + + fun assertOfflineContentNotDisplayed() { + composeTestRule.onNode( + hasTestTag("syncSettingsItem"), + useUnmergedTree = true + ).assertIsNotDisplayed() + } } \ No newline at end of file diff --git a/libs/pandares/src/main/res/drawable-night/ic_panda_dark.xml b/libs/pandares/src/main/res/drawable-night/ic_panda_dark.xml new file mode 100644 index 0000000000..8f2db767bc --- /dev/null +++ b/libs/pandares/src/main/res/drawable-night/ic_panda_dark.xml @@ -0,0 +1,214 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/libs/pandares/src/main/res/drawable-night/ic_panda_light.xml b/libs/pandares/src/main/res/drawable-night/ic_panda_light.xml new file mode 100644 index 0000000000..542ada6198 --- /dev/null +++ b/libs/pandares/src/main/res/drawable-night/ic_panda_light.xml @@ -0,0 +1,183 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/libs/pandares/src/main/res/drawable-night/ic_panda_system.xml b/libs/pandares/src/main/res/drawable-night/ic_panda_system.xml new file mode 100644 index 0000000000..051ed6dde7 --- /dev/null +++ b/libs/pandares/src/main/res/drawable-night/ic_panda_system.xml @@ -0,0 +1,403 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/libs/pandares/src/main/res/drawable/ic_panda_dark.xml b/libs/pandares/src/main/res/drawable/ic_panda_dark.xml new file mode 100644 index 0000000000..a98bc8645a --- /dev/null +++ b/libs/pandares/src/main/res/drawable/ic_panda_dark.xml @@ -0,0 +1,214 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/libs/pandares/src/main/res/drawable/ic_panda_light.xml b/libs/pandares/src/main/res/drawable/ic_panda_light.xml new file mode 100644 index 0000000000..c8fdf56baf --- /dev/null +++ b/libs/pandares/src/main/res/drawable/ic_panda_light.xml @@ -0,0 +1,183 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/libs/pandares/src/main/res/drawable/ic_panda_system.xml b/libs/pandares/src/main/res/drawable/ic_panda_system.xml new file mode 100644 index 0000000000..09b4fbc009 --- /dev/null +++ b/libs/pandares/src/main/res/drawable/ic_panda_system.xml @@ -0,0 +1,403 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/libs/pandares/src/main/res/values/strings.xml b/libs/pandares/src/main/res/values/strings.xml index df28ab9326..ba56b4a51a 100644 --- a/libs/pandares/src/main/res/values/strings.xml +++ b/libs/pandares/src/main/res/values/strings.xml @@ -1332,6 +1332,7 @@ Light Dark Same as device + System Canvas is now available in dark theme Choose app theme diff --git a/libs/pandautils/src/androidTest/java/com/instructure/pandautils/compose/features/settings/SettingsScreenTest.kt b/libs/pandautils/src/androidTest/java/com/instructure/pandautils/compose/features/settings/SettingsScreenTest.kt index d236a06ac5..ec431b0c93 100644 --- a/libs/pandautils/src/androidTest/java/com/instructure/pandautils/compose/features/settings/SettingsScreenTest.kt +++ b/libs/pandautils/src/androidTest/java/com/instructure/pandautils/compose/features/settings/SettingsScreenTest.kt @@ -16,7 +16,6 @@ package com.instructure.pandautils.compose.features.settings import android.content.Context -import androidx.compose.ui.test.assertHasClickAction import androidx.compose.ui.test.hasAnyDescendant import androidx.compose.ui.test.hasTestTag import androidx.compose.ui.test.hasText @@ -24,13 +23,15 @@ import androidx.compose.ui.test.junit4.createComposeRule import androidx.compose.ui.test.onNodeWithText import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.uiautomator.UiDevice +import com.instructure.espresso.retry +import com.instructure.pandautils.R +import com.instructure.pandautils.features.settings.SettingsItem import com.instructure.pandautils.features.settings.SettingsScreen +import com.instructure.pandautils.features.settings.SettingsUiState import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith -import com.instructure.pandautils.R -import com.instructure.pandautils.features.settings.SettingsItem -import com.instructure.pandautils.features.settings.SettingsUiState @RunWith(AndroidJUnit4::class) class SettingsScreenTest { @@ -57,10 +58,12 @@ class SettingsScreenTest { ) val uiState = SettingsUiState( - items, + items = items, + homeroomView = true, offlineState = R.string.daily, - appTheme = R.string.appThemeLight - ) {} + appTheme = R.string.appThemeLight, + actionHandler = {} + ) composeTestRule.setContent { SettingsScreen(uiState = uiState) {} } @@ -68,17 +71,32 @@ class SettingsScreenTest { items.forEach { (title, items) -> composeTestRule.onNodeWithText(context.getString(title)).assertExists() items.forEach { item -> - composeTestRule.onNodeWithText(context.getString(item.res)).assertExists() - composeTestRule.onNode( - hasTestTag("settingsItem").and( - hasAnyDescendant( - hasText( - context.getString(item.res) + retry(catchBlock = { + val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) + val y = device.displayHeight / 2 + val x = device.displayWidth / 2 + device.swipe( + x, + y, + x, + 0, + 10 + ) + }) { + val testTag = when (item) { + SettingsItem.OFFLINE_SYNCHRONIZATION -> "syncSettingsItem" + else -> "settingsItem" + } + composeTestRule.onNode( + hasTestTag(testTag).and( + hasAnyDescendant( + hasText( + context.getString(item.res) + ) ) - ) - ), useUnmergedTree = true - ) - .assertHasClickAction() + ), useUnmergedTree = true + ).assertExists() + } } } } diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/compose/composables/LabelValueSwitch.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/compose/composables/LabelValueSwitch.kt new file mode 100644 index 0000000000..6451ef6e1f --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/compose/composables/LabelValueSwitch.kt @@ -0,0 +1,101 @@ +/* + * Copyright (C) 2024 - 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.compose.composables + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material.Switch +import androidx.compose.material.SwitchDefaults +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.instructure.canvasapi2.utils.ContextKeeper +import com.instructure.pandautils.R +import com.instructure.pandautils.utils.ThemePrefs + +@Composable +fun LabelValueSwitch( + label: String, + value: String?, + isChecked: Boolean, + modifier: Modifier = Modifier, + onCheckedChange: (Boolean) -> Unit +) { + var checked by remember { mutableStateOf(isChecked) } + Row( + modifier = modifier + .fillMaxWidth() + .clickable { + checked = !checked + onCheckedChange(checked) + }, + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Column { + Text( + modifier = Modifier.testTag("label"), + text = label, + style = TextStyle(fontSize = 16.sp, color = colorResource(id = R.color.textDarkest)) + ) + value?.let { + Text( + modifier = Modifier + .padding(top = 4.dp) + .testTag("value"), + text = it, + style = TextStyle( + fontSize = 12.sp, + color = colorResource(id = R.color.textDark) + ) + ) + } + } + Switch( + checked = checked, onCheckedChange = { + checked = it + onCheckedChange(checked) + }, colors = SwitchDefaults.colors( + checkedThumbColor = Color(ThemePrefs.brandColor), + ) + ) + } + +} + +@Preview +@Composable +fun LabelValueSwitchPreview() { + ContextKeeper.appContext = LocalContext.current + LabelValueSwitch(label = "Label", value = "Value", isChecked = true, onCheckedChange = {}) +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/settings/SettingsBehaviour.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/settings/SettingsBehaviour.kt index b0a297c68a..2601f2ce04 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/settings/SettingsBehaviour.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/settings/SettingsBehaviour.kt @@ -21,6 +21,8 @@ import com.instructure.pandautils.R interface SettingsBehaviour { val settingsItems: Map> + + suspend fun applyAppSpecificColorSettings() = Unit } enum class SettingsItem(val res: Int) { @@ -34,6 +36,8 @@ enum class SettingsItem(val res: Int) { ABOUT(R.string.about), LEGAL(R.string.legal), RATE_APP(R.string.rateOnThePlayStore), - FEATURE_FLAGS(R.string.about), + FEATURE_FLAGS(R.string.featureFlags), REMOTE_CONFIG(R.string.remoteConfigParamsTitle), + ACCOUNT_PREFERENCES(R.string.accountPreferences), + HOMEROOM_VIEW(R.string.settingsHomeroomView) } \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/settings/SettingsFragment.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/settings/SettingsFragment.kt index b85e981b83..a1e7df852e 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/settings/SettingsFragment.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/settings/SettingsFragment.kt @@ -17,25 +17,39 @@ package com.instructure.pandautils.features.settings +import android.content.Intent +import android.graphics.Bitmap +import android.graphics.Canvas +import android.net.Uri +import android.os.Build import android.os.Bundle import android.view.LayoutInflater import android.view.View +import android.view.ViewAnimationUtils import android.view.ViewGroup +import android.widget.ImageView +import androidx.appcompat.app.AlertDialog import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue -import androidx.compose.ui.platform.ComposeView import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels import androidx.lifecycle.lifecycleScope +import com.instructure.canvasapi2.utils.ApiPrefs +import com.instructure.interactions.router.Route +import com.instructure.pandautils.R +import com.instructure.pandautils.binding.viewBinding +import com.instructure.pandautils.databinding.FragmentSettingsBinding import com.instructure.pandautils.features.about.AboutFragment import com.instructure.pandautils.features.legal.LegalDialogFragment -import com.instructure.pandautils.utils.AppThemeSelector import com.instructure.pandautils.utils.ThemePrefs import com.instructure.pandautils.utils.ViewStyler import com.instructure.pandautils.utils.collectOneOffEvents +import com.instructure.pandautils.utils.showThemed import dagger.hilt.android.AndroidEntryPoint import javax.inject.Inject +const val OFFLINE_ENABLED = "offlineEnabled" + @AndroidEntryPoint class SettingsFragment : Fragment() { @@ -44,16 +58,85 @@ class SettingsFragment : Fragment() { private val viewModel: SettingsViewModel by viewModels() + private val binding: FragmentSettingsBinding by viewBinding(FragmentSettingsBinding::bind) + + private var bitmap: Bitmap? = null + private var xPos = 0 + private var yPos = 0 + private var scrollValue = 0 + private var appThemeChange = false + + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + ThemePrefs.reapplyCanvasTheme(requireActivity()) + } + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? - ): View { + ): View? { + val view = layoutInflater.inflate(R.layout.fragment_settings, container, false) + + bitmap = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + savedInstanceState?.getParcelable("bitmap", Bitmap::class.java) + } else { + savedInstanceState?.getParcelable("bitmap") + } + + bitmap?.let { + view.findViewById(R.id.backImage).setImageBitmap(it) + } + return view + } + + + override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState(outState) + if (appThemeChange) { + val w = requireView().measuredWidth + val h = requireView().measuredHeight + val bitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888) + val canvas = Canvas(bitmap) + requireView().draw(canvas) + outState.putParcelable("bitmap", bitmap) + outState.putInt("xPos", xPos) + outState.putInt("yPos", yPos) + outState.putInt("scrollValue", scrollValue) + } + } + + override fun onViewStateRestored(savedInstanceState: Bundle?) { + super.onViewStateRestored(savedInstanceState) + + xPos = savedInstanceState?.getInt("xPos") ?: (requireView().measuredWidth / 2) + yPos = savedInstanceState?.getInt("yPos") ?: (requireView().measuredHeight / 2) + + if (bitmap != null) { + binding.settingsComposeView.post { + val w = requireView().measuredWidth + val h = requireView().measuredHeight + val finalRadius = kotlin.math.sqrt((w * w + h * h).toDouble()).toFloat() + val anim = ViewAnimationUtils.createCircularReveal( + binding.settingsComposeView, + xPos, + yPos, + 0f, + finalRadius + ) + anim.duration = 1000 + anim.start() + } + } + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) ViewStyler.setStatusBarDark(requireActivity(), ThemePrefs.primaryColor) lifecycleScope.collectOneOffEvents(viewModel.events, ::handleAction) - return ComposeView(requireActivity()).apply { + binding.settingsComposeView.apply { setContent { val uiState by viewModel.uiState.collectAsState() SettingsScreen(uiState) { @@ -68,6 +151,12 @@ class SettingsFragment : Fragment() { is SettingsViewModelAction.Navigate -> { navigate(action.item) } + + is SettingsViewModelAction.AppThemeClickPosition -> { + xPos = action.xPos + yPos = action.yPos + appThemeChange = true + } } } @@ -77,13 +166,6 @@ class SettingsFragment : Fragment() { AboutFragment.newInstance().show(childFragmentManager, null) } - SettingsItem.APP_THEME -> { - AppThemeSelector.showAppThemeSelectorDialog( - requireContext(), - viewModel::onThemeSelected - ) - } - SettingsItem.PROFILE_SETTINGS -> { settingsRouter.navigateToProfileSettings() } @@ -104,9 +186,65 @@ class SettingsFragment : Fragment() { LegalDialogFragment().show(childFragmentManager, null) } + SettingsItem.OFFLINE_SYNCHRONIZATION -> { + settingsRouter.navigateToSyncSettings() + } + + SettingsItem.SUBSCRIBE_TO_CALENDAR -> { + ApiPrefs.user?.calendar?.ics?.let { calendarFeed -> + AlertDialog.Builder(requireContext()) + .setMessage(R.string.subscribeToCalendarMessage) + .setPositiveButton(R.string.subscribeButton) { dialog, _ -> + dialog.dismiss() + openCalendarLink(calendarFeed) + } + .setNegativeButton(R.string.cancel, { dialog, _ -> dialog.dismiss() }) + .showThemed() + } + } + + SettingsItem.ACCOUNT_PREFERENCES -> { + settingsRouter.navigateToAccountPreferences() + } + + SettingsItem.REMOTE_CONFIG -> { + settingsRouter.navigateToRemoteConfig() + } + + SettingsItem.FEATURE_FLAGS -> { + settingsRouter.navigateToFeatureFlags() + } + + SettingsItem.RATE_APP -> { + settingsRouter.navigateToRateApp() + } + else -> { } } } + + private fun openCalendarLink(calendarLink: String) { + val webcalLink = calendarLink.replace("https://", "webcal://") + val googleCalendarLink = "https://calendar.google.com/calendar/r?cid=$webcalLink" + val intent = Intent(Intent.ACTION_VIEW) + intent.setData(Uri.parse(googleCalendarLink)) + startActivity(intent) + } + + companion object { + fun newInstance(route: Route): SettingsFragment { + return SettingsFragment().apply { + arguments = route.arguments + } + } + + fun makeRoute(offlineEnabled: Boolean): Route { + return Route(SettingsFragment::class.java, null, Bundle().apply { + putBoolean(OFFLINE_ENABLED, offlineEnabled) + }) + } + } } + diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/settings/SettingsRouter.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/settings/SettingsRouter.kt index c56c1ba84f..ae46f85a56 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/settings/SettingsRouter.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/settings/SettingsRouter.kt @@ -17,12 +17,22 @@ package com.instructure.pandautils.features.settings interface SettingsRouter { - fun navigateToProfileSettings() + fun navigateToProfileSettings() = Unit - fun navigateToPushNotificationsSettings() + fun navigateToPushNotificationsSettings() = Unit - fun navigateToEmailNotificationsSettings() + fun navigateToEmailNotificationsSettings() = Unit - fun navigateToPairWithObserver() + fun navigateToPairWithObserver() = Unit + + fun navigateToSyncSettings() = Unit + + fun navigateToAccountPreferences() = Unit + + fun navigateToRemoteConfig() = Unit + + fun navigateToFeatureFlags() = Unit + + fun navigateToRateApp() = Unit } \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/settings/SettingsScreen.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/settings/SettingsScreen.kt index 42f203dccd..83a0ff6bf0 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/settings/SettingsScreen.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/settings/SettingsScreen.kt @@ -16,26 +16,60 @@ */ package com.instructure.pandautils.features.settings +import android.content.res.Configuration +import androidx.annotation.DrawableRes +import androidx.annotation.StringRes +import androidx.compose.foundation.Image +import androidx.compose.foundation.border import androidx.compose.foundation.clickable +import androidx.compose.foundation.gestures.Orientation +import androidx.compose.foundation.gestures.scrollable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.Divider +import androidx.compose.material.IconButton import androidx.compose.material.Scaffold import androidx.compose.material.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.layout.positionInRoot import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import com.instructure.canvasapi2.utils.ContextKeeper import com.instructure.pandautils.R import com.instructure.pandautils.compose.CanvasTheme import com.instructure.pandautils.compose.composables.CanvasThemedAppBar +import com.instructure.pandautils.compose.composables.LabelValueSwitch import com.instructure.pandautils.compose.composables.LabelValueVerticalItem +import com.instructure.pandautils.utils.AppTheme +import com.instructure.pandautils.utils.ThemePrefs @Composable fun SettingsScreen( @@ -64,8 +98,16 @@ fun SettingsScreen( @Composable private fun SettingsContent(uiState: SettingsUiState, modifier: Modifier = Modifier) { - - LazyColumn(modifier = modifier) { + val scrollState = rememberScrollState() + LaunchedEffect(Unit) { + scrollState.scrollTo(uiState.scrollValue) + } + LazyColumn ( + modifier = modifier + .fillMaxSize() + .scrollable(state = scrollState, orientation = Orientation.Vertical) + .testTag("settingsList") + ) { uiState.items.onEachIndexed { index, entry -> val (sectionTitle, items) = entry item { @@ -83,18 +125,33 @@ private fun SettingsContent(uiState: SettingsUiState, modifier: Modifier = Modif items(items) { settingsItem -> when (settingsItem) { SettingsItem.APP_THEME -> { - AppThemeItem(uiState) + AppThemeItem(uiState.appTheme) { appTheme, x, y -> + uiState.actionHandler( + SettingsAction.SetAppTheme( + appTheme, + x, + y, + scrollState.value + ) + ) + } } SettingsItem.OFFLINE_SYNCHRONIZATION -> { OfflineSyncItem(uiState) } + SettingsItem.HOMEROOM_VIEW -> { + HomeroomViewItem(uiState.homeroomView) { + uiState.actionHandler(SettingsAction.SetHomeroomView(it)) + } + } + else -> { LabelValueVerticalItem( modifier = Modifier .clickable { - uiState.onClick(settingsItem) + uiState.actionHandler(SettingsAction.ItemClicked(settingsItem)) } .padding( horizontal = 16.dp, @@ -121,47 +178,169 @@ private fun SettingsContent(uiState: SettingsUiState, modifier: Modifier = Modif } @Composable -private fun AppThemeItem(uiState: SettingsUiState) { - return LabelValueVerticalItem( +private fun AppThemeItem(appTheme: Int, appThemeSelected: (AppTheme, Int, Int) -> Unit) { + Column(modifier = Modifier.testTag("settingsItem")) { + Text( + modifier = Modifier + .testTag("label") + .padding(horizontal = 16.dp, vertical = 8.dp), + text = stringResource(id = R.string.appThemeSettingsTitle), + style = TextStyle(fontSize = 16.sp, color = colorResource(id = R.color.textDarkest)) + ) + } + Row( modifier = Modifier - .clickable { - uiState.onClick(SettingsItem.APP_THEME) - } - .padding( - horizontal = 16.dp, - vertical = 4.dp + .padding(vertical = 8.dp) + .fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceEvenly + ) { + AppThemeButton( + icon = R.drawable.ic_panda_light, + title = R.string.appThemeLight, + testTag = "lightThemeButton", + selected = appTheme == AppTheme.LIGHT.ordinal + ) { + appThemeSelected( + AppTheme.LIGHT, + it.x.toInt(), + it.y.toInt() ) - .testTag("settingsItem"), - label = stringResource(R.string.appThemeSettingsTitle), - value = uiState.appTheme?.let { stringResource(it) } - ) + } + AppThemeButton( + icon = R.drawable.ic_panda_dark, + title = R.string.appThemeDark, + testTag = "darkThemeButton", + selected = appTheme == AppTheme.DARK.ordinal + ) { + appThemeSelected( + AppTheme.DARK, + it.x.toInt(), + it.y.toInt() + ) + } + AppThemeButton( + icon = R.drawable.ic_panda_system, + title = R.string.appThemeAuto, + testTag = "systemThemeButton", + selected = appTheme == AppTheme.SYSTEM.ordinal + ) { + appThemeSelected( + AppTheme.SYSTEM, + it.x.toInt(), + it.y.toInt() + ) + } + } +} + +@Composable +private fun AppThemeButton( + @DrawableRes icon: Int, + @StringRes title: Int, + selected: Boolean, + testTag: String, + modifier: Modifier = Modifier, + onClick: (Offset) -> Unit +) { + var position by remember { mutableStateOf(Offset.Zero) } + Column(modifier = modifier, horizontalAlignment = Alignment.CenterHorizontally) { + IconButton( + modifier = Modifier + .testTag(testTag) + .onGloballyPositioned { + position = Offset( + x = it.positionInRoot().x + it.size.width / 2, + y = it.positionInRoot().y + it.size.height / 2 + ) + } + .border( + 2.dp, + if (selected) Color(ThemePrefs.brandColor) else Color.Transparent, + CircleShape + ), onClick = { onClick(position) }) { + Image( + modifier = Modifier + .padding(4.dp) + .size(88.dp) + .aspectRatio(1f), + painter = painterResource(id = icon), + contentDescription = stringResource(id = title) + ) + } + Text( + modifier = Modifier + .padding(top = 8.dp), + text = stringResource(title), + style = TextStyle( + fontSize = 12.sp, + color = colorResource(id = R.color.textDarkest), + textAlign = TextAlign.Center + ) + ) + } + } @Composable private fun OfflineSyncItem(uiState: SettingsUiState) { - return LabelValueVerticalItem( + LabelValueVerticalItem( modifier = Modifier .clickable { - uiState.onClick(SettingsItem.OFFLINE_SYNCHRONIZATION) + uiState.actionHandler(SettingsAction.ItemClicked(SettingsItem.OFFLINE_SYNCHRONIZATION)) } .padding( horizontal = 16.dp, vertical = 4.dp ) - .testTag("settingsItem"), + .testTag("syncSettingsItem"), label = stringResource(R.string.offlineSyncSettingsTitle), value = uiState.offlineState?.let { stringResource(it) } ) } @Composable -@Preview -fun SettingsScreenPreview() { +private fun HomeroomViewItem(checked: Boolean, onCheckedChange: (Boolean) -> Unit) { + LabelValueSwitch( + modifier = Modifier.padding(horizontal = 16.dp, vertical = 4.dp), + label = stringResource(id = R.string.settingsHomeroomView), + value = stringResource( + id = R.string.settingsElementaryViewSubtitle + ), + isChecked = checked, + onCheckedChange = onCheckedChange + ) +} + +@Composable +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) +fun SettingsScreenDarkPreview() { ContextKeeper.appContext = LocalContext.current SettingsScreen(SettingsUiState( - mapOf( - R.string.preferences to listOf(SettingsItem.APP_THEME), + appTheme = AppTheme.SYSTEM.ordinal, + homeroomView = true, + actionHandler = {}, + items = mapOf( + R.string.preferences to listOf(SettingsItem.APP_THEME, SettingsItem.HOMEROOM_VIEW), R.string.legal to listOf(SettingsItem.ABOUT, SettingsItem.LEGAL) - ) - ) {}, navigationActionClick = {}) + ), + ), navigationActionClick = {}) +} + +@Composable +@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO) +fun SettingsScreenLightPreview() { + ContextKeeper.appContext = LocalContext.current + SettingsScreen(SettingsUiState( + appTheme = AppTheme.SYSTEM.ordinal, + homeroomView = false, + actionHandler = {}, + items = mapOf( + R.string.preferences to listOf( + SettingsItem.APP_THEME, + SettingsItem.HOMEROOM_VIEW, + SettingsItem.PROFILE_SETTINGS + ), + R.string.legal to listOf(SettingsItem.ABOUT, SettingsItem.LEGAL) + ), + ), navigationActionClick = {}) } \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/settings/SettingsUiState.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/settings/SettingsUiState.kt index 707e32be23..b749eb7075 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/settings/SettingsUiState.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/settings/SettingsUiState.kt @@ -16,13 +16,26 @@ */ package com.instructure.pandautils.features.settings +import com.instructure.pandautils.utils.AppTheme + data class SettingsUiState( + val appTheme: Int, + val homeroomView: Boolean, + val scrollValue: Int = 0, val items: Map> = emptyMap(), val offlineState: Int? = null, - val appTheme: Int? = null, - val onClick: (SettingsItem) -> Unit + val actionHandler: (SettingsAction) -> Unit ) sealed class SettingsViewModelAction { data class Navigate(val item: SettingsItem) : SettingsViewModelAction() + data class AppThemeClickPosition(val xPos: Int, val yPos: Int, val scrollValue: Int) : SettingsViewModelAction() +} + +sealed class SettingsAction { + data class SetAppTheme(val appTheme: AppTheme, val xPos: Int, val yPos: Int, val scrollValue: Int) : SettingsAction() + + data class SetHomeroomView(val homeroomView: Boolean) : SettingsAction() + + data class ItemClicked(val settingsItem: SettingsItem) : SettingsAction() } \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/settings/SettingsViewModel.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/settings/SettingsViewModel.kt index 32b0e26bba..a58714e3d5 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/settings/SettingsViewModel.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/settings/SettingsViewModel.kt @@ -16,15 +16,24 @@ */ package com.instructure.pandautils.features.settings +import android.annotation.SuppressLint +import android.content.Context +import android.content.res.Configuration +import androidx.appcompat.app.AppCompatDelegate +import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.asFlow import androidx.lifecycle.viewModelScope +import com.instructure.canvasapi2.utils.ApiPrefs import com.instructure.pandautils.R import com.instructure.pandautils.room.offline.facade.SyncSettingsFacade import com.instructure.pandautils.utils.AppTheme +import com.instructure.pandautils.utils.ColorKeeper import com.instructure.pandautils.utils.ThemePrefs import dagger.hilt.android.lifecycle.HiltViewModel +import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.collectLatest @@ -33,22 +42,43 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import javax.inject.Inject +@SuppressLint("StaticFieldLeak") @HiltViewModel class SettingsViewModel @Inject constructor( - settingsBehaviour: SettingsBehaviour, - themePrefs: ThemePrefs, - private val syncSettingsFacade: SyncSettingsFacade + savedStateHandle: SavedStateHandle, + private val settingsBehaviour: SettingsBehaviour, + @ApplicationContext private val context: Context, + private val syncSettingsFacade: SyncSettingsFacade, + private val colorKeeper: ColorKeeper, + private val themePrefs: ThemePrefs, + private val apiPrefs: ApiPrefs ) : ViewModel() { - private val _uiState = MutableStateFlow(SettingsUiState(onClick = this::onItemClick)) + private val _uiState = MutableStateFlow( + SettingsUiState( + appTheme = themePrefs.appTheme, + homeroomView = apiPrefs.elementaryDashboardEnabledOverride, + actionHandler = this::actionHandler + ) + ) val uiState = _uiState.asStateFlow() private val _events = Channel() val events = _events.receiveAsFlow() + private val offlineEnabled = savedStateHandle.get(OFFLINE_ENABLED) ?: false + private val scrollValue = savedStateHandle.get("scrollValue") ?: 0 + init { - if (settingsBehaviour.settingsItems.any { it.value.contains(SettingsItem.OFFLINE_SYNCHRONIZATION) }) { + val items = settingsBehaviour.settingsItems.filter { + if (it.value.contains(SettingsItem.OFFLINE_SYNCHRONIZATION)) { + offlineEnabled + } else { + true + } + } + if (items.any { it.value.contains(SettingsItem.OFFLINE_SYNCHRONIZATION) }) { viewModelScope.launch { syncSettingsFacade.getSyncSettingsListenable().asFlow() .collectLatest { syncSettings -> @@ -64,17 +94,61 @@ class SettingsViewModel @Inject constructor( } } } - val appTheme = AppTheme.fromIndex(themePrefs.appTheme) - _uiState.update { it.copy(items = settingsBehaviour.settingsItems, appTheme = appTheme.themeNameRes) } + _uiState.update { + it.copy( + items = items, + scrollValue = scrollValue + ) + } } - fun onThemeSelected(theme: AppTheme) { - _uiState.update { it.copy(appTheme = theme.themeNameRes) } + private fun actionHandler(action: SettingsAction) { + when (action) { + is SettingsAction.SetAppTheme -> { + viewModelScope.launch { + _events.send( + SettingsViewModelAction.AppThemeClickPosition( + action.xPos, + action.yPos, + action.scrollValue + ) + ) + } + setAppTheme(action.appTheme) + } + + is SettingsAction.SetHomeroomView -> { + apiPrefs.elementaryDashboardEnabledOverride = action.homeroomView + _uiState.update { + it.copy(homeroomView = action.homeroomView) + } + } + + is SettingsAction.ItemClicked -> { + viewModelScope.launch { + _events.send(SettingsViewModelAction.Navigate(action.settingsItem)) + } + } + } } - private fun onItemClick(item: SettingsItem) { + private fun setAppTheme(appTheme: AppTheme) { + AppCompatDelegate.setDefaultNightMode(appTheme.nightModeType) + themePrefs.appTheme = appTheme.ordinal + + val nightModeFlags: Int = + context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK + colorKeeper.darkTheme = nightModeFlags == Configuration.UI_MODE_NIGHT_YES + themePrefs.isThemeApplied = false viewModelScope.launch { - _events.send(SettingsViewModelAction.Navigate(item)) + delay(100) + settingsBehaviour.applyAppSpecificColorSettings() + } + + _uiState.update { + it.copy( + appTheme = appTheme.ordinal, + ) } } } \ No newline at end of file diff --git a/libs/pandautils/src/main/res/layout/fragment_settings.xml b/libs/pandautils/src/main/res/layout/fragment_settings.xml new file mode 100644 index 0000000000..b360ea4e8f --- /dev/null +++ b/libs/pandautils/src/main/res/layout/fragment_settings.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + diff --git a/libs/pandautils/src/test/java/com/instructure/pandautils/features/settings/SettingsViewModelTest.kt b/libs/pandautils/src/test/java/com/instructure/pandautils/features/settings/SettingsViewModelTest.kt index 12c4d5de12..95c8561bd4 100644 --- a/libs/pandautils/src/test/java/com/instructure/pandautils/features/settings/SettingsViewModelTest.kt +++ b/libs/pandautils/src/test/java/com/instructure/pandautils/features/settings/SettingsViewModelTest.kt @@ -15,21 +15,27 @@ */ package com.instructure.pandautils.features.settings +import android.content.Context import androidx.arch.core.executor.testing.InstantTaskExecutorRule import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleRegistry import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.SavedStateHandle +import com.instructure.canvasapi2.utils.ApiPrefs import com.instructure.pandautils.R import com.instructure.pandautils.features.offline.sync.settings.SyncFrequency import com.instructure.pandautils.room.offline.entities.SyncSettingsEntity import com.instructure.pandautils.room.offline.facade.SyncSettingsFacade import com.instructure.pandautils.utils.AppTheme +import com.instructure.pandautils.utils.ColorKeeper import com.instructure.pandautils.utils.ThemePrefs import io.mockk.coEvery import io.mockk.every import io.mockk.mockk +import io.mockk.slot import io.mockk.unmockkAll +import io.mockk.verify import junit.framework.TestCase.assertEquals import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -55,14 +61,21 @@ class SettingsViewModelTest { private val lifecycleOwner: LifecycleOwner = mockk(relaxed = true) private val lifecycleRegistry = LifecycleRegistry(lifecycleOwner) + private val savedStateHandle: SavedStateHandle = mockk(relaxed = true) private val settingsBehaviour: SettingsBehaviour = mockk(relaxed = true) private val syncSettingsFacade: SyncSettingsFacade = mockk(relaxed = true) private val themePrefs: ThemePrefs = mockk(relaxed = true) + private val context: Context = mockk(relaxed = true) + private val colorKeeper: ColorKeeper = mockk(relaxed = true) + private val apiPrefs: ApiPrefs = mockk(relaxed = true) @Before fun setup() { lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE) Dispatchers.setMain(testDispatcher) + + every { savedStateHandle.get(OFFLINE_ENABLED) } returns true + every { savedStateHandle.get("scrollValue") } returns 0 } @After @@ -90,12 +103,12 @@ class SettingsViewModelTest { val uiState = viewModel.uiState.value - assertEquals(R.string.appThemeLight, uiState.appTheme) + assertEquals(AppTheme.LIGHT.ordinal, uiState.appTheme) assertEquals(items, uiState.items) } @Test - fun `Change app theme`() { + fun `Change app theme`() = runTest { val items = mapOf( R.string.preferences to listOf( SettingsItem.APP_THEME, @@ -111,11 +124,49 @@ class SettingsViewModelTest { val viewModel = createViewModel() - viewModel.onThemeSelected(AppTheme.DARK) + val uiState = viewModel.uiState.value + + uiState.actionHandler(SettingsAction.SetAppTheme(AppTheme.DARK, 0, 0, 0)) + + verify { + themePrefs.appTheme = AppTheme.DARK.ordinal + } + assertEquals(AppTheme.DARK.ordinal, viewModel.uiState.value.appTheme) + + val events = mutableListOf() + backgroundScope.launch(testDispatcher) { + viewModel.events.toList(events) + assertEquals(SettingsViewModelAction.AppThemeClickPosition(0, 0, 0), events.last()) + } + } + + @Test + fun `Set homeroom view action`() = runTest { + val items = mapOf( + R.string.preferences to listOf( + SettingsItem.APP_THEME, + SettingsItem.PROFILE_SETTINGS, + SettingsItem.PUSH_NOTIFICATIONS, + SettingsItem.EMAIL_NOTIFICATIONS, + SettingsItem.HOMEROOM_VIEW + ), + R.string.legal to listOf(SettingsItem.ABOUT, SettingsItem.LEGAL) + ) + every { settingsBehaviour.settingsItems } returns items + + every { themePrefs.appTheme } returns 0 + + val viewModel = createViewModel() val uiState = viewModel.uiState.value - assertEquals(R.string.appThemeDark, uiState.appTheme) + uiState.actionHandler(SettingsAction.SetHomeroomView(true)) + assertEquals(true, viewModel.uiState.value.homeroomView) + + verify { + apiPrefs.elementaryDashboardEnabledOverride = true + } + } @Test @@ -165,7 +216,7 @@ class SettingsViewModelTest { val viewModel = createViewModel() viewModel.uiState.value.items.flatMap { it.value }.forEach { item -> - viewModel.uiState.value.onClick(item) + viewModel.uiState.value.actionHandler(SettingsAction.ItemClicked(item)) val events = mutableListOf() backgroundScope.launch(testDispatcher) { viewModel.events.toList(events) @@ -176,6 +227,6 @@ class SettingsViewModelTest { private fun createViewModel(): SettingsViewModel { - return SettingsViewModel(settingsBehaviour, themePrefs, syncSettingsFacade) + return SettingsViewModel(savedStateHandle, settingsBehaviour, context, syncSettingsFacade, colorKeeper, themePrefs, apiPrefs) } } \ No newline at end of file From 63a018f34b6b3a80d3d6f04cd9732d0cf999d1cc Mon Sep 17 00:00:00 2001 From: domonkosadam <53480952+domonkosadam@users.noreply.github.com> Date: Fri, 8 Nov 2024 13:45:41 +0100 Subject: [PATCH 05/31] [MBL-18065][Student] Wi-Fi only toggle confirmation dialog fix (#2629) refs: MBL-18065 affects: Student release note: none --- .../features/offline/sync/settings/SyncSettingsFragment.kt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/sync/settings/SyncSettingsFragment.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/sync/settings/SyncSettingsFragment.kt index e6f79f7f18..0172254fbf 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/sync/settings/SyncSettingsFragment.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/sync/settings/SyncSettingsFragment.kt @@ -86,6 +86,9 @@ class SyncSettingsFragment : Fragment(), FragmentInteractions { confirmationCallback(true) dialog.dismiss() } + .setOnDismissListener { + confirmationCallback(false) + } .showThemed() } From bcccdc108f58505e95f37d12bed46158900f8ed8 Mon Sep 17 00:00:00 2001 From: domonkosadam <53480952+domonkosadam@users.noreply.github.com> Date: Fri, 8 Nov 2024 13:46:19 +0100 Subject: [PATCH 06/31] [MBL-18048][Teacher] Deeplink support for people list (#2630) refs: MBL-18048 affects: Teacher release note: External links are now supported to People List screen. --- .../src/main/java/com/instructure/teacher/router/RouteMatcher.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/teacher/src/main/java/com/instructure/teacher/router/RouteMatcher.kt b/apps/teacher/src/main/java/com/instructure/teacher/router/RouteMatcher.kt index b53a6b2ee5..acb8653247 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/router/RouteMatcher.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/router/RouteMatcher.kt @@ -240,6 +240,7 @@ object RouteMatcher : BaseRouteMatcher() { DiscussionRouterFragment::class.java ) ) + routes.add(Route(courseOrGroup("/:course_id/users"), PeopleListFragment::class.java)) } private fun initClassMap() { From 332f914d2fb854c5a23a250bd0c71c555c3b1223 Mon Sep 17 00:00:00 2001 From: Kristof Nemere <109959688+kristofnemere@users.noreply.github.com> Date: Tue, 12 Nov 2024 11:13:16 +0100 Subject: [PATCH 07/31] [MBL-17677][Parent] Course details - Syllabus tab refs: MBL-17677 affects: Parent release note: none --- .../details/CourseDetailsScreenTest.kt | 4 ++ .../courses/details/CourseDetailsFragment.kt | 43 +++++++++++++++++-- .../courses/details/CourseDetailsScreen.kt | 39 ++++++++++++++--- .../courses/details/CourseDetailsUiState.kt | 7 ++- .../courses/details/CourseDetailsViewModel.kt | 14 +++++- .../courses/details/ParentGradesScreen.kt | 11 ++++- .../courses/details/SyllabusScreen.kt | 21 +++++++-- .../details/CourseDetailsViewModelTest.kt | 28 ++++++++++-- libs/pandares/src/main/res/values/strings.xml | 1 + .../createupdate/CreateUpdateToDoFragment.kt | 2 - .../features/grades/GradesScreen.kt | 4 +- .../features/grades/GradesUiState.kt | 2 +- .../features/grades/GradesViewModel.kt | 1 + .../features/grades/GradesViewModelTest.kt | 4 +- 14 files changed, 156 insertions(+), 25 deletions(-) diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/compose/courses/details/CourseDetailsScreenTest.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/compose/courses/details/CourseDetailsScreenTest.kt index 13a7470308..a913acaa3c 100644 --- a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/compose/courses/details/CourseDetailsScreenTest.kt +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/compose/courses/details/CourseDetailsScreenTest.kt @@ -50,6 +50,7 @@ class CourseDetailsScreenTest { isLoading = true ), actionHandler = {}, + applyOnWebView = {}, navigationActionClick = {} ) } @@ -67,6 +68,7 @@ class CourseDetailsScreenTest { isError = true ), actionHandler = {}, + applyOnWebView = {}, navigationActionClick = {} ) } @@ -89,6 +91,7 @@ class CourseDetailsScreenTest { tabs = listOf(TabType.SYLLABUS, TabType.SUMMARY) ), actionHandler = {}, + applyOnWebView = {}, navigationActionClick = {} ) } @@ -124,6 +127,7 @@ class CourseDetailsScreenTest { tabs = listOf(TabType.SYLLABUS) ), actionHandler = {}, + applyOnWebView = {}, navigationActionClick = {} ) } diff --git a/apps/parent/src/main/java/com/instructure/parentapp/features/courses/details/CourseDetailsFragment.kt b/apps/parent/src/main/java/com/instructure/parentapp/features/courses/details/CourseDetailsFragment.kt index a81aa9f527..d61d02c010 100644 --- a/apps/parent/src/main/java/com/instructure/parentapp/features/courses/details/CourseDetailsFragment.kt +++ b/apps/parent/src/main/java/com/instructure/parentapp/features/courses/details/CourseDetailsFragment.kt @@ -21,6 +21,7 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import android.webkit.WebView import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.platform.ComposeView @@ -28,9 +29,11 @@ import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels import androidx.lifecycle.lifecycleScope import androidx.navigation.fragment.findNavController +import com.instructure.pandautils.navigation.WebViewRouter import com.instructure.pandautils.utils.ViewStyler import com.instructure.pandautils.utils.collectOneOffEvents import com.instructure.pandautils.utils.studentColor +import com.instructure.pandautils.views.CanvasWebView import com.instructure.parentapp.util.ParentPrefs import com.instructure.parentapp.util.navigation.Navigation import dagger.hilt.android.AndroidEntryPoint @@ -43,8 +46,29 @@ class CourseDetailsFragment : Fragment() { @Inject lateinit var navigation: Navigation + @Inject + lateinit var webViewRouter: WebViewRouter + private val viewModel: CourseDetailsViewModel by viewModels() + private val embeddedWebViewCallback = object : CanvasWebView.CanvasEmbeddedWebViewCallback { + override fun launchInternalWebViewFragment(url: String) = webViewRouter.launchInternalWebViewFragment(url, null) + + override fun shouldLaunchInternalWebViewFragment(url: String): Boolean = true + } + + private val webViewClientCallback = object : CanvasWebView.CanvasWebViewClientCallback { + override fun openMediaFromWebView(mime: String, url: String, filename: String) = webViewRouter.openMedia(url) + + override fun onPageStartedCallback(webView: WebView, url: String) = Unit + + override fun onPageFinishedCallback(webView: WebView, url: String) = Unit + + override fun canRouteInternallyDelegate(url: String) = webViewRouter.canRouteInternally(url) + + override fun routeInternallyCallback(url: String) = webViewRouter.routeInternally(url) + } + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, @@ -55,9 +79,18 @@ class CourseDetailsFragment : Fragment() { return ComposeView(requireActivity()).apply { setContent { val uiState by viewModel.uiState.collectAsState() - CourseDetailsScreen(uiState, viewModel::handleAction) { - findNavController().popBackStack() - } + CourseDetailsScreen( + uiState = uiState, + actionHandler = viewModel::handleAction, + applyOnWebView = { + addVideoClient(requireActivity()) + canvasEmbeddedWebViewCallback = embeddedWebViewCallback + canvasWebViewClientCallback = webViewClientCallback + }, + navigationActionClick = { + findNavController().popBackStack() + } + ) } } } @@ -77,6 +110,10 @@ class CourseDetailsFragment : Fragment() { is CourseDetailsViewModelAction.NavigateToAssignmentDetails -> { navigation.navigate(activity, navigation.assignmentDetailsRoute(action.courseId, action.assignmentId)) } + + is CourseDetailsViewModelAction.OpenLtiScreen -> { + navigation.navigate(activity, navigation.ltiLaunchRoute(action.url, "")) + } } } } diff --git a/apps/parent/src/main/java/com/instructure/parentapp/features/courses/details/CourseDetailsScreen.kt b/apps/parent/src/main/java/com/instructure/parentapp/features/courses/details/CourseDetailsScreen.kt index 5f4ed66ef5..905c07d28a 100644 --- a/apps/parent/src/main/java/com/instructure/parentapp/features/courses/details/CourseDetailsScreen.kt +++ b/apps/parent/src/main/java/com/instructure/parentapp/features/courses/details/CourseDetailsScreen.kt @@ -17,7 +17,6 @@ package com.instructure.parentapp.features.courses.details -import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding @@ -25,6 +24,7 @@ import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.material.FloatingActionButton import androidx.compose.material.Icon +import androidx.compose.material.IconButton import androidx.compose.material.Scaffold import androidx.compose.material.Surface import androidx.compose.material.Tab @@ -52,6 +52,7 @@ import com.instructure.pandautils.compose.composables.CanvasThemedAppBar import com.instructure.pandautils.compose.composables.ErrorContent import com.instructure.pandautils.compose.composables.Loading import com.instructure.pandautils.utils.ThemePrefs +import com.instructure.pandautils.views.CanvasWebView import kotlinx.coroutines.launch @@ -59,6 +60,7 @@ import kotlinx.coroutines.launch internal fun CourseDetailsScreen( uiState: CourseDetailsUiState, actionHandler: (CourseDetailsAction) -> Unit, + applyOnWebView: (CanvasWebView.() -> Unit), navigationActionClick: () -> Unit ) { CanvasTheme { @@ -90,6 +92,7 @@ internal fun CourseDetailsScreen( uiState = uiState, actionHandler = actionHandler, navigationActionClick = navigationActionClick, + applyOnWebView = applyOnWebView, modifier = Modifier.fillMaxSize() ) } @@ -98,12 +101,12 @@ internal fun CourseDetailsScreen( } } -@OptIn(ExperimentalFoundationApi::class) @Composable private fun CourseDetailsScreenContent( uiState: CourseDetailsUiState, actionHandler: (CourseDetailsAction) -> Unit, navigationActionClick: () -> Unit, + applyOnWebView: (CanvasWebView.() -> Unit), modifier: Modifier = Modifier ) { val pagerState = rememberPagerState { uiState.tabs.size } @@ -118,7 +121,7 @@ private fun CourseDetailsScreenContent( val tabContents: List<@Composable () -> Unit> = uiState.tabs.map { when (it) { TabType.GRADES -> { - { ParentGradesScreen(actionHandler) } + { ParentGradesScreen(actionHandler, uiState.forceRefreshGrades) } } TabType.FRONT_PAGE -> { @@ -126,7 +129,14 @@ private fun CourseDetailsScreenContent( } TabType.SYLLABUS -> { - { SyllabusScreen() } + { + SyllabusScreen( + uiState.syllabus, + applyOnWebView + ) { ltiUrl -> + actionHandler(CourseDetailsAction.OnLtiClicked(ltiUrl)) + } + } } TabType.SUMMARY -> { @@ -144,7 +154,24 @@ private fun CourseDetailsScreenContent( navigationActionClick() }, backgroundColor = Color(uiState.studentColor), - contentColor = colorResource(id = R.color.textLightest) + contentColor = colorResource(id = R.color.textLightest), + actions = { + if (uiState.tabs.size > 1) { + IconButton( + onClick = { + actionHandler(CourseDetailsAction.Refresh) + } + ) { + Icon( + painter = painterResource(id = R.drawable.ic_refresh), + tint = colorResource(id = R.color.textLightest), + contentDescription = stringResource( + id = R.string.a11y_refresh + ) + ) + } + } + } ) }, content = { padding -> @@ -219,6 +246,7 @@ private fun CourseDetailsScreenPreview() { ) ), actionHandler = {}, + applyOnWebView = {}, navigationActionClick = {} ) } @@ -233,6 +261,7 @@ private fun CourseDetailsScreenErrorPreview() { isError = true, ), actionHandler = {}, + applyOnWebView = {}, navigationActionClick = {} ) } diff --git a/apps/parent/src/main/java/com/instructure/parentapp/features/courses/details/CourseDetailsUiState.kt b/apps/parent/src/main/java/com/instructure/parentapp/features/courses/details/CourseDetailsUiState.kt index 0ff03ca2f0..57eca7e28c 100644 --- a/apps/parent/src/main/java/com/instructure/parentapp/features/courses/details/CourseDetailsUiState.kt +++ b/apps/parent/src/main/java/com/instructure/parentapp/features/courses/details/CourseDetailsUiState.kt @@ -30,7 +30,9 @@ data class CourseDetailsUiState( val isLoading: Boolean = false, val isError: Boolean = false, val tabs: List = emptyList(), - val currentTab: TabType? = null + val currentTab: TabType? = null, + val syllabus: String = "", + val forceRefreshGrades: Boolean = false ) enum class TabType(@StringRes val labelRes: Int) { @@ -45,9 +47,12 @@ sealed class CourseDetailsAction { data object SendAMessage : CourseDetailsAction() data class NavigateToAssignmentDetails(val courseId: Long, val assignmentId: Long) : CourseDetailsAction() data class CurrentTabChanged(val newTab: TabType) : CourseDetailsAction() + data class OnLtiClicked(val url: String) : CourseDetailsAction() + data object GradesRefreshed : CourseDetailsAction() } sealed class CourseDetailsViewModelAction { data class NavigateToComposeMessageScreen(val options: InboxComposeOptions) : CourseDetailsViewModelAction() data class NavigateToAssignmentDetails(val courseId: Long, val assignmentId: Long) : CourseDetailsViewModelAction() + data class OpenLtiScreen(val url: String) : CourseDetailsViewModelAction() } diff --git a/apps/parent/src/main/java/com/instructure/parentapp/features/courses/details/CourseDetailsViewModel.kt b/apps/parent/src/main/java/com/instructure/parentapp/features/courses/details/CourseDetailsViewModel.kt index 869a6214ef..9cb48bb2dc 100644 --- a/apps/parent/src/main/java/com/instructure/parentapp/features/courses/details/CourseDetailsViewModel.kt +++ b/apps/parent/src/main/java/com/instructure/parentapp/features/courses/details/CourseDetailsViewModel.kt @@ -96,7 +96,9 @@ class CourseDetailsViewModel @Inject constructor( it.copy( courseName = course.name, isLoading = false, - tabs = tabTypes + tabs = tabTypes, + syllabus = course.syllabusBody.orEmpty(), + forceRefreshGrades = forceRefresh ) } } catch { @@ -132,6 +134,16 @@ class CourseDetailsViewModel @Inject constructor( } } } + + is CourseDetailsAction.OnLtiClicked -> { + viewModelScope.launch { + _events.send(CourseDetailsViewModelAction.OpenLtiScreen(action.url)) + } + } + + is CourseDetailsAction.GradesRefreshed -> { + _uiState.update { it.copy(forceRefreshGrades = false) } + } } } diff --git a/apps/parent/src/main/java/com/instructure/parentapp/features/courses/details/ParentGradesScreen.kt b/apps/parent/src/main/java/com/instructure/parentapp/features/courses/details/ParentGradesScreen.kt index e92bf4a303..08b79802d5 100644 --- a/apps/parent/src/main/java/com/instructure/parentapp/features/courses/details/ParentGradesScreen.kt +++ b/apps/parent/src/main/java/com/instructure/parentapp/features/courses/details/ParentGradesScreen.kt @@ -23,6 +23,7 @@ import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.lifecycle.viewmodel.compose.viewModel +import com.instructure.pandautils.features.grades.GradesAction import com.instructure.pandautils.features.grades.GradesScreen import com.instructure.pandautils.features.grades.GradesViewModel import com.instructure.pandautils.features.grades.GradesViewModelAction @@ -30,7 +31,8 @@ import com.instructure.pandautils.features.grades.GradesViewModelAction @Composable internal fun ParentGradesScreen( - actionHandler: (CourseDetailsAction) -> Unit + actionHandler: (CourseDetailsAction) -> Unit, + forceRefresh: Boolean ) { val gradesViewModel: GradesViewModel = viewModel() val gradeUiState by remember { gradesViewModel.uiState }.collectAsState() @@ -45,5 +47,10 @@ internal fun ParentGradesScreen( } } - GradesScreen(gradeUiState, gradesViewModel::handleAction) + if (forceRefresh) { + gradesViewModel.handleAction(GradesAction.Refresh(true)) + actionHandler(CourseDetailsAction.GradesRefreshed) + } else { + GradesScreen(gradeUiState, gradesViewModel::handleAction) + } } diff --git a/apps/parent/src/main/java/com/instructure/parentapp/features/courses/details/SyllabusScreen.kt b/apps/parent/src/main/java/com/instructure/parentapp/features/courses/details/SyllabusScreen.kt index e617430e09..8a063991a0 100644 --- a/apps/parent/src/main/java/com/instructure/parentapp/features/courses/details/SyllabusScreen.kt +++ b/apps/parent/src/main/java/com/instructure/parentapp/features/courses/details/SyllabusScreen.kt @@ -17,11 +17,26 @@ package com.instructure.parentapp.features.courses.details -import androidx.compose.material.Text +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.instructure.pandautils.compose.composables.ComposeCanvasWebViewWrapper +import com.instructure.pandautils.views.CanvasWebView @Composable -internal fun SyllabusScreen() { - Text(text = "Syllabus") +internal fun SyllabusScreen( + syllabus: String, + applyOnWebView: (CanvasWebView) -> Unit, + onLtiButtonPressed: (String) -> Unit +) { + Column(modifier = Modifier.verticalScroll(rememberScrollState())) { + ComposeCanvasWebViewWrapper( + html = syllabus, + onLtiButtonPressed = onLtiButtonPressed, + applyOnWebView = applyOnWebView + ) + } } diff --git a/apps/parent/src/test/java/com/instructure/parentapp/features/courses/details/CourseDetailsViewModelTest.kt b/apps/parent/src/test/java/com/instructure/parentapp/features/courses/details/CourseDetailsViewModelTest.kt index 8ad64e5104..d25c5afeb9 100644 --- a/apps/parent/src/test/java/com/instructure/parentapp/features/courses/details/CourseDetailsViewModelTest.kt +++ b/apps/parent/src/test/java/com/instructure/parentapp/features/courses/details/CourseDetailsViewModelTest.kt @@ -128,7 +128,8 @@ class CourseDetailsViewModelTest { studentColor = 1, isLoading = false, isError = false, - tabs = listOf(TabType.GRADES, TabType.SYLLABUS) + tabs = listOf(TabType.GRADES, TabType.SYLLABUS), + syllabus = "Syllabus body" ) Assert.assertEquals(expected, viewModel.uiState.value) @@ -152,7 +153,8 @@ class CourseDetailsViewModelTest { studentColor = 1, isLoading = false, isError = false, - tabs = listOf(TabType.GRADES, TabType.SYLLABUS, TabType.SUMMARY) + tabs = listOf(TabType.GRADES, TabType.SYLLABUS, TabType.SUMMARY), + syllabus = "Syllabus body" ) Assert.assertEquals(expected, viewModel.uiState.value) @@ -203,10 +205,15 @@ class CourseDetailsViewModelTest { val expectedAfterRefresh = expected.copy( courseName = "Course 2", - tabs = listOf(TabType.GRADES, TabType.SYLLABUS, TabType.SUMMARY) + tabs = listOf(TabType.GRADES, TabType.SYLLABUS, TabType.SUMMARY), + syllabus = "Syllabus body", + forceRefreshGrades = true ) Assert.assertEquals(expectedAfterRefresh, viewModel.uiState.value) + + viewModel.handleAction(CourseDetailsAction.GradesRefreshed) + Assert.assertEquals(false, viewModel.uiState.value.forceRefreshGrades) } @Test @@ -239,6 +246,21 @@ class CourseDetailsViewModelTest { Assert.assertEquals(expected, events.last()) } + @Test + fun `Navigate to LTI screen`() = runTest { + createViewModel() + + val events = mutableListOf() + backgroundScope.launch(testDispatcher) { + viewModel.events.toList(events) + } + + viewModel.handleAction(CourseDetailsAction.OnLtiClicked("ltiUrl")) + + val expected = CourseDetailsViewModelAction.OpenLtiScreen("ltiUrl") + Assert.assertEquals(expected, events.last()) + } + private fun createViewModel() { viewModel = CourseDetailsViewModel(context, savedStateHandle, repository, parentPrefs, apiPrefs) } diff --git a/libs/pandares/src/main/res/values/strings.xml b/libs/pandares/src/main/res/values/strings.xml index ba56b4a51a..3400cb2f36 100644 --- a/libs/pandares/src/main/res/values/strings.xml +++ b/libs/pandares/src/main/res/values/strings.xml @@ -1898,4 +1898,5 @@ Open the Quiz Quiz Submission This quiz opens in a web browser. Select "Open The Quiz" to proceed. + Refresh diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/calendartodo/createupdate/CreateUpdateToDoFragment.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/calendartodo/createupdate/CreateUpdateToDoFragment.kt index 71db10a3bb..72c72ef6b7 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/calendartodo/createupdate/CreateUpdateToDoFragment.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/calendartodo/createupdate/CreateUpdateToDoFragment.kt @@ -21,7 +21,6 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.platform.ComposeView @@ -53,7 +52,6 @@ class CreateUpdateToDoFragment : Fragment(), NavigationCallbacks, FragmentIntera @Inject lateinit var sharedEvents: CalendarSharedEvents - @OptIn(ExperimentalFoundationApi::class) override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/grades/GradesScreen.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/grades/GradesScreen.kt index 14d7b92713..66c2e23a1e 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/grades/GradesScreen.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/grades/GradesScreen.kt @@ -125,7 +125,7 @@ fun GradesScreen( val pullRefreshState = rememberPullRefreshState( refreshing = uiState.isRefreshing, onRefresh = { - actionHandler(GradesAction.Refresh) + actionHandler(GradesAction.Refresh()) } ) Box( @@ -138,7 +138,7 @@ fun GradesScreen( ErrorContent( errorMessage = stringResource(id = R.string.errorLoadingGrades), retryClick = { - actionHandler(GradesAction.Refresh) + actionHandler(GradesAction.Refresh()) }, modifier = Modifier.fillMaxSize() ) } diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/grades/GradesUiState.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/grades/GradesUiState.kt index 9593755030..b7aba57657 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/grades/GradesUiState.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/grades/GradesUiState.kt @@ -71,7 +71,7 @@ enum class SubmissionStateLabel( } sealed class GradesAction { - data object Refresh : GradesAction() + data class Refresh(val clearItems: Boolean = false) : GradesAction() data class GroupHeaderClick(val id: Long) : GradesAction() data object ShowGradePreferences : GradesAction() data object HideGradePreferences : GradesAction() diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/grades/GradesViewModel.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/grades/GradesViewModel.kt index 40423950e8..3d0abea15e 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/grades/GradesViewModel.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/grades/GradesViewModel.kt @@ -233,6 +233,7 @@ class GradesViewModel @Inject constructor( fun handleAction(action: GradesAction) { when (action) { is GradesAction.Refresh -> { + if (action.clearItems) _uiState.update { it.copy(items = emptyList()) } loadGrades(true) } diff --git a/libs/pandautils/src/test/java/com/instructure/pandautils/features/grades/GradesViewModelTest.kt b/libs/pandautils/src/test/java/com/instructure/pandautils/features/grades/GradesViewModelTest.kt index d4777ea908..294084fd08 100644 --- a/libs/pandautils/src/test/java/com/instructure/pandautils/features/grades/GradesViewModelTest.kt +++ b/libs/pandautils/src/test/java/com/instructure/pandautils/features/grades/GradesViewModelTest.kt @@ -511,7 +511,7 @@ class GradesViewModelTest { fun `Refresh reloads grades`() { createViewModel() - viewModel.handleAction(GradesAction.Refresh) + viewModel.handleAction(GradesAction.Refresh()) coVerify { gradesRepository.loadCourse(1, true) } coVerify { gradesRepository.loadGradingPeriods(1, true) } @@ -684,7 +684,7 @@ class GradesViewModelTest { ) coEvery { gradesRepository.loadCourse(1, any()) } throws Exception() - viewModel.handleAction(GradesAction.Refresh) + viewModel.handleAction(GradesAction.Refresh()) val expectedWithSnackbar = loaded.copy(snackbarMessage = "Grade refresh failed") Assert.assertEquals(expectedWithSnackbar, viewModel.uiState.value) From f8b3b1bfb9ddc392aa054d14f0ba945b0d6bf913 Mon Sep 17 00:00:00 2001 From: Akos Hermann <72087159+hermannakos@users.noreply.github.com> Date: Tue, 12 Nov 2024 14:27:14 +0100 Subject: [PATCH 08/31] [MBL-18072][All] Disable save button when event/todo title is blank (#2634) Test plan: See ticket. The save button should be disabled if the title is blank. refs: MBL-18072 affects: Student, Teacher, Parent release note: Fixed a bug where calendar events could be saved with a blank title. --- .../CreateUpdateEventInteractionTest.kt | 20 ++++++++++++++++++ .../CreateUpdateToDoInteractionTest.kt | 21 +++++++++++++++++++ .../compose/CalendarEventCreateEditPage.kt | 10 +++++++++ .../compose/CalendarToDoCreateUpdatePage.kt | 10 +++++++++ .../composables/CreateUpdateEventScreen.kt | 2 +- .../composables/CreateUpdateToDoScreen.kt | 2 +- 6 files changed, 63 insertions(+), 2 deletions(-) diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/interaction/CreateUpdateEventInteractionTest.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/interaction/CreateUpdateEventInteractionTest.kt index a7f9cbaede..e7f4576a34 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/interaction/CreateUpdateEventInteractionTest.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/interaction/CreateUpdateEventInteractionTest.kt @@ -365,6 +365,26 @@ abstract class CreateUpdateEventInteractionTest : CanvasComposeTest() { createUpdateEventDetailsPage.assertUnsavedChangesDialog() } + @Test + fun saveDisabledWhenTitleBlank() { + val data = initData() + goToCreateEvent(data) + + composeTestRule.waitForIdle() + createUpdateEventDetailsPage.typeTitle(" ") + createUpdateEventDetailsPage.assertSaveDisabled() + } + + @Test + fun saveEnabledWhenTitleIsNotBlank() { + val data = initData() + goToCreateEvent(data) + + composeTestRule.waitForIdle() + createUpdateEventDetailsPage.typeTitle("New Title") + createUpdateEventDetailsPage.assertSaveEnabled() + } + abstract fun goToCreateEvent(data: MockCanvas) abstract fun goToEditEvent(data: MockCanvas) diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/interaction/CreateUpdateToDoInteractionTest.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/interaction/CreateUpdateToDoInteractionTest.kt index dd233e220f..5d491943cc 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/interaction/CreateUpdateToDoInteractionTest.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/interaction/CreateUpdateToDoInteractionTest.kt @@ -279,6 +279,27 @@ abstract class CreateUpdateToDoInteractionTest : CanvasComposeTest() { calendarToDoCreateUpdatePage.assertUnsavedChangesDialog() } + @Test + fun saveDisabledWhenTitleBlank() { + val data = initData() + goToCreateToDo(data) + + composeTestRule.waitForIdle() + calendarToDoCreateUpdatePage.typeTodoTitle(" ") + calendarToDoCreateUpdatePage.assertSaveDisabled() + } + + @Test + fun saveEnabledWhenTitleIsNotBlank() { + val data = initData() + goToCreateToDo(data) + + composeTestRule.waitForIdle() + calendarToDoCreateUpdatePage.typeTodoTitle("New Title") + calendarToDoCreateUpdatePage.assertSaveEnabled() + } + + abstract fun goToCreateToDo(data: MockCanvas) abstract fun goToEditToDo(data: MockCanvas) diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/pages/compose/CalendarEventCreateEditPage.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/pages/compose/CalendarEventCreateEditPage.kt index 56489496dc..fae41174cf 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/pages/compose/CalendarEventCreateEditPage.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/pages/compose/CalendarEventCreateEditPage.kt @@ -18,6 +18,8 @@ package com.instructure.canvas.espresso.common.pages.compose import android.widget.DatePicker import android.widget.TimePicker import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.assertIsEnabled +import androidx.compose.ui.test.assertIsNotEnabled import androidx.compose.ui.test.hasParent import androidx.compose.ui.test.hasTestTag import androidx.compose.ui.test.hasText @@ -109,4 +111,12 @@ class CalendarEventCreateEditPage(private val composeTestRule: ComposeTestRule) fun clickClose() { composeTestRule.onNodeWithContentDescription("Close").performClick() } + + fun assertSaveDisabled() { + composeTestRule.onNodeWithText("Save").assertIsDisplayed().assertIsNotEnabled() + } + + fun assertSaveEnabled() { + composeTestRule.onNodeWithText("Save").assertIsDisplayed().assertIsEnabled() + } } \ No newline at end of file diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/pages/compose/CalendarToDoCreateUpdatePage.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/pages/compose/CalendarToDoCreateUpdatePage.kt index 6674c94beb..1f95e72e32 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/pages/compose/CalendarToDoCreateUpdatePage.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/pages/compose/CalendarToDoCreateUpdatePage.kt @@ -19,6 +19,8 @@ import android.content.Context import android.widget.DatePicker import android.widget.TimePicker import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.assertIsEnabled +import androidx.compose.ui.test.assertIsNotEnabled import androidx.compose.ui.test.assertTextEquals import androidx.compose.ui.test.hasParent import androidx.compose.ui.test.hasTestTag @@ -125,4 +127,12 @@ class CalendarToDoCreateUpdatePage(private val composeTestRule: ComposeTestRule) fun clickClose() { composeTestRule.onNodeWithContentDescription("Close").performClick() } + + fun assertSaveDisabled() { + composeTestRule.onNodeWithText("Save").assertIsNotEnabled() + } + + fun assertSaveEnabled() { + composeTestRule.onNodeWithText("Save").assertIsEnabled() + } } \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/calendarevent/createupdate/composables/CreateUpdateEventScreen.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/calendarevent/createupdate/composables/CreateUpdateEventScreen.kt index aa94fba524..fff789fd3b 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/calendarevent/createupdate/composables/CreateUpdateEventScreen.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/calendarevent/createupdate/composables/CreateUpdateEventScreen.kt @@ -268,7 +268,7 @@ private fun ActionsSegment( ) } - val saveEnabled = uiState.title.isNotEmpty() + val saveEnabled = uiState.title.isNotBlank() val focusManager = LocalFocusManager.current TextButton( onClick = { diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/calendartodo/createupdate/composables/CreateUpdateToDoScreen.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/calendartodo/createupdate/composables/CreateUpdateToDoScreen.kt index 081c488c5d..dd187d8b71 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/calendartodo/createupdate/composables/CreateUpdateToDoScreen.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/calendartodo/createupdate/composables/CreateUpdateToDoScreen.kt @@ -226,7 +226,7 @@ private fun ActionsSegment( actionHandler: (CreateUpdateToDoAction) -> Unit, modifier: Modifier = Modifier ) { - val saveEnabled = uiState.title.isNotEmpty() + val saveEnabled = uiState.title.isNotBlank() val focusManager = LocalFocusManager.current TextButton( onClick = { From 3f8341a6feabbb2c947da2bd771383ea679f5ec4 Mon Sep 17 00:00:00 2001 From: Tamas Kozmer <72397075+tamaskozmer@users.noreply.github.com> Date: Wed, 13 Nov 2024 12:57:22 +0100 Subject: [PATCH 09/31] [MBL-17983][All] New Quiz - Dedicated WebView (#2632) refs: MBL-17983 affects: Student, Teacher release note: Better new quizzes integration * Moved the Parent LtiLaunchFragment to the common module and added all the logic from the other apps. * Replaced LtiLaunchFragment in the Student app. * Replaced LtiLaunchFragment in the Teacher app. * LtiLaunchFragment colors. * Open internal LTIs internally in Webview and handle return button. * Inculde LTI submission preview in the Teacher app. * Refactored isNewQuiz and created a common LtiType to make internal ltis future proof. * Fixed an issue where LTI submissions can only be opened once from the submission details screen. * Unit tests. * Removed obsolete import * Fixed PR issues. * Fixed build. * Fixed external tool. --- .../parentapp/di/feature/LtiLaunchModule.kt | 32 +- .../courses/details/CourseDetailsFragment.kt | 3 +- .../features/dashboard/DashboardFragment.kt | 2 +- .../features/lti/LtiLaunchFragment.kt | 113 ----- .../features/lti/LtiLaunchViewModel.kt | 59 --- ....kt => ParentLtiLaunchFragmentBehavior.kt} | 14 +- .../features/splash/SplashFragment.kt | 2 +- .../parentapp/util/navigation/Navigation.kt | 11 +- .../res/layout/activity_route_validator.xml | 2 +- .../main/res/layout/fragment_lti_launch.xml | 48 -- .../features/lti/LtiLaunchRepositoryTest.kt | 51 --- .../features/lti/LtiLaunchViewModelTest.kt | 108 ----- .../student/activity/NavigationActivity.kt | 2 +- .../student/di/feature/LtiLaunchModule.kt | 37 ++ .../details/StudentAssignmentDetailsRouter.kt | 10 +- .../details/DiscussionDetailsFragment.kt | 8 +- .../lti/StudentLtiLaunchFragmentBehavior.kt | 25 ++ .../pages/details/PageDetailsFragment.kt | 4 +- .../fragment/AssignmentBasicFragment.kt | 17 +- .../student/fragment/LtiLaunchFragment.kt | 257 ----------- .../SubmissionDetailsModels.kt | 4 +- .../SubmissionDetailsUpdate.kt | 14 +- .../content/LtiSubmissionViewFragment.kt | 37 +- .../ui/SubmissionDetailsEmptyContentView.kt | 10 +- .../resources/StudentResourcesRouter.kt | 9 +- .../navigation/StudentWebViewRouter.kt | 4 +- .../student/router/RouteResolver.kt | 4 +- .../com/instructure/student/util/TabHelper.kt | 4 +- .../layout/activity_student_view_starter.xml | 2 +- .../main/res/layout/fragment_lti_launch.xml | 49 --- .../main/res/layout/loading_canvas_view.xml | 2 +- .../SubmissionDetailsEffectHandlerTest.kt | 14 +- .../SubmissionDetailsUpdateTest.kt | 55 +-- .../teacher/activities/InitActivity.kt | 6 +- .../instructure/teacher/di/LtiLaunchModule.kt | 37 ++ .../details/AssignmentDetailsFragment.kt | 10 +- .../lti/TeacherLtiLaunchFragmentBehavior.kt | 25 ++ .../fragments/CourseBrowserFragment.kt | 8 +- .../teacher/fragments/LtiLaunchFragment.kt | 232 ---------- .../teacher/fragments/PageDetailsFragment.kt | 3 +- .../teacher/fragments/QuizDetailsFragment.kt | 3 +- .../SpeedGraderLtiSubmissionFragment.kt | 37 +- .../navigation/TeacherWebViewRouter.kt | 4 +- .../teacher/router/RouteMatcher.kt | 4 +- .../teacher/router/RouteResolver.kt | 4 +- .../teacher/view/SubmissionContentView.kt | 3 +- .../res/layout/activity_route_validator.xml | 2 +- .../src/main/res/layout/activity_splash.xml | 2 +- .../res/layout/fragment_internal_webview.xml | 2 +- .../main/res/layout/fragment_lti_launch.xml | 47 -- .../main/res/layout/fragment_page_details.xml | 2 +- .../fragment_speed_grader_lti_submission.xml | 67 +-- .../canvasapi2/apis/LaunchDefinitionsAPI.kt | 6 +- .../canvasapi2/models/Assignment.kt | 24 +- .../instructure/canvasapi2/models/LtiType.kt | 47 ++ .../login/activities/BaseLoginInitActivity.kt | 4 +- .../main/res/layout/activity_init_login.xml | 2 +- .../pandautils/di/LtiLaunchModule.kt | 38 ++ .../details/AssignmentDetailsFragment.kt | 10 +- .../details/AssignmentDetailsRouter.kt | 2 +- .../details/AssignmentDetailsViewData.kt | 2 +- .../details/AssignmentDetailsViewModel.kt | 7 +- .../features/lti/LtiLaunchAction.kt | 3 +- .../features/lti/LtiLaunchFragment.kt | 252 +++++++++++ .../features/lti/LtiLaunchFragmentBehavior.kt | 23 + .../features/lti/LtiLaunchRepository.kt | 34 ++ .../features/lti/LtiLaunchViewModel.kt | 132 ++++++ .../features/offline/sync/StudioSync.kt | 6 +- .../pandautils/utils/AssignmentExtensions.kt | 3 +- .../pandautils/views}/CanvasLoadingView.kt | 4 +- .../main/res/layout/dialog_loading_view.xml | 2 +- .../main/res/layout/fragment_lti_launch.xml | 91 ++++ .../features/lti/LtiLaunchRepositoryTest.kt | 97 +++++ .../features/lti/LtiLaunchViewModelTest.kt | 411 ++++++++++++++++++ 74 files changed, 1497 insertions(+), 1214 deletions(-) delete mode 100644 apps/parent/src/main/java/com/instructure/parentapp/features/lti/LtiLaunchFragment.kt delete mode 100644 apps/parent/src/main/java/com/instructure/parentapp/features/lti/LtiLaunchViewModel.kt rename apps/parent/src/main/java/com/instructure/parentapp/features/lti/{LtiLaunchRepository.kt => ParentLtiLaunchFragmentBehavior.kt} (60%) delete mode 100644 apps/parent/src/main/res/layout/fragment_lti_launch.xml delete mode 100644 apps/parent/src/test/java/com/instructure/parentapp/features/lti/LtiLaunchRepositoryTest.kt delete mode 100644 apps/parent/src/test/java/com/instructure/parentapp/features/lti/LtiLaunchViewModelTest.kt create mode 100644 apps/student/src/main/java/com/instructure/student/di/feature/LtiLaunchModule.kt create mode 100644 apps/student/src/main/java/com/instructure/student/features/lti/StudentLtiLaunchFragmentBehavior.kt delete mode 100644 apps/student/src/main/java/com/instructure/student/fragment/LtiLaunchFragment.kt delete mode 100644 apps/student/src/main/res/layout/fragment_lti_launch.xml create mode 100644 apps/teacher/src/main/java/com/instructure/teacher/di/LtiLaunchModule.kt create mode 100644 apps/teacher/src/main/java/com/instructure/teacher/features/lti/TeacherLtiLaunchFragmentBehavior.kt delete mode 100644 apps/teacher/src/main/java/com/instructure/teacher/fragments/LtiLaunchFragment.kt delete mode 100644 apps/teacher/src/main/res/layout/fragment_lti_launch.xml create mode 100644 libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/models/LtiType.kt create mode 100644 libs/pandautils/src/main/java/com/instructure/pandautils/di/LtiLaunchModule.kt rename {apps/parent/src/main/java/com/instructure/parentapp => libs/pandautils/src/main/java/com/instructure/pandautils}/features/lti/LtiLaunchAction.kt (87%) create mode 100644 libs/pandautils/src/main/java/com/instructure/pandautils/features/lti/LtiLaunchFragment.kt create mode 100644 libs/pandautils/src/main/java/com/instructure/pandautils/features/lti/LtiLaunchFragmentBehavior.kt create mode 100644 libs/pandautils/src/main/java/com/instructure/pandautils/features/lti/LtiLaunchRepository.kt create mode 100644 libs/pandautils/src/main/java/com/instructure/pandautils/features/lti/LtiLaunchViewModel.kt rename libs/{login-api-2/src/main/java/com/instructure/loginapi/login/view => pandautils/src/main/java/com/instructure/pandautils/views}/CanvasLoadingView.kt (98%) create mode 100644 libs/pandautils/src/main/res/layout/fragment_lti_launch.xml create mode 100644 libs/pandautils/src/test/java/com/instructure/pandautils/features/lti/LtiLaunchRepositoryTest.kt create mode 100644 libs/pandautils/src/test/java/com/instructure/pandautils/features/lti/LtiLaunchViewModelTest.kt diff --git a/apps/parent/src/main/java/com/instructure/parentapp/di/feature/LtiLaunchModule.kt b/apps/parent/src/main/java/com/instructure/parentapp/di/feature/LtiLaunchModule.kt index d250896b30..cb1d5d22cb 100644 --- a/apps/parent/src/main/java/com/instructure/parentapp/di/feature/LtiLaunchModule.kt +++ b/apps/parent/src/main/java/com/instructure/parentapp/di/feature/LtiLaunchModule.kt @@ -1,34 +1,34 @@ /* * Copyright (C) 2024 - 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. + * 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 * - * 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 . + * 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.parentapp.di.feature -import com.instructure.canvasapi2.apis.LaunchDefinitionsAPI -import com.instructure.parentapp.features.lti.LtiLaunchRepository +import com.instructure.pandautils.features.lti.LtiLaunchFragmentBehavior +import com.instructure.parentapp.features.lti.ParentLtiLaunchFragmentBehavior +import com.instructure.parentapp.util.ParentPrefs import dagger.Module import dagger.Provides import dagger.hilt.InstallIn -import dagger.hilt.android.components.ViewModelComponent +import dagger.hilt.android.components.FragmentComponent @Module -@InstallIn(ViewModelComponent::class) +@InstallIn(FragmentComponent::class) class LtiLaunchModule { @Provides - fun provideLtiLaunchRepository(launchDefinitionsInterface: LaunchDefinitionsAPI.LaunchDefinitionsInterface): LtiLaunchRepository { - return LtiLaunchRepository(launchDefinitionsInterface) + fun provideLtiLaunchFragmentBehavior(parentPrefs: ParentPrefs): LtiLaunchFragmentBehavior { + return ParentLtiLaunchFragmentBehavior(parentPrefs) } } \ No newline at end of file diff --git a/apps/parent/src/main/java/com/instructure/parentapp/features/courses/details/CourseDetailsFragment.kt b/apps/parent/src/main/java/com/instructure/parentapp/features/courses/details/CourseDetailsFragment.kt index d61d02c010..786f8842be 100644 --- a/apps/parent/src/main/java/com/instructure/parentapp/features/courses/details/CourseDetailsFragment.kt +++ b/apps/parent/src/main/java/com/instructure/parentapp/features/courses/details/CourseDetailsFragment.kt @@ -34,6 +34,7 @@ import com.instructure.pandautils.utils.ViewStyler import com.instructure.pandautils.utils.collectOneOffEvents import com.instructure.pandautils.utils.studentColor import com.instructure.pandautils.views.CanvasWebView +import com.instructure.parentapp.R import com.instructure.parentapp.util.ParentPrefs import com.instructure.parentapp.util.navigation.Navigation import dagger.hilt.android.AndroidEntryPoint @@ -112,7 +113,7 @@ class CourseDetailsFragment : Fragment() { } is CourseDetailsViewModelAction.OpenLtiScreen -> { - navigation.navigate(activity, navigation.ltiLaunchRoute(action.url, "")) + navigation.navigate(activity, navigation.ltiLaunchRoute(action.url, getString(R.string.utils_externalToolTitle), sessionlessLaunch = true)) } } } diff --git a/apps/parent/src/main/java/com/instructure/parentapp/features/dashboard/DashboardFragment.kt b/apps/parent/src/main/java/com/instructure/parentapp/features/dashboard/DashboardFragment.kt index 4e8a57e3ab..1bc6bec4ca 100644 --- a/apps/parent/src/main/java/com/instructure/parentapp/features/dashboard/DashboardFragment.kt +++ b/apps/parent/src/main/java/com/instructure/parentapp/features/dashboard/DashboardFragment.kt @@ -201,7 +201,7 @@ class DashboardFragment : Fragment(), NavigationCallbacks { } } is DashboardViewModelAction.OpenLtiTool -> { - navigation.navigate(requireActivity(), navigation.ltiLaunchRoute(action.url, action.name)) + navigation.navigate(requireActivity(), navigation.ltiLaunchRoute(action.url, action.name, sessionlessLaunch = true)) } } } diff --git a/apps/parent/src/main/java/com/instructure/parentapp/features/lti/LtiLaunchFragment.kt b/apps/parent/src/main/java/com/instructure/parentapp/features/lti/LtiLaunchFragment.kt deleted file mode 100644 index c075e27967..0000000000 --- a/apps/parent/src/main/java/com/instructure/parentapp/features/lti/LtiLaunchFragment.kt +++ /dev/null @@ -1,113 +0,0 @@ -/* - * Copyright (C) 2024 - 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.parentapp.features.lti - -import android.net.Uri -import android.os.Bundle -import android.os.Handler -import android.os.Looper -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.browser.customtabs.CustomTabColorSchemeParams -import androidx.browser.customtabs.CustomTabsIntent -import androidx.fragment.app.Fragment -import androidx.fragment.app.viewModels -import androidx.lifecycle.lifecycleScope -import com.instructure.canvasapi2.utils.validOrNull -import com.instructure.pandautils.binding.viewBinding -import com.instructure.pandautils.utils.NullableStringArg -import com.instructure.pandautils.utils.ThemePrefs -import com.instructure.pandautils.utils.asChooserExcludingInstructure -import com.instructure.pandautils.utils.collectOneOffEvents -import com.instructure.pandautils.utils.setTextForVisibility -import com.instructure.pandautils.utils.studentColor -import com.instructure.pandautils.utils.toast -import com.instructure.parentapp.R -import com.instructure.parentapp.databinding.FragmentLtiLaunchBinding -import com.instructure.parentapp.util.ParentPrefs -import dagger.hilt.android.AndroidEntryPoint - -@AndroidEntryPoint -class LtiLaunchFragment : Fragment() { - - private val binding by viewBinding(FragmentLtiLaunchBinding::bind) - - private val viewModel: LtiLaunchViewModel by viewModels() - - var title: String? by NullableStringArg(key = LTI_TITLE) - - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - return inflater.inflate(R.layout.fragment_lti_launch, container, false) - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - binding.loadingView.setOverrideColor(ParentPrefs.currentStudent?.studentColor ?: ThemePrefs.primaryColor) - binding.toolName.setTextForVisibility(title.validOrNull()) - - lifecycleScope.collectOneOffEvents(viewModel.events, ::handleAction) - } - - private fun handleAction(action: LtiLaunchAction) { - when (action) { - is LtiLaunchAction.LaunchCustomTab -> { - launchCustomTab(action.url) - } - is LtiLaunchAction.ShowError -> { - toast(R.string.errorOccurred) - if (activity != null) { - requireActivity().onBackPressed() - } - } - } - } - - private fun launchCustomTab(url: String) { - val uri = Uri.parse(url) - .buildUpon() - .appendQueryParameter("display", "borderless") - .appendQueryParameter("platform", "android") - .build() - - val colorSchemeParams = CustomTabColorSchemeParams.Builder() - .setToolbarColor(ThemePrefs.primaryColor) - .build() - - var intent = CustomTabsIntent.Builder() - .setDefaultColorSchemeParams(colorSchemeParams) - .setShowTitle(true) - .build() - .intent - - intent.data = uri - - // Exclude Instructure apps from chooser options - intent = intent.asChooserExcludingInstructure() - - requireContext().startActivity(intent) - Handler(Looper.getMainLooper()).postDelayed({ - if (activity == null) return@postDelayed - requireActivity().onBackPressed() - }, 500) - } - - companion object { - const val LTI_URL = "lti_url" - const val LTI_TITLE = "lti_title" - } -} \ No newline at end of file diff --git a/apps/parent/src/main/java/com/instructure/parentapp/features/lti/LtiLaunchViewModel.kt b/apps/parent/src/main/java/com/instructure/parentapp/features/lti/LtiLaunchViewModel.kt deleted file mode 100644 index 8730dae52f..0000000000 --- a/apps/parent/src/main/java/com/instructure/parentapp/features/lti/LtiLaunchViewModel.kt +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright (C) 2024 - 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.parentapp.features.lti - -import androidx.lifecycle.SavedStateHandle -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import com.instructure.canvasapi2.utils.weave.catch -import com.instructure.canvasapi2.utils.weave.tryLaunch -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.channels.Channel -import kotlinx.coroutines.flow.receiveAsFlow -import kotlinx.coroutines.launch -import javax.inject.Inject - -@HiltViewModel -class LtiLaunchViewModel @Inject constructor( - savedStateHandle: SavedStateHandle, - private val repository: LtiLaunchRepository -) : ViewModel() { - - private val ltiUrl: String? = savedStateHandle.get(LtiLaunchFragment.LTI_URL) - - private val _events = Channel() - val events = _events.receiveAsFlow() - - init { - loadLtiAuthenticatedUrl() - } - - private fun loadLtiAuthenticatedUrl() { - viewModelScope.tryLaunch { - ltiUrl?.let { - val ltiTool = repository.getLtiFromAuthenticationUrl(it) - ltiTool.url?.let { url -> - _events.send(LtiLaunchAction.LaunchCustomTab(url)) - } ?: _events.send(LtiLaunchAction.ShowError) - } - } catch { - viewModelScope.launch { - _events.send(LtiLaunchAction.ShowError) - } - } - } -} \ No newline at end of file diff --git a/apps/parent/src/main/java/com/instructure/parentapp/features/lti/LtiLaunchRepository.kt b/apps/parent/src/main/java/com/instructure/parentapp/features/lti/ParentLtiLaunchFragmentBehavior.kt similarity index 60% rename from apps/parent/src/main/java/com/instructure/parentapp/features/lti/LtiLaunchRepository.kt rename to apps/parent/src/main/java/com/instructure/parentapp/features/lti/ParentLtiLaunchFragmentBehavior.kt index 1cb12a7556..c45f67c6c3 100644 --- a/apps/parent/src/main/java/com/instructure/parentapp/features/lti/LtiLaunchRepository.kt +++ b/apps/parent/src/main/java/com/instructure/parentapp/features/lti/ParentLtiLaunchFragmentBehavior.kt @@ -16,14 +16,10 @@ */ package com.instructure.parentapp.features.lti -import com.instructure.canvasapi2.apis.LaunchDefinitionsAPI -import com.instructure.canvasapi2.builders.RestParams -import com.instructure.canvasapi2.models.LTITool +import com.instructure.pandautils.features.lti.LtiLaunchFragmentBehavior +import com.instructure.pandautils.utils.studentColor +import com.instructure.parentapp.util.ParentPrefs -class LtiLaunchRepository( - private val launchDefinitionsApi: LaunchDefinitionsAPI.LaunchDefinitionsInterface -) { - suspend fun getLtiFromAuthenticationUrl(url: String): LTITool { - return launchDefinitionsApi.getLtiFromAuthenticationUrl(url, RestParams(isForceReadFromNetwork = true)).dataOrThrow - } +class ParentLtiLaunchFragmentBehavior(parentPrefs: ParentPrefs) : LtiLaunchFragmentBehavior { + override val toolbarColor: Int = parentPrefs.currentStudent.studentColor } \ No newline at end of file diff --git a/apps/parent/src/main/java/com/instructure/parentapp/features/splash/SplashFragment.kt b/apps/parent/src/main/java/com/instructure/parentapp/features/splash/SplashFragment.kt index 72fd396258..7417919742 100644 --- a/apps/parent/src/main/java/com/instructure/parentapp/features/splash/SplashFragment.kt +++ b/apps/parent/src/main/java/com/instructure/parentapp/features/splash/SplashFragment.kt @@ -36,7 +36,7 @@ import androidx.fragment.app.viewModels import androidx.lifecycle.lifecycleScope import androidx.navigation.fragment.findNavController import com.instructure.canvasapi2.utils.LocaleUtils -import com.instructure.loginapi.login.view.CanvasLoadingView +import com.instructure.pandautils.views.CanvasLoadingView import com.instructure.pandautils.utils.ThemePrefs import com.instructure.pandautils.utils.collectOneOffEvents import com.instructure.parentapp.R diff --git a/apps/parent/src/main/java/com/instructure/parentapp/util/navigation/Navigation.kt b/apps/parent/src/main/java/com/instructure/parentapp/util/navigation/Navigation.kt index 97b5cbfb6c..7be10a6a9a 100644 --- a/apps/parent/src/main/java/com/instructure/parentapp/util/navigation/Navigation.kt +++ b/apps/parent/src/main/java/com/instructure/parentapp/util/navigation/Navigation.kt @@ -36,7 +36,7 @@ import com.instructure.parentapp.features.calendar.ParentCalendarFragment import com.instructure.parentapp.features.courses.details.CourseDetailsFragment import com.instructure.parentapp.features.courses.list.CoursesFragment import com.instructure.parentapp.features.dashboard.DashboardFragment -import com.instructure.parentapp.features.lti.LtiLaunchFragment +import com.instructure.pandautils.features.lti.LtiLaunchFragment import com.instructure.parentapp.features.managestudents.ManageStudentsFragment import com.instructure.parentapp.features.notaparent.NotAParentFragment import com.instructure.parentapp.features.splash.SplashFragment @@ -81,7 +81,7 @@ class Navigation(apiPrefs: ApiPrefs) { private val updateToDo = "$baseUrl/update-todo/{${CreateUpdateToDoFragment.PLANNER_ITEM}}" private val alertSettings = "$baseUrl/alert-settings/{${Const.USER}}" - private val ltiLaunch = "$baseUrl/lti-launch/{${LtiLaunchFragment.LTI_URL}}/{${LtiLaunchFragment.LTI_TITLE}}" + private val ltiLaunch = "$baseUrl/lti-launch/{${LtiLaunchFragment.LTI_URL}}/{${LtiLaunchFragment.LTI_TITLE}}/{${LtiLaunchFragment.SESSION_LESS_LAUNCH}}" fun courseDetailsRoute(id: Long) = "$baseUrl/courses/$id" @@ -97,7 +97,7 @@ class Navigation(apiPrefs: ApiPrefs) { fun globalAnnouncementRoute(alertId: Long) = "$baseUrl/account_notifications/$alertId" - fun ltiLaunchRoute(url: String, title: String) = "$baseUrl/lti-launch/${Uri.encode(url)}/${Uri.encode(title)}" + fun ltiLaunchRoute(url: String, title: String, sessionlessLaunch: Boolean) = "$baseUrl/lti-launch/${Uri.encode(url)}/${Uri.encode(title)}/$sessionlessLaunch" fun crateMainNavGraph(navController: NavController): NavGraph { return navController.createGraph( @@ -239,6 +239,11 @@ class Navigation(apiPrefs: ApiPrefs) { type = NavType.StringType nullable = false } + argument(LtiLaunchFragment.SESSION_LESS_LAUNCH) { + type = NavType.BoolType + nullable = false + defaultValue = false + } } } } diff --git a/apps/parent/src/main/res/layout/activity_route_validator.xml b/apps/parent/src/main/res/layout/activity_route_validator.xml index 47dd4fd893..99fbb4358f 100644 --- a/apps/parent/src/main/res/layout/activity_route_validator.xml +++ b/apps/parent/src/main/res/layout/activity_route_validator.xml @@ -24,7 +24,7 @@ android:layout_height="match_parent" android:visibility="invisible" /> - - - - - - - - - - diff --git a/apps/parent/src/test/java/com/instructure/parentapp/features/lti/LtiLaunchRepositoryTest.kt b/apps/parent/src/test/java/com/instructure/parentapp/features/lti/LtiLaunchRepositoryTest.kt deleted file mode 100644 index 05ab5f04d3..0000000000 --- a/apps/parent/src/test/java/com/instructure/parentapp/features/lti/LtiLaunchRepositoryTest.kt +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright (C) 2024 - 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.parentapp.features.lti - -import com.instructure.canvasapi2.apis.LaunchDefinitionsAPI -import com.instructure.canvasapi2.models.LTITool -import com.instructure.canvasapi2.utils.DataResult -import io.mockk.coEvery -import io.mockk.mockk -import kotlinx.coroutines.test.runTest -import org.junit.Assert.assertEquals -import org.junit.Test - -class LtiLaunchRepositoryTest { - - private val launchDefinitionsApi: LaunchDefinitionsAPI.LaunchDefinitionsInterface = mockk(relaxed = true) - - private val repository = LtiLaunchRepository(launchDefinitionsApi) - - @Test - fun `Get lti from authentication url throws exception when fails`() = runTest { - val url = "https://www.instructure.com" - val result = runCatching { repository.getLtiFromAuthenticationUrl(url) } - assert(result.isFailure) - } - - @Test - fun `Get lti from authentication url returns data when successful`() = runTest { - val url = "https://www.instructure.com" - val expected = LTITool() - coEvery { launchDefinitionsApi.getLtiFromAuthenticationUrl(url, any()) } returns DataResult.Success(expected) - - val result = repository.getLtiFromAuthenticationUrl(url) - - assertEquals(expected, result) - } -} \ No newline at end of file diff --git a/apps/parent/src/test/java/com/instructure/parentapp/features/lti/LtiLaunchViewModelTest.kt b/apps/parent/src/test/java/com/instructure/parentapp/features/lti/LtiLaunchViewModelTest.kt deleted file mode 100644 index 19e8745f60..0000000000 --- a/apps/parent/src/test/java/com/instructure/parentapp/features/lti/LtiLaunchViewModelTest.kt +++ /dev/null @@ -1,108 +0,0 @@ -/* - * Copyright (C) 2024 - 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.parentapp.features.lti - -import androidx.arch.core.executor.testing.InstantTaskExecutorRule -import androidx.lifecycle.SavedStateHandle -import com.instructure.canvasapi2.models.LTITool -import io.mockk.coEvery -import io.mockk.every -import io.mockk.mockk -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.toList -import kotlinx.coroutines.launch -import kotlinx.coroutines.test.UnconfinedTestDispatcher -import kotlinx.coroutines.test.resetMain -import kotlinx.coroutines.test.runTest -import kotlinx.coroutines.test.setMain -import org.junit.After -import org.junit.Assert.assertEquals -import org.junit.Before -import org.junit.Rule -import org.junit.Test - -@OptIn(ExperimentalCoroutinesApi::class) -class LtiLaunchViewModelTest { - - @get:Rule - val instantExecutorRule = InstantTaskExecutorRule() - - private val savedStateHandle: SavedStateHandle = mockk(relaxed = true) - private val repository: LtiLaunchRepository = mockk(relaxed = true) - private val testDispatcher = UnconfinedTestDispatcher() - - private lateinit var viewModel: LtiLaunchViewModel - - @Before - fun setup() { - every { savedStateHandle.get(LtiLaunchFragment.LTI_URL) } returns "url" - Dispatchers.setMain(testDispatcher) - } - - @After - fun tearDown() { - Dispatchers.resetMain() - } - - @Test - fun `Launch custom tab when lti tool url is successful`() = runTest { - val ltiTool = LTITool(url = "url") - coEvery { repository.getLtiFromAuthenticationUrl("url") } returns ltiTool - - viewModel = LtiLaunchViewModel(savedStateHandle, repository) - - val events = mutableListOf() - - backgroundScope.launch(testDispatcher) { - viewModel.events.toList(events) - } - - assertEquals(events[0], LtiLaunchAction.LaunchCustomTab("url")) - } - - @Test - fun `Show error when lti tool url is null`() = runTest { - val ltiTool = LTITool(url = null) - coEvery { repository.getLtiFromAuthenticationUrl("url") } returns ltiTool - - viewModel = LtiLaunchViewModel(savedStateHandle, repository) - - val events = mutableListOf() - - backgroundScope.launch(testDispatcher) { - viewModel.events.toList(events) - } - - assertEquals(events[0], LtiLaunchAction.ShowError) - } - - @Test - fun `Show error when lti request fails`() = runTest { - coEvery { repository.getLtiFromAuthenticationUrl("url") } throws Exception() - - viewModel = LtiLaunchViewModel(savedStateHandle, repository) - - val events = mutableListOf() - - backgroundScope.launch(testDispatcher) { - viewModel.events.toList(events) - } - - assertEquals(events[0], LtiLaunchAction.ShowError) - } -} \ No newline at end of file diff --git a/apps/student/src/main/java/com/instructure/student/activity/NavigationActivity.kt b/apps/student/src/main/java/com/instructure/student/activity/NavigationActivity.kt index a9f3721247..af8e675b26 100644 --- a/apps/student/src/main/java/com/instructure/student/activity/NavigationActivity.kt +++ b/apps/student/src/main/java/com/instructure/student/activity/NavigationActivity.kt @@ -85,6 +85,7 @@ import com.instructure.pandautils.features.calendar.CalendarFragment import com.instructure.pandautils.features.calendarevent.details.EventFragment import com.instructure.pandautils.features.help.HelpDialogFragment import com.instructure.pandautils.features.inbox.list.InboxFragment +import com.instructure.pandautils.features.lti.LtiLaunchFragment import com.instructure.pandautils.features.notification.preferences.PushNotificationPreferencesFragment import com.instructure.pandautils.features.offline.sync.OfflineSyncHelper import com.instructure.pandautils.features.settings.SettingsFragment @@ -137,7 +138,6 @@ import com.instructure.student.fragment.DashboardFragment import com.instructure.student.fragment.InboxComposeMessageFragment import com.instructure.student.fragment.InboxConversationFragment import com.instructure.student.fragment.InboxRecipientsFragment -import com.instructure.student.fragment.LtiLaunchFragment import com.instructure.student.fragment.NotificationListFragment import com.instructure.student.fragment.ToDoListFragment import com.instructure.student.mobius.assignmentDetails.submission.picker.PickerSubmissionUploadEffectHandler diff --git a/apps/student/src/main/java/com/instructure/student/di/feature/LtiLaunchModule.kt b/apps/student/src/main/java/com/instructure/student/di/feature/LtiLaunchModule.kt new file mode 100644 index 0000000000..f6266ceca4 --- /dev/null +++ b/apps/student/src/main/java/com/instructure/student/di/feature/LtiLaunchModule.kt @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2024 - 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.feature + +import androidx.fragment.app.Fragment +import com.instructure.canvasapi2.models.CanvasContext +import com.instructure.pandautils.features.lti.LtiLaunchFragmentBehavior +import com.instructure.pandautils.utils.Const +import com.instructure.student.features.lti.StudentLtiLaunchFragmentBehavior +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.components.FragmentComponent + +@Module +@InstallIn(FragmentComponent::class) +class LtiLaunchModule { + + @Provides + fun provideLtiLaunchFragmentBehavior(fragment: Fragment): LtiLaunchFragmentBehavior { + val canvasContext = fragment.arguments?.getParcelable(Const.CANVAS_CONTEXT) ?: CanvasContext.emptyUserContext() + return StudentLtiLaunchFragmentBehavior(canvasContext) + } +} \ No newline at end of file diff --git a/apps/student/src/main/java/com/instructure/student/features/assignments/details/StudentAssignmentDetailsRouter.kt b/apps/student/src/main/java/com/instructure/student/features/assignments/details/StudentAssignmentDetailsRouter.kt index 042543792b..c9874113db 100644 --- a/apps/student/src/main/java/com/instructure/student/features/assignments/details/StudentAssignmentDetailsRouter.kt +++ b/apps/student/src/main/java/com/instructure/student/features/assignments/details/StudentAssignmentDetailsRouter.kt @@ -27,9 +27,9 @@ import com.instructure.canvasapi2.utils.Analytics import com.instructure.canvasapi2.utils.AnalyticsEventConstants import com.instructure.pandautils.features.assignments.details.AssignmentDetailsRouter import com.instructure.pandautils.features.discussion.router.DiscussionRouterFragment +import com.instructure.pandautils.features.lti.LtiLaunchFragment import com.instructure.student.activity.BaseRouterActivity import com.instructure.student.fragment.BasicQuizViewFragment -import com.instructure.student.fragment.LtiLaunchFragment import com.instructure.student.mobius.assignmentDetails.submission.annnotation.AnnotationSubmissionUploadFragment import com.instructure.student.mobius.assignmentDetails.submission.file.ui.UploadStatusSubmissionFragment import com.instructure.student.mobius.assignmentDetails.submission.picker.PickerSubmissionMode @@ -154,7 +154,8 @@ class StudentAssignmentDetailsRouter: AssignmentDetailsRouter() { title: String?, sessionLessLaunch: Boolean, isAssignmentLTI: Boolean, - ltiTool: LTITool? + ltiTool: LTITool?, + openInternally: Boolean ) { RouteMatcher.route( activity, @@ -163,8 +164,9 @@ class StudentAssignmentDetailsRouter: AssignmentDetailsRouter() { url, title, sessionLessLaunch = sessionLessLaunch, - isAssignmentLTI = isAssignmentLTI, - ltiTool = ltiTool + assignmentLti = isAssignmentLTI, + ltiTool = ltiTool, + openInternally = openInternally ) ) } diff --git a/apps/student/src/main/java/com/instructure/student/features/discussion/details/DiscussionDetailsFragment.kt b/apps/student/src/main/java/com/instructure/student/features/discussion/details/DiscussionDetailsFragment.kt index f9375ebec8..f5c8155a10 100644 --- a/apps/student/src/main/java/com/instructure/student/features/discussion/details/DiscussionDetailsFragment.kt +++ b/apps/student/src/main/java/com/instructure/student/features/discussion/details/DiscussionDetailsFragment.kt @@ -67,6 +67,7 @@ import com.instructure.pandautils.binding.viewBinding import com.instructure.pandautils.discussions.DiscussionCaching import com.instructure.pandautils.discussions.DiscussionEntryHtmlConverter import com.instructure.pandautils.discussions.DiscussionUtils +import com.instructure.pandautils.features.lti.LtiLaunchFragment import com.instructure.pandautils.utils.BooleanArg import com.instructure.pandautils.utils.DiscussionEntryEvent import com.instructure.pandautils.utils.LongArg @@ -103,7 +104,6 @@ import com.instructure.student.features.modules.progression.CourseModuleProgress import com.instructure.student.fragment.DiscussionsReplyFragment import com.instructure.student.fragment.DiscussionsUpdateFragment import com.instructure.student.fragment.InternalWebviewFragment -import com.instructure.student.fragment.LtiLaunchFragment import com.instructure.student.fragment.ParentFragment import com.instructure.student.router.RouteMatcher import com.instructure.student.util.Const @@ -781,7 +781,9 @@ class DiscussionDetailsFragment : ParentFragment(), Bookmarkable { discussionTopicHeaderWebViewWrapper.webView.loadHtmlWithIframes(requireContext(), discussionTopicHeader.message, { if (view != null) loadHTMLTopic(it, discussionTopicHeader.title) - }, onLtiButtonPressed = { LtiLaunchFragment.routeLtiLaunchFragment(requireActivity(), canvasContext, it) }) + }, onLtiButtonPressed = { + RouteMatcher.route(requireActivity(), LtiLaunchFragment.makeSessionlessLtiUrlRoute(requireActivity(), canvasContext, it)) + }) attachmentIcon.setVisible(discussionTopicHeader.attachments.isNotEmpty()) attachmentIcon.onClick { @@ -802,7 +804,7 @@ class DiscussionDetailsFragment : ParentFragment(), Bookmarkable { discussionRepliesWebViewWrapper.webView.loadHtmlWithIframes(requireContext(), html, { formattedHtml -> discussionRepliesWebViewWrapper.loadDataWithBaseUrl(CanvasWebView.getReferrer(true), formattedHtml, "text/html", "UTF-8", null) - }, onLtiButtonPressed = { LtiLaunchFragment.routeLtiLaunchFragment(requireActivity(), canvasContext, it) }) + }, onLtiButtonPressed = { RouteMatcher.route(requireActivity(), LtiLaunchFragment.makeSessionlessLtiUrlRoute(requireActivity(), canvasContext, it)) }) swipeRefreshLayout.isRefreshing = false discussionTopicRepliesTitle.setVisible(discussionTopicHeader.shouldShowReplies) diff --git a/apps/student/src/main/java/com/instructure/student/features/lti/StudentLtiLaunchFragmentBehavior.kt b/apps/student/src/main/java/com/instructure/student/features/lti/StudentLtiLaunchFragmentBehavior.kt new file mode 100644 index 0000000000..b547d38b0f --- /dev/null +++ b/apps/student/src/main/java/com/instructure/student/features/lti/StudentLtiLaunchFragmentBehavior.kt @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2024 - 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.features.lti + +import com.instructure.canvasapi2.models.CanvasContext +import com.instructure.pandautils.features.lti.LtiLaunchFragmentBehavior +import com.instructure.pandautils.utils.color + +class StudentLtiLaunchFragmentBehavior(canvasContext: CanvasContext) : LtiLaunchFragmentBehavior { + override val toolbarColor: Int = canvasContext.color +} \ No newline at end of file diff --git a/apps/student/src/main/java/com/instructure/student/features/pages/details/PageDetailsFragment.kt b/apps/student/src/main/java/com/instructure/student/features/pages/details/PageDetailsFragment.kt index d1d7df70dc..ae95e37b1c 100644 --- a/apps/student/src/main/java/com/instructure/student/features/pages/details/PageDetailsFragment.kt +++ b/apps/student/src/main/java/com/instructure/student/features/pages/details/PageDetailsFragment.kt @@ -43,6 +43,7 @@ import com.instructure.interactions.router.RouterParams import com.instructure.loginapi.login.dialog.NoInternetConnectionDialog import com.instructure.pandautils.analytics.SCREEN_VIEW_PAGE_DETAILS import com.instructure.pandautils.analytics.ScreenView +import com.instructure.pandautils.features.lti.LtiLaunchFragment import com.instructure.pandautils.navigation.WebViewRouter import com.instructure.pandautils.utils.BooleanArg import com.instructure.pandautils.utils.NullableStringArg @@ -59,7 +60,6 @@ import com.instructure.student.R import com.instructure.student.events.PageUpdatedEvent import com.instructure.student.fragment.EditPageDetailsFragment import com.instructure.student.fragment.InternalWebviewFragment -import com.instructure.student.fragment.LtiLaunchFragment import com.instructure.student.router.RouteMatcher import com.instructure.student.util.LockInfoHTMLHelper import dagger.hilt.android.AndroidEntryPoint @@ -230,7 +230,7 @@ class PageDetailsFragment : InternalWebviewFragment(), Bookmarkable { loadHtmlJob = canvasWebViewWrapper.webView.loadHtmlWithIframes(requireContext(), body, { canvasWebViewWrapper.loadHtml(it, page.title, baseUrl = page.htmlUrl) }) { - LtiLaunchFragment.routeLtiLaunchFragment(requireActivity(), canvasContext, it) + RouteMatcher.route(requireActivity(), LtiLaunchFragment.makeSessionlessLtiUrlRoute(requireActivity(), canvasContext, it)) } } else if (page.body == null || page.body?.endsWith("") == true) { loadHtml(resources.getString(R.string.noPageFound), "text/html", "utf-8", null) diff --git a/apps/student/src/main/java/com/instructure/student/fragment/AssignmentBasicFragment.kt b/apps/student/src/main/java/com/instructure/student/fragment/AssignmentBasicFragment.kt index 4d3093f698..9591f8f08f 100644 --- a/apps/student/src/main/java/com/instructure/student/fragment/AssignmentBasicFragment.kt +++ b/apps/student/src/main/java/com/instructure/student/fragment/AssignmentBasicFragment.kt @@ -30,7 +30,16 @@ import com.instructure.interactions.router.Route import com.instructure.pandautils.analytics.SCREEN_VIEW_ASSIGNMENT_BASIC import com.instructure.pandautils.analytics.ScreenView import com.instructure.pandautils.binding.viewBinding -import com.instructure.pandautils.utils.* +import com.instructure.pandautils.features.lti.LtiLaunchFragment +import com.instructure.pandautils.utils.Const +import com.instructure.pandautils.utils.OnBackStackChangedEvent +import com.instructure.pandautils.utils.ParcelableArg +import com.instructure.pandautils.utils.ViewStyler +import com.instructure.pandautils.utils.argsWithContext +import com.instructure.pandautils.utils.loadHtmlWithIframes +import com.instructure.pandautils.utils.setGone +import com.instructure.pandautils.utils.setupAsBackButton +import com.instructure.pandautils.utils.withArgs import com.instructure.pandautils.views.CanvasWebView import com.instructure.student.R import com.instructure.student.databinding.FragmentAssignmentBasicBinding @@ -39,7 +48,9 @@ import kotlinx.coroutines.Job import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.ThreadMode -import java.util.* +import java.util.Calendar +import java.util.Date +import java.util.Locale @ScreenView(SCREEN_VIEW_ASSIGNMENT_BASIC) class AssignmentBasicFragment : ParentFragment() { @@ -142,7 +153,7 @@ class AssignmentBasicFragment : ParentFragment() { loadHtmlJob = assignmentWebViewWrapper.webView.loadHtmlWithIframes(requireContext(), description, { assignmentWebViewWrapper.loadHtml(it, assignment.name) }, { - LtiLaunchFragment.routeLtiLaunchFragment(requireActivity(), canvasContext, it) + RouteMatcher.route(requireActivity(), LtiLaunchFragment.makeSessionlessLtiUrlRoute(requireActivity(), canvasContext, it)) }) } diff --git a/apps/student/src/main/java/com/instructure/student/fragment/LtiLaunchFragment.kt b/apps/student/src/main/java/com/instructure/student/fragment/LtiLaunchFragment.kt deleted file mode 100644 index aa56b19cf8..0000000000 --- a/apps/student/src/main/java/com/instructure/student/fragment/LtiLaunchFragment.kt +++ /dev/null @@ -1,257 +0,0 @@ -/* - * Copyright (C) 2017 - 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.fragment - -import android.net.Uri -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.browser.customtabs.CustomTabColorSchemeParams -import androidx.browser.customtabs.CustomTabsIntent -import androidx.fragment.app.FragmentActivity -import com.instructure.canvasapi2.managers.AssignmentManager -import com.instructure.canvasapi2.managers.SubmissionManager -import com.instructure.canvasapi2.models.CanvasContext -import com.instructure.canvasapi2.models.Course -import com.instructure.canvasapi2.models.Group -import com.instructure.canvasapi2.models.LTITool -import com.instructure.canvasapi2.models.Tab -import com.instructure.canvasapi2.utils.ApiPrefs -import com.instructure.canvasapi2.utils.isValid -import com.instructure.canvasapi2.utils.pageview.PageView -import com.instructure.canvasapi2.utils.pageview.PageViewUrl -import com.instructure.canvasapi2.utils.validOrNull -import com.instructure.canvasapi2.utils.weave.weave -import com.instructure.interactions.router.Route -import com.instructure.pandautils.analytics.SCREEN_VIEW_LTI_LAUNCH -import com.instructure.pandautils.analytics.ScreenView -import com.instructure.pandautils.binding.viewBinding -import com.instructure.pandautils.utils.BooleanArg -import com.instructure.pandautils.utils.Const -import com.instructure.pandautils.utils.HtmlContentFormatter -import com.instructure.pandautils.utils.NullableParcelableArg -import com.instructure.pandautils.utils.NullableStringArg -import com.instructure.pandautils.utils.ParcelableArg -import com.instructure.pandautils.utils.StringArg -import com.instructure.pandautils.utils.argsWithContext -import com.instructure.pandautils.utils.asChooserExcludingInstructure -import com.instructure.pandautils.utils.color -import com.instructure.pandautils.utils.replaceWithURLQueryParameter -import com.instructure.pandautils.utils.setTextForVisibility -import com.instructure.pandautils.utils.toast -import com.instructure.pandautils.utils.withArgs -import com.instructure.student.R -import com.instructure.student.databinding.FragmentLtiLaunchBinding -import com.instructure.student.router.RouteMatcher -import kotlinx.coroutines.Job -import java.net.URLDecoder - -@ScreenView(SCREEN_VIEW_LTI_LAUNCH) -@PageView -class LtiLaunchFragment : ParentFragment() { - - private val binding by viewBinding(FragmentLtiLaunchBinding::bind) - - var canvasContext: CanvasContext by ParcelableArg(default = CanvasContext.emptyUserContext(), key = Const.CANVAS_CONTEXT) - var title: String? by NullableStringArg(key = Const.ACTION_BAR_TITLE) - private var ltiUrl: String by StringArg(key = LTI_URL) - private var ltiTab: Tab? by NullableParcelableArg(key = Const.TAB) - private var ltiTool: LTITool? by NullableParcelableArg(key = Const.LTI_TOOL, default = null) - private var sessionLessLaunch: Boolean by BooleanArg(key = Const.SESSIONLESS_LAUNCH) - private var isAssignmentLTI: Boolean by BooleanArg(key = Const.ASSIGNMENT_LTI) - - /* Tracks whether we have automatically started launching the LTI tool in a chrome custom tab. Because this fragment - re-runs certain logic in onResume, tracking the launch helps us know to pop this fragment instead of erroneously - launching again when the user returns to the app. */ - private var customTabLaunched: Boolean = false - - private var ltiUrlLaunchJob: Job? = null - - @Suppress("unused") - @PageViewUrl - private fun makePageViewUrl() = - ltiTab?.externalUrl ?: ApiPrefs.fullDomain + canvasContext.toAPIString() + "/external_tools" - - override fun title(): String = title.validOrNull() ?: ltiTab?.label?.validOrNull() ?: ltiUrl.validOrNull() ?: "" - - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - return inflater.inflate(R.layout.fragment_lti_launch, container, false) - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - binding.loadingView.setOverrideColor(canvasContext.color) - binding.toolName.setTextForVisibility(title().validOrNull()) - } - - override fun applyTheme() = Unit - - override fun onResume() { - super.onResume() - // If onResume() is called after the custom tab has launched, it means the user is returning and we should close this fragment - if (customTabLaunched) { - activity?.supportFragmentManager?.popBackStack() - return - } - - try { - when { - ltiTab != null -> loadSessionlessLtiUrl(ltiTab!!.ltiUrl) - ltiUrl.isNotBlank() -> { - var url = ltiUrl // Replace deep link scheme - .replaceFirst("canvas-courses://", "${ApiPrefs.protocol}://") - .replaceFirst("canvas-student://", "${ApiPrefs.protocol}://") - .replaceWithURLQueryParameter(HtmlContentFormatter.hasKalturaUrl(ltiUrl)) - - when { - sessionLessLaunch -> { - // This is specific for Studio and Gauge - val id = url.substringAfterLast("/external_tools/").substringBefore("?") - url = when { - (id.toIntOrNull() != null) -> when (canvasContext) { - is Course -> "${ApiPrefs.fullDomain}/api/v1/courses/${canvasContext.id}/external_tools/sessionless_launch?id=$id" - is Group -> "${ApiPrefs.fullDomain}/api/v1/groups/${canvasContext.id}/external_tools/sessionless_launch?id=$id" - else -> "${ApiPrefs.fullDomain}/api/v1/accounts/self/external_tools/sessionless_launch?id=$id" - } - else -> when (canvasContext) { - is Course -> "${ApiPrefs.fullDomain}/api/v1/courses/${canvasContext.id}/external_tools/sessionless_launch?url=$url" - is Group -> "${ApiPrefs.fullDomain}/api/v1/groups/${canvasContext.id}/external_tools/sessionless_launch?url=$url" - else -> "${ApiPrefs.fullDomain}/api/v1/accounts/self/external_tools/sessionless_launch?url=$url" - } - } - loadSessionlessLtiUrl(url) - } - isAssignmentLTI -> loadSessionlessLtiUrl(url) - else -> launchCustomTab(url) - } - } - else -> displayError() - } - } catch (e: Exception) { - // If it gets here we're in trouble and won't know what the tab is, so just display an error message - displayError() - } - } - - private fun loadSessionlessLtiUrl(ltiUrl: String) { - ltiUrlLaunchJob = weave { - val tool = getLtiTool(ltiUrl) - tool?.url?.let { launchCustomTab(it) } ?: displayError() - } - } - - private fun launchCustomTab(url: String) { - val uri = Uri.parse(url) - .buildUpon() - .appendQueryParameter("display", "borderless") - .appendQueryParameter("platform", "android") - .build() - - val colorSchemeParams = CustomTabColorSchemeParams.Builder() - .setToolbarColor(canvasContext.color) - .build() - - var intent = CustomTabsIntent.Builder() - .setDefaultColorSchemeParams(colorSchemeParams) - .setShowTitle(true) - .build() - .intent - - intent.data = uri - - // Exclude Instructure apps from chooser options - intent = intent.asChooserExcludingInstructure() - - context?.startActivity(intent) - - customTabLaunched = true - } - - private fun displayError() { - toast(R.string.errorOccurred) - if (activity != null) { - requireActivity().onBackPressed() - } - } - - private suspend fun getLtiTool(url: String): LTITool? { - return ltiTool?.let { - AssignmentManager.getExternalToolLaunchUrlAsync(it.courseId, it.id, it.assignmentId).await().dataOrNull - } ?: SubmissionManager.getLtiFromAuthenticationUrlAsync(url, true).await().dataOrNull - } - - override fun onDestroy() { - super.onDestroy() - ltiUrlLaunchJob?.cancel() - } - - companion object { - const val LTI_URL = "ltiUrl" - - fun makeLTIBundle(ltiUrl: String, title: String, sessionLessLaunch: Boolean): Bundle { - val args = Bundle() - args.putString(LTI_URL, ltiUrl) - args.putBoolean(Const.SESSIONLESS_LAUNCH, sessionLessLaunch) - args.putString(Const.ACTION_BAR_TITLE, title) - return args - } - - fun makeRoute(canvasContext: CanvasContext, ltiTab: Tab): Route { - val bundle = Bundle().apply { putParcelable(Const.TAB, ltiTab) } - return Route(LtiLaunchFragment::class.java, canvasContext, bundle) - } - - /** - * The ltiTool param is used specifically for launching assignment based lti tools, where its possible to have - * a tool "collision". As such, we need to pre-fetch the correct tool to use here. - */ - fun makeRoute( - canvasContext: CanvasContext, - url: String, - title: String? = null, - sessionLessLaunch: Boolean = false, - isAssignmentLTI: Boolean = false, - ltiTool: LTITool? = null - ): Route { - val bundle = Bundle().apply { - putString(LTI_URL, url) - putBoolean(Const.SESSIONLESS_LAUNCH, sessionLessLaunch) - putBoolean(Const.ASSIGNMENT_LTI, isAssignmentLTI) - putString(Const.ACTION_BAR_TITLE, title) // For 'title' property in InternalWebViewFragment - putParcelable(Const.LTI_TOOL, ltiTool) - } - return Route(LtiLaunchFragment::class.java, canvasContext, bundle) - } - - fun validateRoute(route: Route): Boolean { - route.canvasContext ?: return false - return route.arguments.getParcelable(Const.TAB) != null || route.arguments.getString(LTI_URL).isValid() - } - - fun newInstance(route: Route): LtiLaunchFragment? { - if (!validateRoute(route)) return null - return LtiLaunchFragment().withArgs(route.argsWithContext) - } - - fun routeLtiLaunchFragment(activity: FragmentActivity, canvasContext: CanvasContext?, url: String) { - val args = makeLTIBundle(URLDecoder.decode(url, "utf-8"), activity.getString(R.string.utils_externalToolTitle), true) - RouteMatcher.route(activity, Route(LtiLaunchFragment::class.java, canvasContext, args)) - } - } -} diff --git a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/SubmissionDetailsModels.kt b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/SubmissionDetailsModels.kt index 2e543cc714..19e28ee3ce 100644 --- a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/SubmissionDetailsModels.kt +++ b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/SubmissionDetailsModels.kt @@ -36,7 +36,7 @@ sealed class SubmissionDetailsEvent { data class DataLoaded( val assignment: DataResult, val rootSubmissionResult: DataResult, - val ltiUrlResult: DataResult?, + val ltiTool: DataResult?, val isStudioEnabled: Boolean, val quizResult: DataResult?, val studioLTIToolResult: DataResult?, @@ -93,7 +93,7 @@ sealed class SubmissionDetailsContentType { data class NoSubmissionContent(val canvasContext: CanvasContext, val assignment: Assignment, val isStudioEnabled: Boolean, val quiz: Quiz? = null, val studioLTITool: LTITool? = null, val isObserver: Boolean = false, val ltiTool: LTITool? = null) : SubmissionDetailsContentType() object NoneContent : SubmissionDetailsContentType() - data class ExternalToolContent(val canvasContext: CanvasContext, val url: String, val newQuizLti: Boolean = false) : SubmissionDetailsContentType() + data class ExternalToolContent(val canvasContext: CanvasContext, val ltiTool: LTITool?, val title: String, val ltiType: LtiType = LtiType.EXTERNAL_TOOL) : SubmissionDetailsContentType() object OnPaperContent : SubmissionDetailsContentType() data class UnsupportedContent(val assignmentId: Long) : SubmissionDetailsContentType() data class OtherAttachmentContent(val attachment: Attachment) : SubmissionDetailsContentType() diff --git a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/SubmissionDetailsUpdate.kt b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/SubmissionDetailsUpdate.kt index 920857c7b0..83cbf381f1 100644 --- a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/SubmissionDetailsUpdate.kt +++ b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/SubmissionDetailsUpdate.kt @@ -76,7 +76,7 @@ class SubmissionDetailsUpdate : UpdateInit SubmissionDetailsContentType.OnPaperContent Assignment.SubmissionType.EXTERNAL_TOOL.apiString in assignment?.submissionTypesRaw.orEmpty() -> { if (assignment?.isAllowedToSubmit == true) - SubmissionDetailsContentType.ExternalToolContent(canvasContext, ltiUrl?.url ?: "", assignment.isNewQuizLti()) + SubmissionDetailsContentType.ExternalToolContent(canvasContext, ltiTool, assignment.name.orEmpty(), assignment.ltiToolType()) else SubmissionDetailsContentType.LockedContent } - submission?.submissionType == null -> SubmissionDetailsContentType.NoSubmissionContent(canvasContext, assignment!!, isStudioEnabled!!, quiz, studioLTITool, isObserver, ltiUrl) + submission?.submissionType == null -> SubmissionDetailsContentType.NoSubmissionContent(canvasContext, assignment!!, isStudioEnabled!!, quiz, studioLTITool, isObserver, ltiTool) submission.workflowState != "submitted" && AssignmentUtils2.getAssignmentState(assignment, submission) in listOf(AssignmentUtils2.ASSIGNMENT_STATE_MISSING, AssignmentUtils2.ASSIGNMENT_STATE_GRADED_MISSING) -> SubmissionDetailsContentType.NoSubmissionContent(canvasContext, assignment!!, isStudioEnabled!!, quiz) else -> when (Assignment.getSubmissionTypeFromAPIString(submission.submissionType)) { // LTI submission Assignment.SubmissionType.BASIC_LTI_LAUNCH -> SubmissionDetailsContentType.ExternalToolContent( - canvasContext, - submission.previewUrl.validOrNull() ?: assignment?.url?.validOrNull() ?: assignment?.htmlUrl ?: "", false + canvasContext, + ltiTool, + title = assignment?.name.orEmpty(), + assignment?.ltiToolType() ?: LtiType.EXTERNAL_TOOL ) // Text submission diff --git a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/content/LtiSubmissionViewFragment.kt b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/content/LtiSubmissionViewFragment.kt index 7363e9c3f8..acdba593de 100644 --- a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/content/LtiSubmissionViewFragment.kt +++ b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/content/LtiSubmissionViewFragment.kt @@ -22,15 +22,18 @@ import android.view.View import android.view.ViewGroup import androidx.fragment.app.Fragment import com.instructure.canvasapi2.models.CanvasContext +import com.instructure.canvasapi2.models.LTITool +import com.instructure.canvasapi2.models.LtiType import com.instructure.pandautils.binding.viewBinding -import com.instructure.pandautils.utils.BooleanArg +import com.instructure.pandautils.features.lti.LtiLaunchFragment +import com.instructure.pandautils.utils.NullableParcelableArg import com.instructure.pandautils.utils.ParcelableArg +import com.instructure.pandautils.utils.SerializableArg import com.instructure.pandautils.utils.StringArg import com.instructure.pandautils.utils.ViewStyler import com.instructure.pandautils.utils.onClickWithRequireNetwork import com.instructure.student.R import com.instructure.student.databinding.FragmentLtiSubmissionViewBinding -import com.instructure.student.fragment.LtiLaunchFragment import com.instructure.student.mobius.assignmentDetails.submissionDetails.SubmissionDetailsContentType.ExternalToolContent import com.instructure.student.router.RouteMatcher @@ -39,7 +42,9 @@ class LtiSubmissionViewFragment : Fragment() { private val binding by viewBinding(FragmentLtiSubmissionViewBinding::bind) private var canvasContext: CanvasContext by ParcelableArg() private var url: String by StringArg() - private var newQuizLti: Boolean by BooleanArg() + private var ltiType: LtiType by SerializableArg(LtiType.EXTERNAL_TOOL) + private var title: String by StringArg() + private var ltiTool: LTITool? by NullableParcelableArg() override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { return inflater.inflate(R.layout.fragment_lti_submission_view, container, false) @@ -50,22 +55,34 @@ class LtiSubmissionViewFragment : Fragment() { ViewStyler.themeButton(binding.viewLtiButton) setUpViews() binding.viewLtiButton.onClickWithRequireNetwork { - val route = LtiLaunchFragment.makeRoute(canvasContext = canvasContext, url = url) - RouteMatcher.route(requireActivity(), route) + RouteMatcher.route( + requireActivity(), + LtiLaunchFragment.makeRoute( + canvasContext, + url, + title, + sessionLessLaunch = false, + assignmentLti = true, + ltiTool = ltiTool, + openInternally = ltiType.openInternally + ) + ) } } private fun setUpViews() { - binding.viewLtiButton.text = if (newQuizLti) getString(R.string.openTheQuizButton) else getString(R.string.openTool) - binding.ltiSubmissionTitle.text = if (newQuizLti) getString(R.string.newQuizSubmissionTitle) else getString(R.string.commentSubmissionTypeExternalTool) - binding.ltiSubmissionSubtitle.text = if (newQuizLti) getString(R.string.newQuizSubmissionSubtitle) else getString(R.string.speedGraderExternalToolMessage) + binding.viewLtiButton.text = getString(ltiType.openButtonRes) + binding.ltiSubmissionTitle.text = getString(ltiType.ltiTitleRes) + binding.ltiSubmissionSubtitle.text = getString(ltiType.ltiDescriptionRes) } companion object { fun newInstance(data: ExternalToolContent) = LtiSubmissionViewFragment().apply { canvasContext = data.canvasContext - url = data.url - newQuizLti = data.newQuizLti + url = data.ltiTool?.url.orEmpty() + ltiTool = data.ltiTool + title = data.title + ltiType = data.ltiType } } } diff --git a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/content/emptySubmission/ui/SubmissionDetailsEmptyContentView.kt b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/content/emptySubmission/ui/SubmissionDetailsEmptyContentView.kt index a52b0c6da9..fa6f602542 100644 --- a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/content/emptySubmission/ui/SubmissionDetailsEmptyContentView.kt +++ b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/content/emptySubmission/ui/SubmissionDetailsEmptyContentView.kt @@ -33,6 +33,7 @@ import com.instructure.canvasapi2.models.Quiz import com.instructure.canvasapi2.utils.AnalyticsEventConstants import com.instructure.canvasapi2.utils.ApiPrefs import com.instructure.pandautils.features.discussion.router.DiscussionRouterFragment +import com.instructure.pandautils.features.lti.LtiLaunchFragment import com.instructure.pandautils.utils.ThemePrefs import com.instructure.pandautils.utils.onClickWithRequireNetwork import com.instructure.pandautils.utils.setHidden @@ -43,16 +44,15 @@ import com.instructure.student.databinding.DialogSubmissionPickerBinding import com.instructure.student.databinding.DialogSubmissionPickerMediaBinding import com.instructure.student.databinding.FragmentSubmissionDetailsEmptyContentBinding import com.instructure.student.fragment.BasicQuizViewFragment -import com.instructure.student.fragment.LtiLaunchFragment import com.instructure.student.fragment.StudioWebViewFragment -import com.instructure.student.mobius.assignmentDetails.submissionDetails.content.emptySubmission.SubmissionDetailsEmptyContentEvent -import com.instructure.student.mobius.assignmentDetails.submissionDetails.content.emptySubmission.ui.SubmissionDetailsEmptyContentViewState.Loaded -import com.instructure.student.mobius.assignmentDetails.submissionDetails.ui.SubmissionTypesVisibilities import com.instructure.student.mobius.assignmentDetails.submission.annnotation.AnnotationSubmissionUploadFragment import com.instructure.student.mobius.assignmentDetails.submission.picker.PickerSubmissionMode import com.instructure.student.mobius.assignmentDetails.submission.picker.ui.PickerSubmissionUploadFragment import com.instructure.student.mobius.assignmentDetails.submission.text.ui.TextSubmissionUploadFragment import com.instructure.student.mobius.assignmentDetails.submission.url.ui.UrlSubmissionUploadFragment +import com.instructure.student.mobius.assignmentDetails.submissionDetails.content.emptySubmission.SubmissionDetailsEmptyContentEvent +import com.instructure.student.mobius.assignmentDetails.submissionDetails.content.emptySubmission.ui.SubmissionDetailsEmptyContentViewState.Loaded +import com.instructure.student.mobius.assignmentDetails.submissionDetails.ui.SubmissionTypesVisibilities import com.instructure.student.mobius.common.ui.MobiusView import com.instructure.student.router.RouteMatcher import com.spotify.mobius.functions.Consumer @@ -145,7 +145,7 @@ class SubmissionDetailsEmptyContentView( canvasContext, ltiTool?.url ?: "", title, - isAssignmentLTI = true, + assignmentLti = true, ltiTool = ltiTool )) } diff --git a/apps/student/src/main/java/com/instructure/student/mobius/elementary/resources/StudentResourcesRouter.kt b/apps/student/src/main/java/com/instructure/student/mobius/elementary/resources/StudentResourcesRouter.kt index c6c5ceff05..80a84af5f4 100644 --- a/apps/student/src/main/java/com/instructure/student/mobius/elementary/resources/StudentResourcesRouter.kt +++ b/apps/student/src/main/java/com/instructure/student/mobius/elementary/resources/StudentResourcesRouter.kt @@ -17,10 +17,13 @@ package com.instructure.student.mobius.elementary.resources import androidx.fragment.app.FragmentActivity -import com.instructure.canvasapi2.models.* +import com.instructure.canvasapi2.models.Course +import com.instructure.canvasapi2.models.LTITool +import com.instructure.canvasapi2.models.Recipient +import com.instructure.canvasapi2.models.User import com.instructure.pandautils.features.elementary.resources.itemviewmodels.ResourcesRouter +import com.instructure.pandautils.features.lti.LtiLaunchFragment import com.instructure.student.fragment.InboxComposeMessageFragment -import com.instructure.student.fragment.LtiLaunchFragment import com.instructure.student.router.RouteMatcher class StudentResourcesRouter(private val activity: FragmentActivity) : ResourcesRouter { @@ -32,7 +35,7 @@ class StudentResourcesRouter(private val activity: FragmentActivity) : Resources ltiTool.url ?: ltiTool.courseNavigation?.url ?: "", ltiTool.courseNavigation?.text ?: ltiTool.name ?: "", sessionLessLaunch = true, - isAssignmentLTI = false, + assignmentLti = false, ltiTool = ltiTool) RouteMatcher.route(activity, route) } diff --git a/apps/student/src/main/java/com/instructure/student/navigation/StudentWebViewRouter.kt b/apps/student/src/main/java/com/instructure/student/navigation/StudentWebViewRouter.kt index a05b3f115d..88fa970f5e 100644 --- a/apps/student/src/main/java/com/instructure/student/navigation/StudentWebViewRouter.kt +++ b/apps/student/src/main/java/com/instructure/student/navigation/StudentWebViewRouter.kt @@ -20,10 +20,10 @@ import android.os.Bundle import androidx.fragment.app.FragmentActivity import com.instructure.canvasapi2.models.CanvasContext import com.instructure.canvasapi2.utils.ApiPrefs +import com.instructure.pandautils.features.lti.LtiLaunchFragment import com.instructure.pandautils.navigation.WebViewRouter import com.instructure.student.activity.BaseRouterActivity import com.instructure.student.fragment.InternalWebviewFragment -import com.instructure.student.fragment.LtiLaunchFragment import com.instructure.student.router.RouteMatcher class StudentWebViewRouter(val activity: FragmentActivity) : WebViewRouter { @@ -53,7 +53,7 @@ class StudentWebViewRouter(val activity: FragmentActivity) : WebViewRouter { } override fun openLtiScreen(canvasContext: CanvasContext?, url: String) { - LtiLaunchFragment.routeLtiLaunchFragment(activity, canvasContext, url) + RouteMatcher.route(activity, LtiLaunchFragment.makeSessionlessLtiUrlRoute(activity, canvasContext, url)) } override fun launchInternalWebViewFragment(url: String, canvasContext: CanvasContext?) { diff --git a/apps/student/src/main/java/com/instructure/student/router/RouteResolver.kt b/apps/student/src/main/java/com/instructure/student/router/RouteResolver.kt index 36bcad986b..1ec4212010 100644 --- a/apps/student/src/main/java/com/instructure/student/router/RouteResolver.kt +++ b/apps/student/src/main/java/com/instructure/student/router/RouteResolver.kt @@ -3,6 +3,7 @@ package com.instructure.student.router import androidx.fragment.app.Fragment import com.instructure.canvasapi2.models.CanvasContext import com.instructure.interactions.router.Route +import com.instructure.pandautils.features.assignments.details.AssignmentDetailsFragment import com.instructure.pandautils.features.calendar.CalendarFragment import com.instructure.pandautils.features.calendarevent.createupdate.CreateUpdateEventFragment import com.instructure.pandautils.features.calendarevent.details.EventFragment @@ -12,6 +13,7 @@ import com.instructure.pandautils.features.dashboard.edit.EditDashboardFragment import com.instructure.pandautils.features.discussion.details.DiscussionDetailsWebViewFragment import com.instructure.pandautils.features.discussion.router.DiscussionRouterFragment import com.instructure.pandautils.features.inbox.list.InboxFragment +import com.instructure.pandautils.features.lti.LtiLaunchFragment import com.instructure.pandautils.features.notification.preferences.EmailNotificationPreferencesFragment import com.instructure.pandautils.features.notification.preferences.PushNotificationPreferencesFragment import com.instructure.pandautils.features.offline.offlinecontent.OfflineContentFragment @@ -21,7 +23,6 @@ import com.instructure.pandautils.fragments.RemoteConfigParamsFragment import com.instructure.pandautils.utils.Const import com.instructure.student.AnnotationComments.AnnotationCommentListFragment import com.instructure.student.activity.NothingToSeeHereFragment -import com.instructure.pandautils.features.assignments.details.AssignmentDetailsFragment import com.instructure.student.features.assignments.list.AssignmentListFragment import com.instructure.student.features.coursebrowser.CourseBrowserFragment import com.instructure.student.features.discussion.details.DiscussionDetailsFragment @@ -55,7 +56,6 @@ import com.instructure.student.fragment.InboxComposeMessageFragment import com.instructure.student.fragment.InboxConversationFragment import com.instructure.student.fragment.InboxRecipientsFragment import com.instructure.student.fragment.InternalWebviewFragment -import com.instructure.student.fragment.LtiLaunchFragment import com.instructure.student.fragment.NotificationListFragment import com.instructure.student.fragment.ProfileSettingsFragment import com.instructure.student.fragment.StudioWebViewFragment diff --git a/apps/student/src/main/java/com/instructure/student/util/TabHelper.kt b/apps/student/src/main/java/com/instructure/student/util/TabHelper.kt index b3fd8de5ee..74b6e50eed 100644 --- a/apps/student/src/main/java/com/instructure/student/util/TabHelper.kt +++ b/apps/student/src/main/java/com/instructure/student/util/TabHelper.kt @@ -24,6 +24,7 @@ import com.instructure.canvasapi2.utils.ApiPrefs import com.instructure.canvasapi2.utils.ContextKeeper import com.instructure.canvasapi2.utils.validOrNull import com.instructure.interactions.router.Route +import com.instructure.pandautils.features.lti.LtiLaunchFragment import com.instructure.student.R import com.instructure.student.activity.NothingToSeeHereFragment import com.instructure.student.features.assignments.list.AssignmentListFragment @@ -37,12 +38,11 @@ import com.instructure.student.features.people.list.PeopleListFragment import com.instructure.student.features.quiz.list.QuizListFragment import com.instructure.student.fragment.AnnouncementListFragment import com.instructure.student.fragment.CourseSettingsFragment -import com.instructure.student.fragment.LtiLaunchFragment import com.instructure.student.fragment.NotificationListFragment import com.instructure.student.fragment.UnsupportedTabFragment import com.instructure.student.mobius.conferences.conference_list.ui.ConferenceListRepositoryFragment import com.instructure.student.mobius.syllabus.ui.SyllabusRepositoryFragment -import java.util.* +import java.util.Locale object TabHelper { diff --git a/apps/student/src/main/res/layout/activity_student_view_starter.xml b/apps/student/src/main/res/layout/activity_student_view_starter.xml index ce779a49cb..6ad182ea3a 100644 --- a/apps/student/src/main/res/layout/activity_student_view_starter.xml +++ b/apps/student/src/main/res/layout/activity_student_view_starter.xml @@ -23,7 +23,7 @@ android:orientation="vertical" android:background="@color/backgroundLightest"> - - - - - - - - - - diff --git a/apps/student/src/main/res/layout/loading_canvas_view.xml b/apps/student/src/main/res/layout/loading_canvas_view.xml index 8be21f26a2..1cf6836211 100644 --- a/apps/student/src/main/res/layout/loading_canvas_view.xml +++ b/apps/student/src/main/res/layout/loading_canvas_view.xml @@ -40,7 +40,7 @@ android:layout_gravity="center_horizontal|bottom" style="@style/TextFont.Medium"/> - . + * + */ +package com.instructure.teacher.features.lti + +import com.instructure.canvasapi2.models.CanvasContext +import com.instructure.pandautils.features.lti.LtiLaunchFragmentBehavior +import com.instructure.pandautils.utils.color + +class TeacherLtiLaunchFragmentBehavior(canvasContext: CanvasContext) : LtiLaunchFragmentBehavior { + override val toolbarColor: Int = canvasContext.color +} \ No newline at end of file diff --git a/apps/teacher/src/main/java/com/instructure/teacher/fragments/CourseBrowserFragment.kt b/apps/teacher/src/main/java/com/instructure/teacher/fragments/CourseBrowserFragment.kt index cb6eb0913f..179934ed0b 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/fragments/CourseBrowserFragment.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/fragments/CourseBrowserFragment.kt @@ -36,6 +36,7 @@ import com.instructure.interactions.router.Route import com.instructure.pandautils.analytics.SCREEN_VIEW_COURSE_BROWSER import com.instructure.pandautils.analytics.ScreenView import com.instructure.pandautils.binding.viewBinding +import com.instructure.pandautils.features.lti.LtiLaunchFragment import com.instructure.pandautils.fragments.BaseSyncFragment import com.instructure.pandautils.utils.Const import com.instructure.pandautils.utils.Const.CANVAS_STUDENT_ID @@ -285,11 +286,8 @@ class CourseBrowserFragment : BaseSyncFragment< Route(AttendanceListFragment::class.java, presenter.canvasContext, args) ) } else { - val args = LtiLaunchFragment.makeTabBundle(presenter.canvasContext, tab) - RouteMatcher.route( - requireActivity(), - Route(LtiLaunchFragment::class.java, presenter.canvasContext, args) - ) + val route = LtiLaunchFragment.makeRoute(presenter.canvasContext, tab) + RouteMatcher.route(requireActivity(), route) } } } diff --git a/apps/teacher/src/main/java/com/instructure/teacher/fragments/LtiLaunchFragment.kt b/apps/teacher/src/main/java/com/instructure/teacher/fragments/LtiLaunchFragment.kt deleted file mode 100644 index d57be8d7ea..0000000000 --- a/apps/teacher/src/main/java/com/instructure/teacher/fragments/LtiLaunchFragment.kt +++ /dev/null @@ -1,232 +0,0 @@ -/* - * Copyright (C) 2017 - 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.teacher.fragments - -import android.app.Activity -import android.net.Uri -import android.os.Bundle -import android.os.Handler -import android.view.View -import androidx.browser.customtabs.CustomTabColorSchemeParams -import androidx.browser.customtabs.CustomTabsIntent -import androidx.fragment.app.FragmentActivity -import com.instructure.canvasapi2.managers.SubmissionManager -import com.instructure.canvasapi2.models.CanvasContext -import com.instructure.canvasapi2.models.Course -import com.instructure.canvasapi2.models.Group -import com.instructure.canvasapi2.models.Tab -import com.instructure.canvasapi2.utils.ApiPrefs -import com.instructure.canvasapi2.utils.pageview.PageView -import com.instructure.canvasapi2.utils.pageview.PageViewUrl -import com.instructure.canvasapi2.utils.validOrNull -import com.instructure.canvasapi2.utils.weave.weave -import com.instructure.interactions.router.Route -import com.instructure.pandautils.analytics.SCREEN_VIEW_LTI_LAUNCH -import com.instructure.pandautils.analytics.ScreenView -import com.instructure.pandautils.binding.viewBinding -import com.instructure.pandautils.fragments.BaseFragment -import com.instructure.pandautils.utils.BooleanArg -import com.instructure.pandautils.utils.Const -import com.instructure.pandautils.utils.HtmlContentFormatter -import com.instructure.pandautils.utils.NullableParcelableArg -import com.instructure.pandautils.utils.NullableStringArg -import com.instructure.pandautils.utils.StringArg -import com.instructure.pandautils.utils.ThemePrefs -import com.instructure.pandautils.utils.ViewStyler -import com.instructure.pandautils.utils.asChooserExcludingInstructure -import com.instructure.pandautils.utils.color -import com.instructure.pandautils.utils.replaceWithURLQueryParameter -import com.instructure.pandautils.utils.setTextForVisibility -import com.instructure.pandautils.utils.toast -import com.instructure.teacher.R -import com.instructure.teacher.databinding.FragmentLtiLaunchBinding -import com.instructure.teacher.router.RouteMatcher -import kotlinx.coroutines.Job -import java.net.URLDecoder - -@PageView -@ScreenView(SCREEN_VIEW_LTI_LAUNCH) -class LtiLaunchFragment : BaseFragment() { - - private val binding by viewBinding(FragmentLtiLaunchBinding::bind) - - var canvasContext: CanvasContext? by NullableParcelableArg(key = Const.CANVAS_CONTEXT) - - private var title: String? by NullableStringArg(key = Const.TITLE) - private var ltiUrl: String by StringArg(key = LTI_URL) - private var ltiTab: Tab? by NullableParcelableArg(key = TAB) - private var sessionLessLaunch: Boolean by BooleanArg(key = SESSION_LESS) - - /* Tracks whether we have automatically started launching the LTI tool in a chrome custom tab. Because this fragment - re-runs certain logic in onResume, tracking the launch helps us know to pop this fragment instead of erroneously - launching again when the user returns to the app. */ - private var customTabLaunched: Boolean = false - - private var ltiUrlLaunchJob: Job? = null - - @Suppress("unused") - @PageViewUrl - private fun makePageViewUrl() = - ltiTab?.externalUrl ?: ApiPrefs.fullDomain + canvasContext?.toAPIString() + "/external_tools" - - override fun layoutResId(): Int = R.layout.fragment_lti_launch - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - customTabLaunched = savedInstanceState?.getBoolean(CUSTOM_TAB_LAUNCHED_STATE) ?: false - } - - override fun onSaveInstanceState(outState: Bundle) { - super.onSaveInstanceState(outState) - outState.putBoolean(CUSTOM_TAB_LAUNCHED_STATE, customTabLaunched) - } - - override fun onCreateView(view: View) = Unit - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - val color = canvasContext?.color ?: ThemePrefs.primaryColor - ViewStyler.setStatusBarDark(requireActivity(), color) - binding.loadingView.setOverrideColor(color) - binding.toolName.setTextForVisibility(title.validOrNull() ?: ltiTab?.label?.validOrNull() ?: ltiUrl.validOrNull()) - } - - override fun onResume() { - super.onResume() - // If onResume() is called after the custom tab has launched, it means the user is returning and we should close this fragment - if (customTabLaunched) { - /* Due to how fragment management is set up, attempting to pop this fragment directly from onResume can - result in a crash. We'll work around this by posting the action to the main thread message queue. */ - Handler().post { activity?.onBackPressed() } - return - } - - try { - when { - ltiTab != null -> getSessionlessLtiUrl(ltiTab!!.ltiUrl) - ltiUrl.isNotBlank() -> { - var url = ltiUrl // Replace deep link scheme - .replaceFirst("canvas-courses://", "${ApiPrefs.protocol}://") - .replaceFirst("canvas-student://", "${ApiPrefs.protocol}://") - .replaceWithURLQueryParameter(HtmlContentFormatter.hasKalturaUrl(ltiUrl)) - - if (sessionLessLaunch) { - if (url.contains("sessionless_launch")) { - getSessionlessLtiUrl(url) - } else { - val id = url.substringAfterLast("/external_tools/").substringBefore("?") - url = when { - (id.toIntOrNull() != null) -> when (canvasContext) { - is Course -> "${ApiPrefs.fullDomain}/api/v1/courses/${(canvasContext as Course).id}/external_tools/sessionless_launch?id=$id" - is Group -> "${ApiPrefs.fullDomain}/api/v1/groups/${(canvasContext as Group).id}/external_tools/sessionless_launch?id=$id" - else -> "${ApiPrefs.fullDomain}/api/v1/accounts/self/external_tools/sessionless_launch?id=$id" - } - - else -> { - when (canvasContext) { - is Course -> "${ApiPrefs.fullDomain}/api/v1/courses/${(canvasContext as Course).id}/external_tools/sessionless_launch?url=$url" - is Group -> "${ApiPrefs.fullDomain}/api/v1/groups/${(canvasContext as Group).id}/external_tools/sessionless_launch?url=$url" - else -> "${ApiPrefs.fullDomain}/api/v1/accounts/self/external_tools/sessionless_launch?url=$url" - } - } - } - getSessionlessLtiUrl(url) - } - } else { - launchCustomTab(url) - } - } - else -> displayError() - } - } catch (e: Exception) { - // If it gets here we're in trouble and won't know what the tab is, so just display an error message - displayError() - } - } - - private fun getSessionlessLtiUrl(url: String) { - ltiUrlLaunchJob = weave { - val tool = SubmissionManager.getLtiFromAuthenticationUrlAsync(url, true).await().dataOrNull - tool?.url?.let { launchCustomTab(it) } ?: displayError() - } - } - - private fun launchCustomTab(url: String) { - val uri = Uri.parse(url) - .buildUpon() - .appendQueryParameter("display", "borderless") - .appendQueryParameter("platform", "android") - .build() - - val colorSchemeParams = CustomTabColorSchemeParams.Builder() - .setToolbarColor(canvasContext?.color ?: ThemePrefs.primaryColor) - .build() - - var intent = CustomTabsIntent.Builder() - .setDefaultColorSchemeParams(colorSchemeParams) - .setShowTitle(true) - .build() - .intent - - intent.data = uri - - // Exclude Instructure apps from chooser options - intent = intent.asChooserExcludingInstructure() - - context?.startActivity(intent) - - customTabLaunched = true - } - - private fun displayError() { - toast(R.string.errorOccurred) - (requireContext() as? Activity)?.onBackPressed() - } - - override fun onDestroyView() { - super.onDestroyView() - ltiUrlLaunchJob?.cancel() - } - - companion object { - private const val TAB = "tab" - private const val LTI_URL = "lti_url" - private const val SESSION_LESS = "session_less" - private const val CUSTOM_TAB_LAUNCHED_STATE = "custom_tab_launched_state" - - fun makeTabBundle(canvasContext: CanvasContext, ltiTab: Tab): Bundle { - val args = createBundle(canvasContext) - args.putParcelable(TAB, ltiTab) - return args - } - - fun makeBundle(canvasContext: CanvasContext?, url: String, title: String, sessionLessLaunch: Boolean): Bundle { - val args = createBundle(canvasContext) - args.putString(LTI_URL, url) - args.putBoolean(SESSION_LESS, sessionLessLaunch) - args.putString(Const.TITLE, title) - return args - } - - fun newInstance(args: Bundle) = LtiLaunchFragment().apply { arguments = args } - - fun routeLtiLaunchFragment(activity: FragmentActivity, canvasContext: CanvasContext?, url: String) { - val args = makeBundle(canvasContext, URLDecoder.decode(url, "utf-8"), activity.getString(R.string.utils_externalToolTitle), true) - RouteMatcher.route(activity, Route(LtiLaunchFragment::class.java, canvasContext, args)) - } - } -} diff --git a/apps/teacher/src/main/java/com/instructure/teacher/fragments/PageDetailsFragment.kt b/apps/teacher/src/main/java/com/instructure/teacher/fragments/PageDetailsFragment.kt index 3ac8a2fcb4..de38eae11f 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/fragments/PageDetailsFragment.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/fragments/PageDetailsFragment.kt @@ -34,6 +34,7 @@ import com.instructure.interactions.MasterDetailInteractions import com.instructure.interactions.router.Route import com.instructure.pandautils.analytics.SCREEN_VIEW_PAGE_DETAILS import com.instructure.pandautils.analytics.ScreenView +import com.instructure.pandautils.features.lti.LtiLaunchFragment import com.instructure.pandautils.fragments.BasePresenterFragment import com.instructure.pandautils.utils.ParcelableArg import com.instructure.pandautils.utils.PermissionUtils @@ -202,7 +203,7 @@ class PageDetailsFragment : BasePresenterFragment< loadHtmlJob = binding.canvasWebViewWraper.webView.loadHtmlWithIframes(requireContext(), page.body, { if (view != null) binding.canvasWebViewWraper.loadHtml(it, page.title, baseUrl = this.page.htmlUrl) }) { - LtiLaunchFragment.routeLtiLaunchFragment(requireActivity(), canvasContext, it) + RouteMatcher.route(requireActivity(), LtiLaunchFragment.makeSessionlessLtiUrlRoute(requireActivity(), canvasContext, it)) } setupToolbar() } diff --git a/apps/teacher/src/main/java/com/instructure/teacher/fragments/QuizDetailsFragment.kt b/apps/teacher/src/main/java/com/instructure/teacher/fragments/QuizDetailsFragment.kt index 59b84fb8fd..2e82c965ac 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/fragments/QuizDetailsFragment.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/fragments/QuizDetailsFragment.kt @@ -36,6 +36,7 @@ import com.instructure.interactions.MasterDetailInteractions import com.instructure.interactions.router.Route import com.instructure.pandautils.analytics.SCREEN_VIEW_EDIT_QUIZ_DETAILS import com.instructure.pandautils.analytics.ScreenView +import com.instructure.pandautils.features.lti.LtiLaunchFragment import com.instructure.pandautils.fragments.BasePresenterFragment import com.instructure.pandautils.utils.Const import com.instructure.pandautils.utils.LongArg @@ -405,7 +406,7 @@ class QuizDetailsFragment : BasePresenterFragment< loadHtmlJob = instructionsWebViewWrapper.webView.loadHtmlWithIframes(requireContext(), quiz.description, { instructionsWebViewWrapper.loadHtml(it, quiz.title, baseUrl = this@QuizDetailsFragment.quiz.htmlUrl) }) { - LtiLaunchFragment.routeLtiLaunchFragment(requireActivity(), canvasContext, it) + RouteMatcher.route(requireActivity(), LtiLaunchFragment.makeSessionlessLtiUrlRoute(requireActivity(), canvasContext, it)) } } diff --git a/apps/teacher/src/main/java/com/instructure/teacher/fragments/SpeedGraderLtiSubmissionFragment.kt b/apps/teacher/src/main/java/com/instructure/teacher/fragments/SpeedGraderLtiSubmissionFragment.kt index ad31cc278e..29a0b6ebc4 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/fragments/SpeedGraderLtiSubmissionFragment.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/fragments/SpeedGraderLtiSubmissionFragment.kt @@ -20,19 +20,20 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import android.webkit.WebView import androidx.fragment.app.Fragment import com.instructure.canvasapi2.models.CanvasContext -import com.instructure.interactions.router.Route import com.instructure.pandautils.analytics.SCREEN_VIEW_SPEED_GRADER_LTI_SUBMISSION import com.instructure.pandautils.analytics.ScreenView import com.instructure.pandautils.binding.viewBinding import com.instructure.pandautils.utils.ParcelableArg import com.instructure.pandautils.utils.StringArg -import com.instructure.pandautils.utils.ViewStyler -import com.instructure.pandautils.utils.onClick +import com.instructure.pandautils.utils.enableAlgorithmicDarkening +import com.instructure.pandautils.utils.setGone +import com.instructure.pandautils.utils.setVisible +import com.instructure.pandautils.views.CanvasWebView import com.instructure.teacher.R import com.instructure.teacher.databinding.FragmentSpeedGraderLtiSubmissionBinding -import com.instructure.teacher.router.RouteMatcher import com.instructure.teacher.view.ExternalToolContent @ScreenView(SCREEN_VIEW_SPEED_GRADER_LTI_SUBMISSION) @@ -40,8 +41,7 @@ class SpeedGraderLtiSubmissionFragment : Fragment() { private val binding by viewBinding(FragmentSpeedGraderLtiSubmissionBinding::bind) - private var mUrl by StringArg() - private var mCanvasContext by ParcelableArg() + private var url by StringArg() override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { return inflater.inflate(R.layout.fragment_speed_grader_lti_submission, container, false) @@ -53,19 +53,30 @@ class SpeedGraderLtiSubmissionFragment : Fragment() { } private fun setupViews() { - ViewStyler.themeButton(binding.viewLtiButton) - binding.viewLtiButton.onClick { - val args = InternalWebViewFragment.makeBundle(mUrl, getString(R.string.canvasAPI_externalTool), shouldAuthenticate = true, shouldRouteInternally = false) - RouteMatcher.route(requireActivity(), Route(InternalWebViewFragment::class.java, mCanvasContext, args)) + binding.webView.enableAlgorithmicDarkening() + binding.webView.setZoomSettings(false) + binding.webView.canvasWebViewClientCallback = object : CanvasWebView.CanvasWebViewClientCallback { + override fun openMediaFromWebView(mime: String, url: String, filename: String) = Unit + + override fun onPageStartedCallback(webView: WebView, url: String) { + if (isAdded) binding.webViewProgress.setVisible() + } + + override fun onPageFinishedCallback(webView: WebView, url: String) { + if (isAdded) binding.webViewProgress.setGone() + } + + override fun canRouteInternallyDelegate(url: String): Boolean = false + + override fun routeInternallyCallback(url: String) = Unit } + binding.webView.loadUrl(url) } companion object { fun newInstance(content: ExternalToolContent) = SpeedGraderLtiSubmissionFragment().apply { - mCanvasContext = content.canvasContext - mUrl = content.url + url = content.url } } - } diff --git a/apps/teacher/src/main/java/com/instructure/teacher/navigation/TeacherWebViewRouter.kt b/apps/teacher/src/main/java/com/instructure/teacher/navigation/TeacherWebViewRouter.kt index 3540d69515..4d15cf9e9f 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/navigation/TeacherWebViewRouter.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/navigation/TeacherWebViewRouter.kt @@ -21,11 +21,11 @@ import androidx.fragment.app.FragmentActivity import com.instructure.canvasapi2.models.CanvasContext import com.instructure.canvasapi2.utils.ApiPrefs import com.instructure.interactions.router.Route +import com.instructure.pandautils.features.lti.LtiLaunchFragment import com.instructure.pandautils.navigation.WebViewRouter import com.instructure.teacher.activities.InternalWebViewActivity import com.instructure.teacher.fragments.FullscreenInternalWebViewFragment import com.instructure.teacher.fragments.InternalWebViewFragment -import com.instructure.teacher.fragments.LtiLaunchFragment import com.instructure.teacher.router.RouteMatcher class TeacherWebViewRouter(val activity: FragmentActivity) : WebViewRouter { @@ -53,7 +53,7 @@ class TeacherWebViewRouter(val activity: FragmentActivity) : WebViewRouter { } override fun openLtiScreen(canvasContext: CanvasContext?, url: String) { - LtiLaunchFragment.routeLtiLaunchFragment(activity, canvasContext, url) + RouteMatcher.route(activity, LtiLaunchFragment.makeSessionlessLtiUrlRoute(activity, canvasContext, url)) } override fun launchInternalWebViewFragment(url: String, canvasContext: CanvasContext?) { diff --git a/apps/teacher/src/main/java/com/instructure/teacher/router/RouteMatcher.kt b/apps/teacher/src/main/java/com/instructure/teacher/router/RouteMatcher.kt index acb8653247..132365d8b2 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/router/RouteMatcher.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/router/RouteMatcher.kt @@ -50,6 +50,7 @@ import com.instructure.pandautils.features.dashboard.edit.EditDashboardFragment import com.instructure.pandautils.features.discussion.details.DiscussionDetailsWebViewFragment import com.instructure.pandautils.features.discussion.router.DiscussionRouterFragment import com.instructure.pandautils.features.inbox.list.InboxFragment +import com.instructure.pandautils.features.lti.LtiLaunchFragment import com.instructure.pandautils.features.settings.SettingsFragment import com.instructure.pandautils.fragments.HtmlContentFragment import com.instructure.pandautils.loaders.OpenMediaAsyncTaskLoader @@ -96,7 +97,6 @@ import com.instructure.teacher.fragments.EditQuizDetailsFragment import com.instructure.teacher.fragments.FileListFragment import com.instructure.teacher.fragments.FullscreenInternalWebViewFragment import com.instructure.teacher.fragments.InternalWebViewFragment -import com.instructure.teacher.fragments.LtiLaunchFragment import com.instructure.teacher.fragments.MessageThreadFragment import com.instructure.teacher.fragments.PageDetailsFragment import com.instructure.teacher.fragments.PageListFragment @@ -513,7 +513,7 @@ object RouteMatcher : BaseRouteMatcher() { .newInstance(route.arguments) SettingsFragment::class.java.isAssignableFrom(cls) -> fragment = SettingsFragment.newInstance(route) ProfileEditFragment::class.java.isAssignableFrom(cls) -> fragment = ProfileEditFragment.newInstance(route.arguments) - LtiLaunchFragment::class.java.isAssignableFrom(cls) -> fragment = LtiLaunchFragment.newInstance(route.arguments) + LtiLaunchFragment::class.java.isAssignableFrom(cls) -> fragment = LtiLaunchFragment.newInstance(route) PeopleListFragment::class.java.isAssignableFrom(cls) -> fragment = PeopleListFragment.newInstance(canvasContext!!) StudentContextFragment::class.java.isAssignableFrom(cls) -> fragment = StudentContextFragment.newInstance(route.arguments) AttendanceListFragment::class.java.isAssignableFrom(cls) -> fragment = AttendanceListFragment diff --git a/apps/teacher/src/main/java/com/instructure/teacher/router/RouteResolver.kt b/apps/teacher/src/main/java/com/instructure/teacher/router/RouteResolver.kt index 2e707076f8..d14bf2b5ef 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/router/RouteResolver.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/router/RouteResolver.kt @@ -13,6 +13,7 @@ import com.instructure.pandautils.features.dashboard.edit.EditDashboardFragment import com.instructure.pandautils.features.discussion.details.DiscussionDetailsWebViewFragment import com.instructure.pandautils.features.discussion.router.DiscussionRouterFragment import com.instructure.pandautils.features.inbox.list.InboxFragment +import com.instructure.pandautils.features.lti.LtiLaunchFragment import com.instructure.pandautils.features.notification.preferences.EmailNotificationPreferencesFragment import com.instructure.pandautils.features.notification.preferences.PushNotificationPreferencesFragment import com.instructure.pandautils.features.settings.SettingsFragment @@ -53,7 +54,6 @@ import com.instructure.teacher.fragments.FeatureFlagsFragment import com.instructure.teacher.fragments.FileListFragment import com.instructure.teacher.fragments.FullscreenInternalWebViewFragment import com.instructure.teacher.fragments.InternalWebViewFragment -import com.instructure.teacher.fragments.LtiLaunchFragment import com.instructure.teacher.fragments.MessageThreadFragment import com.instructure.teacher.fragments.PageDetailsFragment import com.instructure.teacher.fragments.PageListFragment @@ -201,7 +201,7 @@ object RouteResolver { } else if (RemoteConfigParamsFragment::class.java.isAssignableFrom(cls)) { fragment = RemoteConfigParamsFragment() } else if (LtiLaunchFragment::class.java.isAssignableFrom(cls)) { - fragment = LtiLaunchFragment.newInstance(route.arguments) + fragment = LtiLaunchFragment.newInstance(route) } else if (PeopleListFragment::class.java.isAssignableFrom(cls)) { fragment = PeopleListFragment.newInstance(canvasContext!!) } else if (StudentContextFragment::class.java.isAssignableFrom(cls)) { diff --git a/apps/teacher/src/main/java/com/instructure/teacher/view/SubmissionContentView.kt b/apps/teacher/src/main/java/com/instructure/teacher/view/SubmissionContentView.kt index f81016eae0..473ffc246d 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/view/SubmissionContentView.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/view/SubmissionContentView.kt @@ -384,7 +384,6 @@ class SubmissionContentView( // LTI submission SubmissionType.BASIC_LTI_LAUNCH -> ExternalToolContent( - mCourse, submission.previewUrl.validOrNull() ?: mAssignment.url.validOrNull() ?: mAssignment.htmlUrl ?: "" ) @@ -1043,7 +1042,7 @@ class UploadMediaCommentEvent(val file: File, val assignmentId: Long, val course sealed class GradeableContent object NoSubmissionContent : GradeableContent() object NoneContent : GradeableContent() -class ExternalToolContent(val canvasContext: CanvasContext, val url: String) : GradeableContent() +class ExternalToolContent(val url: String) : GradeableContent() object OnPaperContent : GradeableContent() object UnsupportedContent : GradeableContent() class OtherAttachmentContent(val attachment: Attachment) : GradeableContent() diff --git a/apps/teacher/src/main/res/layout/activity_route_validator.xml b/apps/teacher/src/main/res/layout/activity_route_validator.xml index d5922f088f..446c382ba5 100644 --- a/apps/teacher/src/main/res/layout/activity_route_validator.xml +++ b/apps/teacher/src/main/res/layout/activity_route_validator.xml @@ -29,7 +29,7 @@ android:layout_height="match_parent" android:visibility="invisible"/> - - - - - - - - - - - - - diff --git a/apps/teacher/src/main/res/layout/fragment_page_details.xml b/apps/teacher/src/main/res/layout/fragment_page_details.xml index 60e2eb6680..f08875494d 100644 --- a/apps/teacher/src/main/res/layout/fragment_page_details.xml +++ b/apps/teacher/src/main/res/layout/fragment_page_details.xml @@ -40,7 +40,7 @@ android:layout_marginTop="8dp" android:layout_marginBottom="8dp" /> - . --> - + android:fillViewport="true"> - - - - - - - + android:layout_height="match_parent" /> - - -