diff --git a/apps/flutter_parent/pubspec.yaml b/apps/flutter_parent/pubspec.yaml index be8cd80c1a..98699a76f3 100644 --- a/apps/flutter_parent/pubspec.yaml +++ b/apps/flutter_parent/pubspec.yaml @@ -25,7 +25,7 @@ description: Canvas Parent # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -version: 3.3.6+41 +version: 3.4.0+42 module: androidX: true diff --git a/apps/student/build.gradle b/apps/student/build.gradle index fd904fddd4..525fcedb68 100644 --- a/apps/student/build.gradle +++ b/apps/student/build.gradle @@ -54,8 +54,8 @@ android { applicationId "com.instructure.candroid" minSdkVersion Versions.MIN_SDK targetSdkVersion Versions.TARGET_SDK - versionCode = 241 - versionName = '6.18.2' + versionCode = 242 + versionName = '6.19.0' vectorDrawables.useSupportLibrary = true multiDexEnabled = true diff --git a/apps/student/flank.yml b/apps/student/flank.yml index c2a137af89..c1c32355e7 100644 --- a/apps/student/flank.yml +++ b/apps/student/flank.yml @@ -14,7 +14,7 @@ gcloud: test-targets: - notAnnotation com.instructure.canvas.espresso.E2E, com.instructure.canvas.espresso.Stub, com.instructure.canvas.espresso.FlakyE2E, com.instructure.canvas.espresso.KnownBug device: - - model: NexusLowRes + - model: Nexus6P version: 26 locale: en_US orientation: portrait diff --git a/apps/student/flank_e2e.yml b/apps/student/flank_e2e.yml index d7862027cb..f494132881 100644 --- a/apps/student/flank_e2e.yml +++ b/apps/student/flank_e2e.yml @@ -15,7 +15,7 @@ gcloud: - annotation com.instructure.canvas.espresso.E2E - notAnnotation com.instructure.canvas.espresso.Stub, com.instructure.canvas.espresso.FlakyE2E, com.instructure.canvas.espresso.KnownBug device: - - model: NexusLowRes + - model: Nexus6P version: 26 locale: en_US orientation: portrait diff --git a/apps/student/flank_e2e_coverage.yml b/apps/student/flank_e2e_coverage.yml index 87e66ca5e4..ef7cccbfd2 100644 --- a/apps/student/flank_e2e_coverage.yml +++ b/apps/student/flank_e2e_coverage.yml @@ -22,7 +22,7 @@ gcloud: - annotation com.instructure.canvas.espresso.E2E - notAnnotation com.instructure.canvas.espresso.Stub device: - - model: NexusLowRes + - model: Nexus6P version: 26 locale: en_US orientation: portrait diff --git a/apps/student/flank_e2e_flaky.yml b/apps/student/flank_e2e_flaky.yml index d0693d5bcc..9a73a6fd64 100644 --- a/apps/student/flank_e2e_flaky.yml +++ b/apps/student/flank_e2e_flaky.yml @@ -14,7 +14,7 @@ gcloud: test-targets: - annotation com.instructure.canvas.espresso.FlakyE2E device: - - model: NexusLowRes + - model: Nexus6P version: 26 locale: en_US orientation: portrait diff --git a/apps/student/flank_e2e_knownbug.yml b/apps/student/flank_e2e_knownbug.yml index 88fabb644b..b1cb8ff730 100644 --- a/apps/student/flank_e2e_knownbug.yml +++ b/apps/student/flank_e2e_knownbug.yml @@ -14,7 +14,7 @@ gcloud: test-targets: - annotation com.instructure.canvas.espresso.KnownBug device: - - model: NexusLowRes + - model: Nexus6P version: 26 locale: en_US orientation: portrait diff --git a/apps/student/flank_multi_api_level.yml b/apps/student/flank_multi_api_level.yml index c9c633567f..cf63ab1f30 100644 --- a/apps/student/flank_multi_api_level.yml +++ b/apps/student/flank_multi_api_level.yml @@ -14,15 +14,15 @@ gcloud: test-targets: - notAnnotation com.instructure.canvas.espresso.E2E, com.instructure.canvas.espresso.Stub device: - - model: NexusLowRes + - model: Nexus6P version: 27 locale: en_US orientation: portrait - - model: NexusLowRes + - model: Nexus6P version: 28 locale: en_US orientation: portrait - - model: NexusLowRes + - model: Nexus6P version: 29 locale: en_US orientation: portrait diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/FilesE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/FilesE2ETest.kt index 6431b77053..fc6ce70ea3 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/FilesE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/FilesE2ETest.kt @@ -196,7 +196,7 @@ class FilesE2ETest: StudentTest() { submissionDetailsPage.assertCommentAttachmentDisplayed(commentUploadInfo.fileName, student) Log.d(STEP_TAG,"Navigate back to Dashboard Page.") - ViewUtils.pressBackButton(5) + ViewUtils.pressBackButton(4) Log.d(STEP_TAG,"Navigate to 'Files' menu in user left-side menubar.") dashboardPage.gotoGlobalFiles() @@ -217,7 +217,6 @@ class FilesE2ETest: StudentTest() { Log.d(STEP_TAG,"Delete $newFileName file.") fileListPage.deleteFile(newFileName) - fileListPage.assertPageObjects() Log.d(STEP_TAG,"Assert that empty view is displayed after deletion.") fileListPage.assertViewEmpty() diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/InboxE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/InboxE2ETest.kt index 657015df25..95e09eecd4 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/InboxE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/InboxE2ETest.kt @@ -18,7 +18,11 @@ package com.instructure.student.ui.e2e import android.os.SystemClock.sleep import android.util.Log +import androidx.test.espresso.Espresso +import androidx.test.espresso.matcher.ViewMatchers import com.instructure.canvas.espresso.E2E +import com.instructure.canvas.espresso.refresh +import com.instructure.canvasapi2.apis.InboxApi import com.instructure.dataseeding.api.ConversationsApi import com.instructure.dataseeding.api.GroupsApi import com.instructure.panda_annotations.FeatureCategory @@ -53,26 +57,30 @@ class InboxE2ETest: StudentTest() { val student1 = data.studentsList[0] val student2 = data.studentsList[1] - // Create a group and put both students in it val groupCategory = GroupsApi.createCourseGroupCategory(course.id, teacher.token) val group = GroupsApi.createGroup(groupCategory.id, teacher.token) Log.d(PREPARATION_TAG, "Create group membership for ${student1.name} and ${student2.name} students to the group: ${group.name}.") GroupsApi.createGroupMembership(group.id, student1.id, teacher.token) GroupsApi.createGroupMembership(group.id, student2.id, teacher.token) - Log.d(PREPARATION_TAG,"Seed an email from the teacher to ${student1.name} and ${student2.name} students.") - val seededConversation = ConversationsApi.createConversation( - token = teacher.token, - recipients = listOf(student1.id.toString(), student2.id.toString()) - ).get(0) - Log.d(STEP_TAG,"Login with user: ${student1.name}, login id: ${student1.loginId} , password: ${student1.password}") tokenLogin(student1) dashboardPage.waitForRender() + dashboardPage.assertDisplaysCourse(course) Log.d(STEP_TAG,"Open Inbox Page. Assert that the previously seeded conversation is displayed.") dashboardPage.clickInboxTab() - inboxPage.assertPageObjects() //TODO: Refactor to assert to the empty view just like in teacher would be better. AFTER THAT, seed the conversation. + inboxPage.assertInboxEmpty() + + Log.d(PREPARATION_TAG,"Seed an email from the teacher to ${student1.name} and ${student2.name} students.") + val seededConversation = ConversationsApi.createConversation( + token = teacher.token, + recipients = listOf(student1.id.toString(), student2.id.toString()) + )[0] + + Log.d(STEP_TAG,"Refresh the page. Assert that there is a conversation and it is the previously seeded one.") + refresh() + inboxPage.assertHasConversation() inboxPage.assertConversationDisplayed(seededConversation) Log.d(STEP_TAG,"Click on 'New Message' button.") @@ -123,7 +131,58 @@ class InboxE2ETest: StudentTest() { Log.d(STEP_TAG,"Open Inbox Page. Assert that both, the previously seeded 'normal' conversation and the group conversation are displayed.") dashboardPage.clickInboxTab() inboxPage.assertConversationDisplayed(seededConversation) - inboxPage.assertConversationDisplayed("Hey There") + inboxPage.assertConversationDisplayed(newMessageSubject) + inboxPage.assertConversationDisplayed("Group Message") + + Log.d(STEP_TAG,"Select $newGroupMessageSubject conversation.") + inboxPage.selectConversation(newMessageSubject) + val newReplyMessage = "This is a quite new reply message." + Log.d(STEP_TAG,"Reply to $newGroupMessageSubject conversation with '$newReplyMessage' message. Assert that the reply is displayed.") + inboxConversationPage.replyToMessage(newReplyMessage) + + Log.d(STEP_TAG,"Delete $newReplyMessage reply and assert is has been deleted.") + inboxConversationPage.deleteMessage(newReplyMessage) + inboxConversationPage.assertMessageNotDisplayed(newReplyMessage) + + Log.d(STEP_TAG,"Delete the whole '$newGroupMessageSubject' subject and assert that it has been removed from the conversation list on the Inbox Page.") + inboxConversationPage.deleteConversation() //After deletion we will be navigated back to Inbox Page + inboxPage.assertConversationNotDisplayed(newMessageSubject) + inboxPage.assertConversationDisplayed(seededConversation) inboxPage.assertConversationDisplayed("Group Message") + + Log.d(STEP_TAG,"Select ${seededConversation.subject} conversation. Assert that is has not been starred already.") + inboxPage.selectConversation(seededConversation) + inboxConversationPage.assertNotStarred() + + Log.d(STEP_TAG,"Toggle Starred to mark ${seededConversation.subject} conversation as favourite. Assert that it has became starred.") + inboxConversationPage.toggleStarred() + inboxConversationPage.assertStarred() + + Log.d(STEP_TAG,"Navigate back to Inbox Page and assert that the conversation itself is starred as well.") + Espresso.pressBack() // To main inbox page + inboxPage.assertConversationStarred(seededConversation.subject) + + Log.d(STEP_TAG,"Select ${seededConversation.subject} conversation. Mark as Unread by clicking on the 'More Options' menu, 'Mark as Unread' menu point.") + inboxPage.assertUnreadMarkerVisibility(seededConversation.subject, ViewMatchers.Visibility.GONE) + inboxPage.selectConversation(seededConversation) + inboxConversationPage.markUnread() //After select 'Mark as Unread', we will be navigated back to Inbox Page + + Log.d(STEP_TAG,"Assert that ${seededConversation.subject} conversation has been marked as unread.") + inboxPage.assertUnreadMarkerVisibility(seededConversation.subject, ViewMatchers.Visibility.VISIBLE) + + Log.d(STEP_TAG,"Select ${seededConversation.subject} conversation. Archive it by clicking on the 'More Options' menu, 'Archive' menu point.") + inboxPage.selectConversation(seededConversation) + inboxConversationPage.archive() //After select 'Archive', we will be navigated back to Inbox Page + + Log.d(STEP_TAG,"Assert that ${seededConversation.subject} conversation has removed from 'All' tab.") //TODO: Discuss this logic if it's ok if we don't show Archived messages on 'All' tab... + inboxPage.assertConversationNotDisplayed(seededConversation) + + Log.d(STEP_TAG,"Select 'Archived' conversation filter.") + inboxPage.selectInboxScope(InboxApi.Scope.ARCHIVED) + + Log.d(STEP_TAG,"Assert that ${seededConversation.subject} conversation is displayed by the 'Archived' filter, and other conversations are not displayed.") + inboxPage.assertConversationDisplayed(seededConversation) + inboxPage.assertConversationNotDisplayed("Group Message") + } } \ No newline at end of file diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/SettingsE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/SettingsE2ETest.kt index f74e1faa41..aed91b26e3 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 @@ -51,10 +51,10 @@ class SettingsE2ETest : StudentTest() { Log.d(PREPARATION_TAG, "Seeding data.") val data = seedData(students = 1, teachers = 1, courses = 1) - val teacher = data.teachersList[0] + val student = data.studentsList[0] - Log.d(STEP_TAG, "Login with user: ${teacher.name}, login id: ${teacher.loginId} , password: ${teacher.password}") - tokenLogin(teacher) + Log.d(STEP_TAG, "Login with user: ${student.name}, login id: ${student.loginId} , password: ${student.password}") + tokenLogin(student) dashboardPage.waitForRender() Log.d(STEP_TAG, "Navigate to User Settings Page.") @@ -62,7 +62,7 @@ class SettingsE2ETest : StudentTest() { settingsPage.assertPageObjects() Log.d(STEP_TAG, "Open Profile Settings Page.") - settingsPage.launchProfileSettings() + settingsPage.openProfileSettings() profileSettingsPage.assertPageObjects() val newUserName = "John Doe" @@ -78,7 +78,7 @@ class SettingsE2ETest : StudentTest() { Log.d(STEP_TAG, "Navigate to Settings Page again and open Panda Avatar Creator.") dashboardPage.launchSettingsPage() settingsPage.assertPageObjects() - settingsPage.launchProfileSettings() + settingsPage.openProfileSettings() profileSettingsPage.assertPageObjects() profileSettingsPage.launchPandaAvatarCreator() @@ -106,6 +106,55 @@ class SettingsE2ETest : StudentTest() { } + @E2E + @Test + @TestMetaData(Priority.IMPORTANT, FeatureCategory.SETTINGS, TestCategory.E2E) + fun testDarkModeE2E() { + Log.d(PREPARATION_TAG, "Seeding data.") + val data = seedData(students = 1, teachers = 1, courses = 1) + val student = data.studentsList[0] + val course = data.coursesList[0] + + Log.d(STEP_TAG, "Login with user: ${student.name}, login id: ${student.loginId} , password: ${student.password}") + tokenLogin(student) + dashboardPage.waitForRender() + + Log.d(STEP_TAG, "Navigate to User Settings Page.") + dashboardPage.launchSettingsPage() + 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("#FFC7CDD1") //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() + dashboardPage.assertCourseLabelTextColor("#FFFFFFFF") + + Log.d(STEP_TAG,"Select ${course.name} course and assert on the Course Browser Page that the tabs has the proper text color (which is used in Dark mode).") + dashboardPage.selectCourse(course) + courseBrowserPage.assertTabLabelTextColor("Discussions","#FFFFFFFF") + courseBrowserPage.assertTabLabelTextColor("Grades","#FFFFFFFF") + + Log.d(STEP_TAG,"Navigate to Settings Page and open App Theme Settings again.") + Espresso.pressBack() + dashboardPage.launchSettingsPage() + 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("#FF2D3B45") //Currently, this color is used in the Light mode for the AppTheme Title texts. + settingsPage.assertAppThemeStatusTextColor("#FF556572") //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() + dashboardPage.assertCourseLabelTextColor("#FF2D3B45") + } + @E2E @Test @TestMetaData(Priority.MANDATORY, FeatureCategory.SETTINGS, TestCategory.E2E) @@ -124,7 +173,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.launchLegalPage() + settingsPage.openLegalPage() legalPage.assertPageObjects() } @@ -146,7 +195,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.launchAboutPage() + settingsPage.openAboutPage() aboutPage.assertPageObjects() Log.d(STEP_TAG,"Check that domain is equal to: ${student.domain} (student's domain).") @@ -182,7 +231,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.launchRemoteConfigParams() + settingsPage.openRemoteConfigParams() RemoteConfigParam.values().forEach { param -> @@ -202,7 +251,7 @@ class SettingsE2ETest : StudentTest() { Espresso.pressBack() Log.d(STEP_TAG, "Navigate to Remote Config Settings Page.") - settingsPage.launchRemoteConfigParams() + settingsPage.openRemoteConfigParams() Log.d(STEP_TAG, "Assert that all fields have maintained their initial value.") RemoteConfigParam.values().forEach { param -> diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/TodoE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/TodoE2ETest.kt index 34cef2228f..c0bda5daec 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/TodoE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/TodoE2ETest.kt @@ -16,7 +16,6 @@ import com.instructure.student.ui.utils.seedData import com.instructure.student.ui.utils.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test -import java.util.Calendar @HiltAndroidTest class TodoE2ETest: StudentTest() { @@ -33,28 +32,28 @@ class TodoE2ETest: StudentTest() { @TestMetaData(Priority.MANDATORY, FeatureCategory.TODOS, TestCategory.E2E) fun testTodoE2E() { - // Don't attempt this test on a Friday, Saturday or Sunday. - // The TODO tab doesn't seem to behave correctly on Fridays (or presumably weekends). - val dayOfWeek = Calendar.getInstance().get(Calendar.DAY_OF_WEEK) - if(dayOfWeek == Calendar.FRIDAY || dayOfWeek == Calendar.SATURDAY || dayOfWeek == Calendar.SUNDAY) { - println("We don't run the TODO E2E test on weekends") - return - } - Log.d(PREPARATION_TAG, "Seeding data.") val data = seedData(students = 1, teachers = 1, courses = 1) val student = data.studentsList[0] val teacher = data.teachersList[0] val course = data.coursesList[0] - Log.d(PREPARATION_TAG,"Seed an assignment for ${course.name} course.") + Log.d(PREPARATION_TAG,"Seed an assignment for ${course.name} course with tomorrow due date.") val seededAssignments = seedAssignments( courseId = course.id, teacherToken = teacher.token, dueAt = 1.days.fromNow.iso8601 ) + Log.d(PREPARATION_TAG,"Seed another assignment for ${course.name} course with 7 days from now due date.") + val seededAssignments2 = seedAssignments( + courseId = course.id, + teacherToken = teacher.token, + dueAt = 7.days.fromNow.iso8601 + ) + val testAssignment = seededAssignments[0] + val borderDateAssignment = seededAssignments2[0] //We show items in the to do section which are within 7 days. Log.d(PREPARATION_TAG,"Seed a quiz for ${course.name} course with tomorrow due date.") val quiz = QuizzesApi.createQuiz( @@ -66,6 +65,16 @@ class TodoE2ETest: StudentTest() { dueAt = 1.days.fromNow.iso8601) ) + Log.d(PREPARATION_TAG,"Seed another quiz for ${course.name} course with 8 days from now due date..") + val tooFarAwayQuiz = QuizzesApi.createQuiz( + QuizzesApi.CreateQuizRequest( + courseId = course.id, + withDescription = true, + published = true, + token = teacher.token, + dueAt = 8.days.fromNow.iso8601) + ) + Log.d(STEP_TAG, "Login with user: ${student.name}, login id: ${student.loginId} , password: ${student.password}") tokenLogin(student) dashboardPage.waitForRender() @@ -73,10 +82,12 @@ class TodoE2ETest: StudentTest() { Log.d(STEP_TAG,"Navigate to 'To Do' page via bottom-menu.") dashboardPage.clickTodoTab() - Log.d(STEP_TAG,"Assert that ${testAssignment.name} assignment is displayed.") + Log.d(STEP_TAG,"Assert that ${testAssignment.name} assignment is displayed and ${borderDateAssignment.name} is displayed because it's 7 days away from now..") todoPage.assertAssignmentDisplayed(testAssignment) + todoPage.assertAssignmentDisplayed(borderDateAssignment) - Log.d(STEP_TAG,"Assert that ${quiz.title} quiz is displayed.") + Log.d(STEP_TAG,"Assert that ${quiz.title} quiz is displayed and ${tooFarAwayQuiz.title} quiz is not displayed because it's end date is more than a week away..") todoPage.assertQuizDisplayed(quiz) + todoPage.assertQuizNotDisplayed(tooFarAwayQuiz) } } \ No newline at end of file diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/AnnouncementInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/AnnouncementInteractionTest.kt index 56ffbf4801..a857544b3b 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/AnnouncementInteractionTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/AnnouncementInteractionTest.kt @@ -17,14 +17,8 @@ package com.instructure.student.ui.interaction import androidx.test.espresso.Espresso import androidx.test.espresso.web.webdriver.Locator -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.addCoursePermissions -import com.instructure.canvas.espresso.mockCanvas.addDiscussionTopicToCourse -import com.instructure.canvas.espresso.mockCanvas.init -import com.instructure.canvasapi2.models.CanvasContextPermission -import com.instructure.canvasapi2.models.Course -import com.instructure.canvasapi2.models.Tab -import com.instructure.canvasapi2.models.User +import com.instructure.canvas.espresso.mockCanvas.* +import com.instructure.canvasapi2.models.* import com.instructure.panda_annotations.FeatureCategory import com.instructure.panda_annotations.Priority import com.instructure.panda_annotations.TestCategory @@ -42,6 +36,10 @@ class AnnouncementInteractionTest : StudentTest() { private lateinit var course: Course private lateinit var user: User + private lateinit var group : Group + private lateinit var discussion : DiscussionTopicHeader + private lateinit var announcement : DiscussionTopicHeader + // Student enrolled in intended section can see and reply to the announcement // (This kind of seems like more of a test of the mocked endpoint, but we'll go with it.) @Test @@ -156,10 +154,12 @@ class AnnouncementInteractionTest : StudentTest() { val course = data.courses.values.first() val announcement = data.courseDiscussionTopicHeaders[course.id]!!.first() discussionListPage.assertTopicDisplayed(announcement.title!!) - discussionListPage.createAnnouncement("Announcement Topic", "Awesome announcement topic") + val newAnnouncementName = "Announcement Topic" + discussionListPage.createAnnouncement(newAnnouncementName, "Awesome announcement topic") + discussionListPage.assertAnnouncementCreated(newAnnouncementName) } - // Tests code around closing / aborting announcement creation + // Tests code around closing / aborting announcement creation (as a teacher) @Test @TestMetaData(Priority.IMPORTANT, FeatureCategory.ANNOUNCEMENTS, TestCategory.INTERACTION, false) fun testAnnouncementCreate_abort() { @@ -177,23 +177,34 @@ class AnnouncementInteractionTest : StudentTest() { discussionListPage.assertAnnouncementCount(2) // header + the one test announcement } - // Tests code around creating an announcement with no description + // Tests code around creating an announcement with no description (as a teacher) @Test @TestMetaData(Priority.COMMON, FeatureCategory.ANNOUNCEMENTS, TestCategory.INTERACTION, false) fun testAnnouncementCreate_missingDescription() { getToAnnouncementList() - discussionListPage.createAnnouncement("title", "", verify = false) - // easier than looking for the "A description is required" toast message - discussionListPage.assertOnNewAnnouncementPage() + discussionListPage.createAnnouncement("title", "") + discussionListPage.assertOnNewAnnouncementPage() // easier than looking for the "A description is required" toast message } - // Tests code around creating an announcement with no title + // Tests code around creating an announcement with no title (as a teacher) @Test @TestMetaData(Priority.COMMON, FeatureCategory.ANNOUNCEMENTS, TestCategory.INTERACTION, false) fun testAnnouncementCreate_missingTitle() { getToAnnouncementList() discussionListPage.createAnnouncement("", "description") + discussionListPage.assertAnnouncementCreated("") + } + + @Test + @TestMetaData(Priority.IMPORTANT, FeatureCategory.ANNOUNCEMENTS, TestCategory.INTERACTION, false) + fun testGroupAnnouncementCreateAsStudent() { + getToGroup() + + courseBrowserPage.selectAnnouncements() + val newAnnouncementName = "Student created Group Announcement" + discussionListPage.createAnnouncement(newAnnouncementName, "Cool group announcement") + discussionListPage.assertAnnouncementCreated(newAnnouncementName) } @Test @@ -206,6 +217,7 @@ class AnnouncementInteractionTest : StudentTest() { val existingAnnouncementName = announcement.title discussionListPage.createAnnouncement(testAnnouncementName, "description") + discussionListPage.assertAnnouncementCreated(testAnnouncementName) discussionListPage.clickOnSearchButton() discussionListPage.typeToSearchBar(testAnnouncementName) @@ -225,24 +237,82 @@ class AnnouncementInteractionTest : StudentTest() { courseCount: Int = 1, createSections: Boolean = false ): MockCanvas { + + val data = initData(studentCount,courseCount,createSections) + + val token = data.tokenFor(user)!! + tokenLogin(data.domain, token, user) + dashboardPage.waitForRender() + + dashboardPage.selectCourse(course) + + return data + } + + private fun getToGroup( + studentCount: Int = 1, + courseCount: Int = 1, + createSections: Boolean = false + ): MockCanvas { + + val data = initData(studentCount,courseCount,createSections) + + val token = data.tokenFor(user)!! + tokenLogin(data.domain, token, user) + dashboardPage.waitForRender() + + dashboardPage.selectGroup(group) + + return data + } + + private fun initData( studentCount: Int = 1, + courseCount: Int = 1, + createSections: Boolean = false): MockCanvas { val data = MockCanvas.init( - studentCount = studentCount, - courseCount = courseCount, - favoriteCourseCount = courseCount, - createSections = createSections) + studentCount = studentCount, + courseCount = courseCount, + favoriteCourseCount = courseCount, + createSections = createSections) course = data.courses.values.first() user = data.students[0] + // Add a group + val user = data.users.values.first() + group = data.addGroupToCourse( + course = course, + members = listOf(user), + isFavorite = true + ) + + // Add a discussion + discussion = data.addDiscussionTopicToCourse( + course = course, + user = user, + groupId = group.id + ) + + // Add an announcement + announcement = data.addDiscussionTopicToCourse( + course = course, + user = user, + groupId = group.id, + isAnnouncement = true + ) + val announcementsTab = Tab(position = 2, label = "Announcements", visibility = "public", tabId = Tab.ANNOUNCEMENTS_ID) data.courseTabs[course.id]!! += announcementsTab - val token = data.tokenFor(user)!! - tokenLogin(data.domain, token, user) - dashboardPage.waitForRender() - - dashboardPage.selectCourse(course) + data.groupTabs[group.id] = mutableListOf( + Tab(position = 0, label = "Discussions", tabId = Tab.DISCUSSIONS_ID, visibility = "public"), + Tab(position = 1, label = "Announcements", tabId = Tab.ANNOUNCEMENTS_ID, visibility = "public"), + ) + MockCanvas.data.addCoursePermissions( + course.id, + CanvasContextPermission(canCreateAnnouncement = true) + ) return data } diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/InboxInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/InboxInteractionTest.kt index b4c7a157f4..f3f75e8622 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/InboxInteractionTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/InboxInteractionTest.kt @@ -394,7 +394,7 @@ class InboxInteractionTest : StudentTest() { inboxConversationPage.toggleStarred() inboxConversationPage.assertStarred() Espresso.pressBack() // To main inbox page - inboxPage.assertConversationStarred(conversation) + inboxPage.assertConversationStarred(conversation.subject!!) } @Test 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 06953de666..7a861663c9 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 @@ -43,7 +43,7 @@ class ProfileSettingsInteractionTest : StudentTest() { tokenLogin(data.domain, token, student) dashboardPage.launchSettingsPage() - settingsPage.launchProfileSettings() + settingsPage.openProfileSettings() profileSettingsPage.changeUserNameTo(newUserName) Espresso.pressBack() // to settings page @@ -62,7 +62,7 @@ class ProfileSettingsInteractionTest : StudentTest() { tokenLogin(data.domain, token, student) dashboardPage.launchSettingsPage() - settingsPage.launchProfileSettings() + settingsPage.openProfileSettings() profileSettingsPage.assertSettingsDisabled() // No permissions granted } @@ -86,7 +86,7 @@ class ProfileSettingsInteractionTest : StudentTest() { // Navigate to avatar creation page dashboardPage.launchSettingsPage() - settingsPage.launchProfileSettings() + settingsPage.openProfileSettings() profileSettingsPage.launchPandaAvatarCreator() // Select head 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 da3ac3fb7b..1a2c2544c7 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 @@ -58,7 +58,7 @@ class SettingsInteractionTest : StudentTest() { setUpAndSignIn() dashboardPage.launchSettingsPage() - settingsPage.launchLegalPage() + settingsPage.openLegalPage() Intents.init() try { @@ -79,7 +79,7 @@ class SettingsInteractionTest : StudentTest() { setUpAndSignIn() dashboardPage.launchSettingsPage() - settingsPage.launchLegalPage() + settingsPage.openLegalPage() legalPage.openTermsOfUse() legalPage.assertTermsOfUseDisplayed() } @@ -91,7 +91,7 @@ class SettingsInteractionTest : StudentTest() { setUpAndSignIn() dashboardPage.launchSettingsPage() - settingsPage.launchLegalPage() + settingsPage.openLegalPage() legalPage.openPrivacyPolicy() canvasWebViewPage.acceptCookiePolicyIfNecessary() canvasWebViewPage.checkWebViewURL("https://www.instructure.com/canvas/privacy") @@ -107,7 +107,7 @@ class SettingsInteractionTest : StudentTest() { ApiPrefs.canGeneratePairingCode = true dashboardPage.launchSettingsPage() - settingsPage.launchPairObserverPage() + settingsPage.openPairObserverPage() pairObserverPage.hasCode("1") pairObserverPage.refresh() diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/TodoInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/TodoInteractionTest.kt index ffe511b42d..d30a428387 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/TodoInteractionTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/TodoInteractionTest.kt @@ -16,6 +16,7 @@ package com.instructure.student.ui.interaction import androidx.test.espresso.Espresso +import com.instructure.canvas.espresso.StubLandscape import com.instructure.canvas.espresso.mockCanvas.MockCanvas import com.instructure.canvas.espresso.mockCanvas.addAssignment import com.instructure.canvas.espresso.mockCanvas.addQuizToCourse @@ -63,8 +64,10 @@ class TodoInteractionTest : StudentTest() { } @Test + @StubLandscape("Stubbed because on lowres device in landscape mode, the space is too narrow to scroll properly. Will be refactored and running when we changed to non-lowres device on nightly runs.") @TestMetaData(Priority.IMPORTANT, FeatureCategory.TODOS, TestCategory.INTERACTION, false) fun testFilters() { + //TODO: Check and refactor (if necessary) after migrated nightly runs from lowres device to non-lowres one. val data = goToTodos(courseCount = 2, favoriteCourseCount = 1) val favoriteCourse = data.courses.values.first {course -> course.isFavorite} val notFavoriteCourse = data.courses.values.first {course -> !course.isFavorite} diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/CourseBrowserPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/CourseBrowserPage.kt index 669ce60d3c..723ce6ccce 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/CourseBrowserPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/CourseBrowserPage.kt @@ -23,22 +23,15 @@ import androidx.test.espresso.UiController import androidx.test.espresso.ViewAction import androidx.test.espresso.action.ViewActions import androidx.test.espresso.assertion.ViewAssertions.doesNotExist -import androidx.test.espresso.matcher.ViewMatchers.isAssignableFrom -import androidx.test.espresso.matcher.ViewMatchers.isDisplayed -import androidx.test.espresso.matcher.ViewMatchers.isDisplayingAtLeast -import androidx.test.espresso.matcher.ViewMatchers.withId -import androidx.test.espresso.matcher.ViewMatchers.withText +import androidx.test.espresso.matcher.ViewMatchers.* import com.instructure.canvas.espresso.scrollRecyclerView import com.instructure.canvas.espresso.withCustomConstraints import com.instructure.canvasapi2.models.Course import com.instructure.canvasapi2.models.Group import com.instructure.canvasapi2.models.Tab import com.instructure.dataseeding.model.CourseApiModel -import com.instructure.espresso.WaitForViewWithId -import com.instructure.espresso.assertHasText -import com.instructure.espresso.click +import com.instructure.espresso.* import com.instructure.espresso.page.BasePage -import com.instructure.espresso.swipeUp import com.instructure.pandautils.views.SwipeRefreshLayoutAppBar import com.instructure.student.R import org.hamcrest.Matcher @@ -136,6 +129,10 @@ class CourseBrowserPage : BasePage(R.id.courseBrowserPage) { assertTabDisplayed(tab.label!!) } + fun assertTabLabelTextColor(tabTitle: String, expectedColor: String) { + onView(withText(tabTitle)).check(TextViewColorAssertion(expectedColor)) + } + fun assertTabDisplayed(tabTitle: String) { recyclerViewScrollTo(allOf(withText(tabTitle),withId(R.id.label))) } diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/DashboardPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/DashboardPage.kt index 551826ff7b..8c343ee9b7 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/DashboardPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/DashboardPage.kt @@ -28,11 +28,7 @@ import androidx.test.espresso.action.ViewActions.click import androidx.test.espresso.assertion.ViewAssertions.doesNotExist import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.matcher.ViewMatchers -import androidx.test.espresso.matcher.ViewMatchers.isAssignableFrom -import androidx.test.espresso.matcher.ViewMatchers.isDisplayed -import androidx.test.espresso.matcher.ViewMatchers.isDisplayingAtLeast -import androidx.test.espresso.matcher.ViewMatchers.withContentDescription -import androidx.test.espresso.matcher.ViewMatchers.withText +import androidx.test.espresso.matcher.ViewMatchers.* import com.instructure.canvas.espresso.scrollRecyclerView import com.instructure.canvas.espresso.waitForMatcherWithSleeps import com.instructure.canvas.espresso.withCustomConstraints @@ -43,24 +39,8 @@ import com.instructure.canvasapi2.models.User import com.instructure.dataseeding.model.CanvasUserApiModel import com.instructure.dataseeding.model.CourseApiModel import com.instructure.dataseeding.model.GroupApiModel -import com.instructure.espresso.OnViewWithContentDescription -import com.instructure.espresso.OnViewWithId -import com.instructure.espresso.WaitForViewWithId -import com.instructure.espresso.assertDisplayed -import com.instructure.espresso.assertNotDisplayed -import com.instructure.espresso.click -import com.instructure.espresso.page.BasePage -import com.instructure.espresso.page.onView -import com.instructure.espresso.page.onViewWithId -import com.instructure.espresso.page.onViewWithText -import com.instructure.espresso.page.plus -import com.instructure.espresso.page.withAncestor -import com.instructure.espresso.page.withId -import com.instructure.espresso.page.withParent -import com.instructure.espresso.page.withText -import com.instructure.espresso.scrollTo -import com.instructure.espresso.swipeDown -import com.instructure.espresso.waitForCheck +import com.instructure.espresso.* +import com.instructure.espresso.page.* import com.instructure.student.R import org.hamcrest.CoreMatchers.allOf import org.hamcrest.CoreMatchers.containsString @@ -140,6 +120,10 @@ class DashboardPage : BasePage(R.id.dashboardPage) { } + fun assertCourseLabelTextColor(expectedTextColor: String) { + onView(withId(R.id.courseLabel)).check(TextViewColorAssertion(expectedTextColor)) + } + fun pressChangeUser() { onView(hamburgerButtonMatcher).click() onViewWithId(R.id.navigationDrawerItem_changeUser).scrollTo().click() @@ -179,7 +163,7 @@ class DashboardPage : BasePage(R.id.dashboardPage) { } fun assertUnreadEmails(count: Int) { - onView(allOf(withParent(R.id.bottomNavigationInbox), withId(R.id.badge), withText(count.toString()))).assertDisplayed() + onView(withId(R.id.bottomBar)).check(NotificationBadgeAssertion(R.id.bottomNavigationInbox, count)) } fun clickCalendarTab() { @@ -238,7 +222,7 @@ class DashboardPage : BasePage(R.id.dashboardPage) { fun selectCourse(course: Course) { assertDisplaysCourse(course) - onView(withText(course.originalName)).perform(withCustomConstraints(click(), isDisplayingAtLeast(10))) + onView(withId(R.id.titleTextView) + withText(course.originalName)).perform(withCustomConstraints(click(), isDisplayingAtLeast(10))) } fun selectGroup(group: Group) { diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/DiscussionDetailsPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/DiscussionDetailsPage.kt index 09bfe982d5..9717e6cd62 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/DiscussionDetailsPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/DiscussionDetailsPage.kt @@ -20,33 +20,21 @@ import android.os.SystemClock.sleep import androidx.test.espresso.Espresso import androidx.test.espresso.Espresso.onView import androidx.test.espresso.action.ViewActions.swipeDown -import androidx.test.espresso.action.ViewActions.swipeUp import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.matcher.ViewMatchers.isDisplayingAtLeast import androidx.test.espresso.matcher.ViewMatchers.withId -import androidx.test.espresso.web.assertion.WebViewAssertions import androidx.test.espresso.web.assertion.WebViewAssertions.webMatches import androidx.test.espresso.web.sugar.Web.onWebView import androidx.test.espresso.web.webdriver.DriverAtoms.findElement import androidx.test.espresso.web.webdriver.DriverAtoms.getText import androidx.test.espresso.web.webdriver.DriverAtoms.webClick import androidx.test.espresso.web.webdriver.Locator -import com.instructure.canvas.espresso.containsTextCaseInsensitive -import com.instructure.canvas.espresso.isElementDisplayed -import com.instructure.canvas.espresso.waitForMatcherWithSleeps -import com.instructure.canvas.espresso.withCustomConstraints -import com.instructure.canvas.espresso.withElementRepeat +import com.instructure.canvas.espresso.* import com.instructure.canvasapi2.models.DiscussionEntry import com.instructure.canvasapi2.models.DiscussionTopicHeader -import com.instructure.espresso.OnViewWithId -import com.instructure.espresso.assertDisplayed -import com.instructure.espresso.assertGone -import com.instructure.espresso.assertHasText -import com.instructure.espresso.assertNotDisplayed -import com.instructure.espresso.click +import com.instructure.espresso.* import com.instructure.espresso.page.BasePage -import com.instructure.espresso.page.withText -import com.instructure.espresso.scrollTo +import com.instructure.espresso.page.waitForViewWithId import com.instructure.student.R import com.instructure.student.ui.utils.TypeInRCETextEditor import org.hamcrest.Matchers.allOf @@ -112,7 +100,7 @@ class DiscussionDetailsPage : BasePage(R.id.discussionDetailsPage) { fun sendReply(replyMessage: String) { clickReply() - onView(withId(R.id.rce_webView)).perform(TypeInRCETextEditor(replyMessage)) + waitForViewWithId(R.id.rce_webView).perform(TypeInRCETextEditor(replyMessage)) onView(withId(R.id.menu_send)).click() sleep(3000) // wait out the toast message diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/DiscussionListPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/DiscussionListPage.kt index ff70d393ea..28bbb99a01 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/DiscussionListPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/DiscussionListPage.kt @@ -106,19 +106,19 @@ class DiscussionListPage : BasePage(R.id.discussionListPage) { waitForDiscussionTopicToDisplay(name) } - fun createAnnouncement(name: String, description: String, verify: Boolean = true) { + fun createAnnouncement(name: String, description: String) { createNewDiscussion.click() onView(withId(R.id.announcementNameEditText)).perform(DirectlyPopulateEditText(name)) onView(withId(R.id.rce_webView)).perform(TypeInRCETextEditor(description)) onView(withId(R.id.menuSaveAnnouncement)).perform(explicitClick()) + } - if(verify) { - var expectedTitle = name - if (name.isNullOrEmpty()) { + fun assertAnnouncementCreated(inputTitle: String) { + var expectedTitle = inputTitle + if (inputTitle.isNullOrEmpty()) { expectedTitle = InstrumentationRegistry.getInstrumentation().targetContext.resources.getString(R.string.utils_noTitle) } waitForDiscussionTopicToDisplay(expectedTitle) - } } fun launchCreateAnnouncementThenClose() { diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/InboxConversationPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/InboxConversationPage.kt index f289d611af..c703f81202 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/InboxConversationPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/InboxConversationPage.kt @@ -36,6 +36,7 @@ import androidx.test.espresso.matcher.ViewMatchers.isAssignableFrom import androidx.test.espresso.matcher.ViewMatchers.isDisplayingAtLeast import androidx.test.espresso.matcher.ViewMatchers.withContentDescription import androidx.test.espresso.matcher.ViewMatchers.withHint +import androidx.test.platform.app.InstrumentationRegistry import com.instructure.canvas.espresso.containsTextCaseInsensitive import com.instructure.canvas.espresso.explicitClick import com.instructure.canvas.espresso.scrollRecyclerView @@ -119,8 +120,8 @@ class InboxConversationPage : BasePage(R.id.inboxConversationPage) { fun assertMessageDisplayed(message: String) { val itemMatcher = CoreMatchers.allOf( - ViewMatchers.hasSibling(withId(R.id.attachmentContainer)), - ViewMatchers.hasSibling(withId(R.id.headerDivider)), + hasSibling(withId(R.id.attachmentContainer)), + hasSibling(withId(R.id.headerDivider)), withId(R.id.messageBody), withText(message) ) @@ -137,8 +138,8 @@ class InboxConversationPage : BasePage(R.id.inboxConversationPage) { } fun refresh() { - Espresso.onView(Matchers.allOf(ViewMatchers.withId(R.id.swipeRefreshLayout), ViewMatchers.isDisplayingAtLeast(10))) - .perform(withCustomConstraints(ViewActions.swipeDown(), ViewMatchers.isDisplayingAtLeast(10))) + onView(allOf(ViewMatchers.withId(R.id.swipeRefreshLayout), isDisplayingAtLeast(10))) + .perform(withCustomConstraints(ViewActions.swipeDown(), isDisplayingAtLeast(10))) } fun toggleStarred() { diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/InboxPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/InboxPage.kt index c4579b654a..4095b8436b 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/InboxPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/InboxPage.kt @@ -35,10 +35,13 @@ import com.instructure.canvasapi2.models.Conversation import com.instructure.canvasapi2.models.Course import com.instructure.dataseeding.model.ConversationApiModel import com.instructure.espresso.OnViewWithId +import com.instructure.espresso.RecyclerViewItemCountGreaterThanAssertion +import com.instructure.espresso.WaitForViewWithId import com.instructure.espresso.assertDisplayed import com.instructure.espresso.click import com.instructure.espresso.page.* import com.instructure.espresso.scrollTo +import com.instructure.espresso.swipeDown import com.instructure.student.R import org.hamcrest.Matchers.allOf import org.hamcrest.Matchers.not @@ -49,6 +52,7 @@ class InboxPage : BasePage(R.id.inboxPage) { private val createMessageButton by OnViewWithId(R.id.addMessage) private val scopeButton by OnViewWithId(R.id.filterButton) private val filterButton by OnViewWithId(R.id.inboxFilter) + private val inboxRecyclerView by WaitForViewWithId(R.id.inboxRecyclerView) fun assertConversationDisplayed(conversation: ConversationApiModel) { assertConversationDisplayed(conversation.subject) @@ -60,6 +64,10 @@ class InboxPage : BasePage(R.id.inboxPage) { onView(matcher).assertDisplayed() } + fun assertConversationNotDisplayed(conversation: ConversationApiModel) { + assertConversationNotDisplayed(conversation.subject) + } + fun assertConversationNotDisplayed(subject: String) { val matcher = withText(subject) onView(matcher).check(doesNotExist()) @@ -72,12 +80,16 @@ class InboxPage : BasePage(R.id.inboxPage) { onView(matcher).assertDisplayed() } - fun selectConversation(conversation: ConversationApiModel) { - val matcher = withText(conversation.subject) + fun selectConversation(subject: String) { + val matcher = withText(subject) scrollRecyclerView(R.id.inboxRecyclerView, matcher) onView(matcher).click() } + fun selectConversation(conversation: ConversationApiModel) { + selectConversation(conversation.subject) + } + fun selectConversation(conversation: Conversation) { waitForView(withId(R.id.inboxRecyclerView)) val matcher = withText(conversation.subject) @@ -110,13 +122,15 @@ class InboxPage : BasePage(R.id.inboxPage) { onView(withId(R.id.bottomNavigationHome)).click() } - fun assertConversationStarred(conversation: Conversation) { + fun assertConversationStarred(subject: String) { val matcher = allOf( - withId(R.id.star), - withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE), - ViewMatchers.withParent(ViewMatchers.withParent(withChild( - allOf(withId(R.id.message), withText(conversation.lastMessage)) - )))) + withId(R.id.star), + withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE), + hasSibling(withId(R.id.userName)), + hasSibling(withId(R.id.date)), + ViewMatchers.withParent(ViewMatchers.withParent(withChild( + allOf(withId(R.id.subjectView), withText(subject)))) + )) waitForMatcherWithRefreshes(matcher) // May need to refresh before the star shows up scrollRecyclerView(R.id.inboxRecyclerView, matcher) onView(matcher).assertDisplayed() @@ -127,7 +141,7 @@ class InboxPage : BasePage(R.id.inboxPage) { val matcher = allOf( withId(R.id.unreadMark), withEffectiveVisibility(visibility), - ViewMatchers.withParent(ViewMatchers.hasSibling(withChild( + ViewMatchers.withParent(hasSibling(withChild( allOf(withId(R.id.message), withText(conversation.lastMessage)) )))) @@ -141,5 +155,34 @@ class InboxPage : BasePage(R.id.inboxPage) { } } + fun assertUnreadMarkerVisibility(subject: String, visibility: ViewMatchers.Visibility) { + val matcher = allOf( + withId(R.id.unreadMark), + withEffectiveVisibility(visibility), + hasSibling(allOf(withId(R.id.avatar))), + ViewMatchers.withParent(hasSibling(withChild( + allOf(withId(R.id.subjectView), withText(subject)))) + ) + ) + if(visibility == ViewMatchers.Visibility.VISIBLE) { + waitForMatcherWithRefreshes(matcher) // May need to refresh before the unread mark shows up + scrollRecyclerView(R.id.inboxRecyclerView, matcher) + onView(matcher).assertDisplayed() + } + else if(visibility == ViewMatchers.Visibility.GONE) { + onView(matcher).check(matches(not(isDisplayed()))) + } + } + + fun assertInboxEmpty() { + onView(withId(R.id.emptyInboxView)).assertDisplayed() + } + fun assertHasConversation() { + assertConversationCountIsGreaterThan(0) + } + + fun assertConversationCountIsGreaterThan(count: Int) { + inboxRecyclerView.check(RecyclerViewItemCountGreaterThanAssertion(count)) + } } \ No newline at end of file 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 9cbb41a9de..5301e642e4 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 @@ -17,9 +17,9 @@ package com.instructure.student.ui.pages import com.instructure.espresso.OnViewWithId -import com.instructure.espresso.assertHasContentDescription +import com.instructure.espresso.TextViewColorAssertion import com.instructure.espresso.click -import com.instructure.espresso.page.BasePage +import com.instructure.espresso.page.* import com.instructure.espresso.scrollTo import com.instructure.student.R @@ -28,29 +28,49 @@ 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 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 remoteConfigLabel by OnViewWithId(R.id.remoteConfigParams) + private val appThemeTitle by OnViewWithId(R.id.appThemeTitle) + private val appThemeStatus by OnViewWithId(R.id.appThemeStatus) - fun launchAboutPage() { + fun openAboutPage() { aboutLabel.click() } - fun launchLegalPage() { + fun openLegalPage() { legalLabel.scrollTo().click() } - fun launchRemoteConfigParams() { + fun openRemoteConfigParams() { remoteConfigLabel.scrollTo().click() } - fun launchPairObserverPage() { + fun openPairObserverPage() { pairObserverLabel.scrollTo().click() } - fun launchProfileSettings() { + fun openProfileSettings() { profileSettingLabel.scrollTo().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)) + } } diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/TodoPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/TodoPage.kt index e49581bad1..34efa39342 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/TodoPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/TodoPage.kt @@ -17,15 +17,12 @@ package com.instructure.student.ui.pages import android.widget.Button -import androidx.test.espresso.Espresso import androidx.test.espresso.Espresso.onView import androidx.test.espresso.assertion.ViewAssertions.doesNotExist -import androidx.test.espresso.matcher.ViewMatchers import androidx.test.espresso.matcher.ViewMatchers.isAssignableFrom import androidx.test.espresso.matcher.ViewMatchers.withId import androidx.test.espresso.matcher.ViewMatchers.withText import com.instructure.canvas.espresso.containsTextCaseInsensitive -import com.instructure.canvas.espresso.scrollRecyclerView import com.instructure.canvasapi2.models.Assignment import com.instructure.canvasapi2.models.Quiz import com.instructure.dataseeding.model.AssignmentApiModel @@ -51,10 +48,18 @@ class TodoPage: BasePage(R.id.todoPage) { assertTextDisplayedInRecyclerView(assignment.name!!) } + fun assertQuizDisplayed(quiz: QuizApiModel) { + assertTextDisplayedInRecyclerView(quiz.title) + } + fun assertQuizDisplayed(quiz: Quiz) { assertTextDisplayedInRecyclerView(quiz.title!!) } + fun assertQuizNotDisplayed(quiz: QuizApiModel) { + onView(withText(quiz.title!!)).check(doesNotExist()) + } + fun assertQuizNotDisplayed(quiz: Quiz) { onView(withText(quiz.title!!)).check(doesNotExist()) } @@ -69,9 +74,7 @@ class TodoPage: BasePage(R.id.todoPage) { onView(withText(quiz.title!!)).click() } - fun assertQuizDisplayed(quiz: QuizApiModel) { - assertTextDisplayedInRecyclerView(quiz.title) - } + fun chooseFavoriteCourseFilter() { onView(withId(R.id.todoListFilter)).click() diff --git a/apps/student/src/main/java/com/instructure/student/AnnotationComments/AnnotationCommentListFragment.kt b/apps/student/src/main/java/com/instructure/student/AnnotationComments/AnnotationCommentListFragment.kt index e85b97f110..fcf1f0b600 100644 --- a/apps/student/src/main/java/com/instructure/student/AnnotationComments/AnnotationCommentListFragment.kt +++ b/apps/student/src/main/java/com/instructure/student/AnnotationComments/AnnotationCommentListFragment.kt @@ -58,6 +58,7 @@ class AnnotationCommentListFragment : ParentFragment() { private var docSession by ParcelableArg() private var apiValues by ParcelableArg() private var headAnnotationId by StringArg() + private var canComment by BooleanArg(true, CAN_COMMENT) private var recyclerAdapter: AnnotationCommentListRecyclerAdapter? = null @@ -132,7 +133,7 @@ class AnnotationCommentListFragment : ParentFragment() { private fun setupCommentInput() { // We want users with read permission to still be able to create and respond to comments. - if(docSession.annotationMetadata?.canRead() == false) { + if(docSession.annotationMetadata?.canRead() == false || !canComment) { commentInputContainer.setVisible(false) } else { sendCommentButton.imageTintList = ViewStyler.generateColorStateList( @@ -233,11 +234,12 @@ class AnnotationCommentListFragment : ParentFragment() { private const val DOC_SESSION = "docSession" private const val API_VALUES = "apiValues" private const val HEAD_ANNOTATION_ID = "headAnnotationId" + private const val CAN_COMMENT = "canComment" fun newInstance(bundle: Bundle) = AnnotationCommentListFragment().apply { arguments = bundle } - fun makeRoute(annotations: ArrayList, headAnnotationId: String, docSession: DocSession, apiValues: ApiValues, assigneeId: Long): Route { - val args = makeBundle(annotations, headAnnotationId, docSession, apiValues, assigneeId) + fun makeRoute(annotations: ArrayList, headAnnotationId: String, docSession: DocSession, apiValues: ApiValues, assigneeId: Long, canComment: Boolean): Route { + val args = makeBundle(annotations, headAnnotationId, docSession, apiValues, assigneeId, canComment) return Route(null, AnnotationCommentListFragment::class.java, null, args) } @@ -255,13 +257,14 @@ class AnnotationCommentListFragment : ParentFragment() { return AnnotationCommentListFragment().withArgs(route.arguments) } - fun makeBundle(annotations: ArrayList, headAnnotationId: String, docSession: DocSession, apiValues: ApiValues, assigneeId: Long): Bundle { + fun makeBundle(annotations: ArrayList, headAnnotationId: String, docSession: DocSession, apiValues: ApiValues, assigneeId: Long, canComment: Boolean): Bundle { val args = Bundle() args.putParcelableArrayList(ANNOTATIONS, annotations) args.putLong(ASSIGNEE_ID, assigneeId) args.putParcelable(DOC_SESSION, docSession) args.putParcelable(API_VALUES, apiValues) args.putString(HEAD_ANNOTATION_ID, headAnnotationId) + args.putBoolean(CAN_COMMENT, canComment) return args } } diff --git a/apps/student/src/main/java/com/instructure/student/activity/CallbackActivity.kt b/apps/student/src/main/java/com/instructure/student/activity/CallbackActivity.kt index d39e0c2b4b..4386a7ba62 100644 --- a/apps/student/src/main/java/com/instructure/student/activity/CallbackActivity.kt +++ b/apps/student/src/main/java/com/instructure/student/activity/CallbackActivity.kt @@ -49,7 +49,7 @@ abstract class CallbackActivity : ParentActivity(), InboxFragment.OnUnreadCountI private var loadInitialDataJob: Job? = null abstract fun gotLaunchDefinitions(launchDefinitions: List?) - abstract fun updateUnreadCount(unreadCount: String) + abstract fun updateUnreadCount(unreadCount: Int) abstract fun initialCoreDataLoadingComplete() override fun onCreate(savedInstanceState: Bundle?) { @@ -139,7 +139,8 @@ abstract class CallbackActivity : ParentActivity(), InboxFragment.OnUnreadCountI private suspend fun getUnreadMessageCount() { val unreadCount = awaitApi { UnreadCountManager.getUnreadConversationCount(it, true) } unreadCount.let { - updateUnreadCount(it.unreadCount!!) + val unreadCountInt = (it.unreadCount ?: "0").toInt() + updateUnreadCount(unreadCountInt) } } 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 579c9f20da..768f7c9380 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 @@ -34,6 +34,8 @@ import android.widget.CompoundButton import android.widget.ImageView import android.widget.TextView import android.widget.Toast +import androidx.annotation.IdRes +import androidx.annotation.PluralsRes import androidx.appcompat.app.ActionBarDrawerToggle import androidx.appcompat.app.AlertDialog import androidx.appcompat.widget.Toolbar @@ -1037,51 +1039,21 @@ class NavigationActivity : BaseRouterActivity(), Navigation, MasqueradingDialog. override fun canBookmark(): Boolean = navigationBehavior.visibleNavigationMenuItems.contains(NavigationMenuItem.BOOKMARKS) - override fun updateUnreadCount(unreadCount: String) { - // get the view - val bottomBarNavView = bottomBar?.getChildAt(0) - // get the inbox item - val view = (bottomBarNavView as BottomNavigationMenuView).getChildAt(4) - - // create the badge, set the text and color it - val unreadCountValue = unreadCount.toInt() - var unreadCountDisplay = unreadCount - if(unreadCountValue > 99) { - unreadCountDisplay = getString(R.string.moreThan99) - } else if(unreadCountValue <= 0) { - //don't set the badge or display it, remove any badge - if(view.children.size > 2 && view.children[2] is TextView) { - (view as BottomNavigationItemView).removeViewAt(2) - } - // update content description with no unread count number - bottomBar.menu.items.find { it.itemId == R.id.bottomNavigationInbox }.let { - val title = it?.title - MenuItemCompat.setContentDescription(it, title) - } - return - } - - // update content description - bottomBar.menu.items.find { it.itemId == R.id.bottomNavigationInbox }.let { - var title: String = it?.title as String - title += "$unreadCountValue " + getString(R.string.unread) - MenuItemCompat.setContentDescription(it, title) - } - - // check to see if we already have a badge created - with((view as BottomNavigationItemView)) { - // first child is the imageView that we use for the bottom bar, second is a layout for the label - if(childCount > 2 && getChildAt(2) is TextView) { - (getChildAt(2) as TextView).text = unreadCountDisplay - } else { - // no badge, we need to create one - val badge = LayoutInflater.from(context) - .inflate(R.layout.unread_count, bottomBar, false) - (badge as TextView).text = unreadCountDisplay + override fun updateUnreadCount(unreadCount: Int) { + updateBottomBarBadge(R.id.bottomNavigationInbox, unreadCount, R.plurals.a11y_inboxUnreadCount) + } - ColorUtils.colorIt(ContextCompat.getColor(context, R.color.backgroundInfo), badge.background) - addView(badge) + private fun updateBottomBarBadge(@IdRes menuItemId: Int, count: Int, @PluralsRes quantityContentDescription: Int? = null) { + if (count > 0) { + bottomBar.getOrCreateBadge(menuItemId).number = count + bottomBar.getOrCreateBadge(menuItemId).backgroundColor = getColor(R.color.backgroundInfo) + bottomBar.getOrCreateBadge(menuItemId).badgeTextColor = getColor(R.color.white) + if (quantityContentDescription != null) { + bottomBar.getOrCreateBadge(menuItemId).setContentDescriptionQuantityStringsResource(quantityContentDescription) } + } else { + // Don't set the badge or display it, remove any badge + bottomBar.removeBadge(menuItemId) } } diff --git a/apps/student/src/main/java/com/instructure/student/fragment/ApplicationSettingsFragment.kt b/apps/student/src/main/java/com/instructure/student/fragment/ApplicationSettingsFragment.kt index dce655d913..5e9131f1f8 100644 --- a/apps/student/src/main/java/com/instructure/student/fragment/ApplicationSettingsFragment.kt +++ b/apps/student/src/main/java/com/instructure/student/fragment/ApplicationSettingsFragment.kt @@ -23,8 +23,7 @@ import android.view.View import android.view.ViewGroup import androidx.appcompat.app.AlertDialog import androidx.fragment.app.Fragment -import com.instructure.canvasapi2.utils.APIHelper -import com.instructure.canvasapi2.utils.ApiPrefs +import com.instructure.canvasapi2.utils.* import com.instructure.canvasapi2.utils.pageview.PageView import com.instructure.loginapi.login.dialog.NoInternetConnectionDialog import com.instructure.pandautils.analytics.SCREEN_VIEW_APPLICATION_SETTINGS @@ -117,6 +116,11 @@ class ApplicationSettingsFragment : ParentFragment() { 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) } } diff --git a/apps/student/src/main/java/com/instructure/student/fragment/BasicQuizViewFragment.kt b/apps/student/src/main/java/com/instructure/student/fragment/BasicQuizViewFragment.kt index 3f7e2150f8..2466ecdfd1 100644 --- a/apps/student/src/main/java/com/instructure/student/fragment/BasicQuizViewFragment.kt +++ b/apps/student/src/main/java/com/instructure/student/fragment/BasicQuizViewFragment.kt @@ -116,7 +116,9 @@ class BasicQuizViewFragment : InternalWebviewFragment() { view.loadUrl(url, APIHelper.referrer) true } else { // It's content but not a quiz. Could link to a discussion (or whatever) in a quiz. Route - RouteMatcher.canRouteInternally(requireActivity(), url, ApiPrefs.domain, true) + activity?.let { + RouteMatcher.canRouteInternally(it, url, ApiPrefs.domain, true) + } ?: false }// Might need to log in to take the quiz -- the url would say domain/login. If we just use the AppRouter it will take the user // back to the dashboard. This check will keep them here and let them log in and take the quiz } diff --git a/apps/student/src/main/java/com/instructure/student/fragment/CreateAnnouncementFragment.kt b/apps/student/src/main/java/com/instructure/student/fragment/CreateAnnouncementFragment.kt index 026a1de8cc..299eb26e61 100644 --- a/apps/student/src/main/java/com/instructure/student/fragment/CreateAnnouncementFragment.kt +++ b/apps/student/src/main/java/com/instructure/student/fragment/CreateAnnouncementFragment.kt @@ -106,7 +106,7 @@ class CreateAnnouncementFragment : ParentFragment() { else -> null }?.let { imageUri -> // If the image Uri is not null, upload it - rceImageUploadJob = MediaUploadUtils.uploadRceImageJob(imageUri, canvasContext, requireActivity()) { text, alt -> announcementRCEView.insertImage(text, alt) } + rceImageUploadJob = MediaUploadUtils.uploadRceImageJob(imageUri, canvasContext, requireActivity()) { imageUrl -> announcementRCEView.insertImage(requireActivity(), imageUrl) } } } } diff --git a/apps/student/src/main/java/com/instructure/student/fragment/CreateDiscussionFragment.kt b/apps/student/src/main/java/com/instructure/student/fragment/CreateDiscussionFragment.kt index b7d1d89f62..2964157553 100644 --- a/apps/student/src/main/java/com/instructure/student/fragment/CreateDiscussionFragment.kt +++ b/apps/student/src/main/java/com/instructure/student/fragment/CreateDiscussionFragment.kt @@ -124,7 +124,7 @@ class CreateDiscussionFragment : ParentFragment() { else -> null }?.let { imageUri -> // If the image Uri is not null, upload it - rceImageJob = MediaUploadUtils.uploadRceImageJob(imageUri, canvasContext, requireActivity()) { text, alt -> descriptionRCEView.insertImage(text, alt) } + rceImageJob = MediaUploadUtils.uploadRceImageJob(imageUri, canvasContext, requireActivity()) { imageUrl -> descriptionRCEView.insertImage(requireActivity(), imageUrl) } } } } diff --git a/apps/student/src/main/java/com/instructure/student/fragment/DashboardFragment.kt b/apps/student/src/main/java/com/instructure/student/fragment/DashboardFragment.kt index 6acd19c5c4..b700bc17ec 100644 --- a/apps/student/src/main/java/com/instructure/student/fragment/DashboardFragment.kt +++ b/apps/student/src/main/java/com/instructure/student/fragment/DashboardFragment.kt @@ -134,7 +134,7 @@ class DashboardFragment : ParentFragment() { course.name = response.nickname!! course.originalName = response.name } - recyclerAdapter?.addOrUpdateItem(DashboardRecyclerAdapter.ItemType.COURSE_HEADER, course) + recyclerAdapter?.notifyDataSetChanged() } catch { toast(R.string.courseNicknameError) } diff --git a/apps/student/src/main/java/com/instructure/student/fragment/DiscussionListFragment.kt b/apps/student/src/main/java/com/instructure/student/fragment/DiscussionListFragment.kt index 0245b19e81..6ef8af3715 100644 --- a/apps/student/src/main/java/com/instructure/student/fragment/DiscussionListFragment.kt +++ b/apps/student/src/main/java/com/instructure/student/fragment/DiscussionListFragment.kt @@ -121,10 +121,12 @@ open class DiscussionListFragment : ParentFragment(), Bookmarkable { // Show the FAB. if(canPost) createNewDiscussion?.show() if (recyclerAdapter.size() == 0) { - if (isAnnouncement) { - setEmptyView(emptyView, R.drawable.ic_panda_noannouncements, R.string.noAnnouncements, R.string.noAnnouncementsSubtext) - } else { - setEmptyView(emptyView, R.drawable.ic_panda_nodiscussions, R.string.noDiscussions, R.string.noDiscussionsSubtext) + emptyView?.let { + if (isAnnouncement) { + setEmptyView(it, R.drawable.ic_panda_noannouncements, R.string.noAnnouncements, R.string.noAnnouncementsSubtext) + } else { + setEmptyView(it, R.drawable.ic_panda_nodiscussions, R.string.noDiscussions, R.string.noDiscussionsSubtext) + } } } } diff --git a/apps/student/src/main/java/com/instructure/student/fragment/DiscussionsReplyFragment.kt b/apps/student/src/main/java/com/instructure/student/fragment/DiscussionsReplyFragment.kt index 1fa11d9664..f352041ff8 100644 --- a/apps/student/src/main/java/com/instructure/student/fragment/DiscussionsReplyFragment.kt +++ b/apps/student/src/main/java/com/instructure/student/fragment/DiscussionsReplyFragment.kt @@ -119,7 +119,7 @@ class DiscussionsReplyFragment : ParentFragment() { else -> null }?.let { imageUri -> // If the image Uri is not null, upload it - MediaUploadUtils.uploadRceImageJob(imageUri, canvasContext, requireActivity()) { text, alt -> rceTextEditor.insertImage(text, alt) } + MediaUploadUtils.uploadRceImageJob(imageUri, canvasContext, requireActivity()) { imageUrl -> rceTextEditor.insertImage(requireActivity(), imageUrl) } } } } @@ -187,7 +187,7 @@ class DiscussionsReplyFragment : ParentFragment() { } } } catch { - if (isAdded && (it as StatusCallbackError).response?.code() != 400) messageFailure() + if (isVisible && (it as StatusCallbackError).response?.code() != 400) messageFailure() } } @@ -213,16 +213,16 @@ class DiscussionsReplyFragment : ParentFragment() { } else { // Post failure // 400 will be handled elsewhere. it means the quota has been reached - if (response.code() != 400 && isAdded) { + if (response.code() != 400 && isVisible) { messageFailure() } } } private fun messageFailure() { - toolbar.menu.findItem(R.id.menu_send).isVisible = true - toolbar.menu.findItem(R.id.menu_attachment).isVisible = true - savingProgressBar.visibility = View.GONE + toolbar.menu.findItem(R.id.menu_send)?.isVisible = true + toolbar.menu.findItem(R.id.menu_attachment)?.isVisible = true + savingProgressBar?.visibility = View.GONE toast(R.string.utils_discussionSentFailure) } //endregion diff --git a/apps/student/src/main/java/com/instructure/student/fragment/EditPageDetailsFragment.kt b/apps/student/src/main/java/com/instructure/student/fragment/EditPageDetailsFragment.kt index 1943616e0d..8135eb2e46 100644 --- a/apps/student/src/main/java/com/instructure/student/fragment/EditPageDetailsFragment.kt +++ b/apps/student/src/main/java/com/instructure/student/fragment/EditPageDetailsFragment.kt @@ -95,7 +95,7 @@ class EditPageDetailsFragment : ParentFragment() { else -> null }?.let { imageUri -> // If the image Uri is not null, upload it - rceImageJob = MediaUploadUtils.uploadRceImageJob(imageUri, canvasContext, requireActivity()) { text, alt -> pageRCEView.insertImage(text, alt) } + rceImageJob = MediaUploadUtils.uploadRceImageJob(imageUri, canvasContext, requireActivity()) { imageUrl -> pageRCEView.insertImage(requireActivity(), imageUrl) } } } } diff --git a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submission/annnotation/AnnotationSubmissionUploadFragment.kt b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submission/annnotation/AnnotationSubmissionUploadFragment.kt index 997bbecbcb..eb841adf7c 100644 --- a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submission/annnotation/AnnotationSubmissionUploadFragment.kt +++ b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submission/annnotation/AnnotationSubmissionUploadFragment.kt @@ -57,7 +57,7 @@ class AnnotationSubmissionUploadFragment : Fragment() { viewModel.pdfUrl.observe(viewLifecycleOwner, { binding.annotationSubmissionViewContainer.addView( - PdfStudentSubmissionView(requireActivity(), it, true) + PdfStudentSubmissionView(requireActivity(), it, studentAnnotationSubmit = true) ) }) diff --git a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submission/text/ui/TextSubmissionUploadView.kt b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submission/text/ui/TextSubmissionUploadView.kt index e91fb711af..f591a0773b 100644 --- a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submission/text/ui/TextSubmissionUploadView.kt +++ b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submission/text/ui/TextSubmissionUploadView.kt @@ -130,8 +130,13 @@ class TextSubmissionUploadView(inflater: LayoutInflater, parent: ViewGroup) : } } - private fun insertImage(text: String, alt: String) { - rce.insertImage(text, alt) + private fun insertImage(imageUrl: String) { + val activity = context as? Activity + if (activity != null) { + rce?.insertImage(activity, imageUrl) + } else { + rce?.insertImage(imageUrl, "") + } } fun showFailedImageMessage() { diff --git a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/content/AnnotationSubmissionViewFragment.kt b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/content/AnnotationSubmissionViewFragment.kt index 5d94035df7..e717dc1b01 100644 --- a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/content/AnnotationSubmissionViewFragment.kt +++ b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/content/AnnotationSubmissionViewFragment.kt @@ -48,7 +48,7 @@ class AnnotationSubmissionViewFragment : Fragment() { viewModel.loadAnnotatedPdfUrl(submissionId, submissionAttempt.toString()) viewModel.pdfUrl.observe(viewLifecycleOwner, { - binding.annotationSubmissionViewContainer.addView(PdfStudentSubmissionView(requireActivity(), it)) + binding.annotationSubmissionViewContainer.addView(PdfStudentSubmissionView(requireActivity(), it, studentAnnotationView = true)) }) return binding.root diff --git a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/content/PdfStudentSubmissionView.kt b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/content/PdfStudentSubmissionView.kt index ac48f263c5..df8caa206f 100644 --- a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/content/PdfStudentSubmissionView.kt +++ b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/content/PdfStudentSubmissionView.kt @@ -61,8 +61,9 @@ import java.util.ArrayList class PdfStudentSubmissionView( context: Context, private val pdfUrl: String, - private val studentAnnotation: Boolean = false -) : PdfSubmissionView(context), AnnotationManager.OnAnnotationCreationModeChangeListener, AnnotationManager.OnAnnotationEditingModeChangeListener { + private val studentAnnotationSubmit: Boolean = false, + private val studentAnnotationView: Boolean = false +) : PdfSubmissionView(context, studentAnnotationView), AnnotationManager.OnAnnotationCreationModeChangeListener, AnnotationManager.OnAnnotationEditingModeChangeListener { private var initJob: Job? = null private var deleteJob: Job? = null @@ -85,7 +86,7 @@ class PdfStudentSubmissionView( override fun setIsCurrentlyAnnotating(boolean: Boolean) {} override fun showAnnotationComments(commentList: ArrayList, headAnnotationId: String, docSession: DocSession, apiValues: ApiValues) { - if (isAttachedToWindow) RouteMatcher.route(context, AnnotationCommentListFragment.makeRoute(commentList, headAnnotationId, docSession, apiValues, ApiPrefs.user!!.id)) + if (isAttachedToWindow) RouteMatcher.route(context, AnnotationCommentListFragment.makeRoute(commentList, headAnnotationId, docSession, apiValues, ApiPrefs.user!!.id, !studentAnnotationView)) } override fun showFileError() { @@ -99,7 +100,7 @@ class PdfStudentSubmissionView( override fun configureCommentView(commentsButton: ImageView) { // If we are making annotations position the comments button as we would position in the teacher. - if (studentAnnotation) { + if (studentAnnotationSubmit) { super.configureCommentView(commentsButton) return } @@ -151,7 +152,7 @@ class PdfStudentSubmissionView( override fun attachDocListener() { // We need to add this flag, because we want to show the toolbar in the student annotation, but hide when // we open an already submitted file submission with a teacher's annotations. - if (!studentAnnotation) { + if (!studentAnnotationSubmit) { // Modify the session data permissions to make sure students can't annotate already submitted assignments if (docSession.annotationMetadata?.canWrite() == true) { docSession.annotationMetadata?.permissions = "read" diff --git a/apps/student/src/main/res/layout/unread_count.xml b/apps/student/src/main/res/layout/unread_count.xml deleted file mode 100644 index 6688c6fdee..0000000000 --- a/apps/student/src/main/res/layout/unread_count.xml +++ /dev/null @@ -1,36 +0,0 @@ - - - diff --git a/apps/teacher/build.gradle b/apps/teacher/build.gradle index 4fefc2a55e..b3baa306d5 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 = 47 - versionName = '1.17.0' + versionCode = 51 + versionName = '1.18.3' vectorDrawables.useSupportLibrary = true multiDexEnabled true testInstrumentationRunner 'com.instructure.teacher.ui.espresso.TeacherHiltTestRunner' diff --git a/apps/teacher/flank.yml b/apps/teacher/flank.yml index 5ad2bf0aa7..ffe312f9ee 100644 --- a/apps/teacher/flank.yml +++ b/apps/teacher/flank.yml @@ -11,7 +11,7 @@ gcloud: test-targets: - notAnnotation com.instructure.canvas.espresso.E2E, com.instructure.canvas.espresso.Stub, com.instructure.canvas.espresso.FlakyE2E, com.instructure.canvas.espresso.KnownBug device: - - model: NexusLowRes + - model: Nexus6P version: 26 locale: en_US orientation: portrait diff --git a/apps/teacher/flank_e2e.yml b/apps/teacher/flank_e2e.yml index 8e6947ac6d..8137c96453 100644 --- a/apps/teacher/flank_e2e.yml +++ b/apps/teacher/flank_e2e.yml @@ -15,7 +15,7 @@ gcloud: - annotation com.instructure.canvas.espresso.E2E - notAnnotation com.instructure.canvas.espresso.Stub, com.instructure.canvas.espresso.FlakyE2E, com.instructure.canvas.espresso.KnownBug device: - - model: NexusLowRes + - model: Nexus6P version: 26 locale: en_US orientation: portrait diff --git a/apps/teacher/flank_e2e_coverage.yml b/apps/teacher/flank_e2e_coverage.yml index 5976c65b23..eea5ec934e 100644 --- a/apps/teacher/flank_e2e_coverage.yml +++ b/apps/teacher/flank_e2e_coverage.yml @@ -22,7 +22,7 @@ gcloud: - annotation com.instructure.canvas.espresso.E2E - notAnnotation com.instructure.canvas.espresso.Stub device: - - model: NexusLowRes + - model: Nexus6P version: 26 locale: en_US orientation: portrait diff --git a/apps/teacher/flank_e2e_flaky.yml b/apps/teacher/flank_e2e_flaky.yml index 11036f912f..00402815a8 100644 --- a/apps/teacher/flank_e2e_flaky.yml +++ b/apps/teacher/flank_e2e_flaky.yml @@ -14,7 +14,7 @@ gcloud: test-targets: - annotation com.instructure.canvas.espresso.FlakyE2E device: - - model: NexusLowRes + - model: Nexus6P version: 26 locale: en_US orientation: portrait diff --git a/apps/teacher/flank_e2e_knownbug.yml b/apps/teacher/flank_e2e_knownbug.yml index b96ebb1cda..a8bee4b7e0 100644 --- a/apps/teacher/flank_e2e_knownbug.yml +++ b/apps/teacher/flank_e2e_knownbug.yml @@ -14,7 +14,7 @@ gcloud: test-targets: - annotation com.instructure.canvas.espresso.KnownBug device: - - model: NexusLowRes + - model: Nexus6P version: 26 locale: en_US orientation: portrait diff --git a/apps/teacher/flank_landscape.yml b/apps/teacher/flank_landscape.yml index 651b8bb67e..4fde941873 100644 --- a/apps/teacher/flank_landscape.yml +++ b/apps/teacher/flank_landscape.yml @@ -14,7 +14,7 @@ gcloud: test-targets: - notAnnotation com.instructure.canvas.espresso.E2E, com.instructure.canvas.espresso.Stub device: - - model: NexusLowRes + - model: Nexus6P version: 26 locale: en_US orientation: landscape diff --git a/apps/teacher/flank_multi_api_level.yml b/apps/teacher/flank_multi_api_level.yml index 5f609c55db..19dd916d04 100644 --- a/apps/teacher/flank_multi_api_level.yml +++ b/apps/teacher/flank_multi_api_level.yml @@ -14,15 +14,15 @@ gcloud: test-targets: - notAnnotation com.instructure.canvas.espresso.E2E, com.instructure.canvas.espresso.Stub device: - - model: NexusLowRes + - model: Nexus6P version: 27 locale: en_US orientation: portrait - - model: NexusLowRes + - model: Nexus6P version: 28 locale: en_US orientation: portrait - - model: NexusLowRes + - model: Nexus6P version: 29 locale: en_US orientation: portrait diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/FilesE2ETest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/FilesE2ETest.kt index f824cda036..f755422921 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/FilesE2ETest.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/FilesE2ETest.kt @@ -20,6 +20,7 @@ import android.os.Environment import android.util.Log import androidx.test.espresso.Espresso import com.instructure.canvas.espresso.E2E +import com.instructure.canvas.espresso.KnownBug import com.instructure.canvasapi2.managers.DiscussionManager import com.instructure.canvasapi2.models.CanvasContext import com.instructure.canvasapi2.models.DiscussionEntry @@ -58,6 +59,7 @@ class FilesE2ETest: TeacherTest() { @E2E @Test + @KnownBug @TestMetaData(Priority.MANDATORY, FeatureCategory.FILES, TestCategory.E2E) fun testFilesE2E() { @@ -204,6 +206,7 @@ class FilesE2ETest: TeacherTest() { Log.d(STEP_TAG,"Delete $newFileName file.") fileListPage.deleteFile(newFileName) + //TODO bug: https://instructure.atlassian.net/browse/MBL-16108 fileListPage.assertPageObjects() Log.d(STEP_TAG,"Assert that empty view is displayed after deletion.") 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 089059ad9a..f64998d7ee 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 @@ -18,16 +18,10 @@ package com.instructure.teacher.ui.e2e import android.util.Log import androidx.test.espresso.Espresso -import com.instructure.teacher.R -import androidx.test.espresso.Espresso.onView import androidx.test.espresso.NoMatchingViewException -import androidx.test.espresso.assertion.ViewAssertions.matches -import androidx.test.espresso.matcher.ViewMatchers.isDisplayed -import androidx.test.espresso.matcher.ViewMatchers.withId import com.instructure.canvas.espresso.E2E import com.instructure.canvasapi2.utils.RemoteConfigParam import com.instructure.canvasapi2.utils.RemoteConfigUtils -import com.instructure.espresso.ViewUtils import com.instructure.panda_annotations.FeatureCategory import com.instructure.panda_annotations.Priority import com.instructure.panda_annotations.TestCategory @@ -93,14 +87,62 @@ class SettingsE2ETest : TeacherTest() { Log.d(STEP_TAG,"Edit username to 'Unsaved userName' but DO NOT CLICK ON SAVE. Navigate back to Profile Settings Page without saving.") editProfileSettingsPage.editUserName("Unsaved userName") - ViewUtils.pressBackButton(2) - profileSettingsPage.assertPageObjects() + Espresso.pressBack() Log.d(STEP_TAG,"Assert that the username value remained $newUserName.") profileSettingsPage.assertUserNameIs(newUserName) } + @E2E + @Test + @TestMetaData(Priority.IMPORTANT, FeatureCategory.SETTINGS, TestCategory.E2E) + fun testDarkModeE2E() { + Log.d(PREPARATION_TAG, "Seeding data.") + val data = seedData(students = 1, teachers = 1, courses = 1) + val teacher = data.teachersList[0] + val course = data.coursesList[0] + + Log.d(STEP_TAG, "Login with user: ${teacher.name}, login id: ${teacher.loginId} , password: ${teacher.password}") + tokenLogin(teacher) + dashboardPage.waitForRender() + + Log.d(STEP_TAG, "Navigate to User Settings Page.") + dashboardPage.openUserSettingsPage() + 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("#FFC7CDD1") //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() + dashboardPage.assertCourseLabelTextColor("#FFFFFFFF") + + Log.d(STEP_TAG,"Select ${course.name} course and assert on the Course Browser Page that the tabs has the proper text color (which is used in Dark mode).") + dashboardPage.openCourse(course.name) + courseBrowserPage.assertTabLabelTextColor("Announcements","#FFFFFFFF") + courseBrowserPage.assertTabLabelTextColor("Assignments","#FFFFFFFF") + + Log.d(STEP_TAG,"Navigate to Settins Page and open App Theme Settings again.") + Espresso.pressBack() + dashboardPage.openUserSettingsPage() + 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("#FF2D3B45") //Currently, this color is used in the Light mode for the AppTheme Title texts. + settingsPage.assertAppThemeStatusTextColor("#FF556572") //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() + dashboardPage.assertCourseLabelTextColor("#FF2D3B45") + } + @E2E @Test @TestMetaData(Priority.MANDATORY, FeatureCategory.SETTINGS, TestCategory.E2E) diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/TodoE2ETest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/TodoE2ETest.kt index b0635a2fe7..5c41d4fecf 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/TodoE2ETest.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/TodoE2ETest.kt @@ -30,7 +30,6 @@ import com.instructure.panda_annotations.TestMetaData import com.instructure.teacher.ui.utils.* import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test -import java.util.* @HiltAndroidTest class TodoE2ETest : TeacherTest() { @@ -44,14 +43,6 @@ class TodoE2ETest : TeacherTest() { @Test @TestMetaData(Priority.MANDATORY, FeatureCategory.TODOS, TestCategory.E2E) fun testTodoE2E() { - // Inherited from student todo tests, may check this out later - // Don't attempt this test on a Friday, Saturday or Sunday. - // The TODO tab doesn't seem to behave correctly on Fridays (or presumably weekends). - val dayOfWeek = Calendar.getInstance().get(Calendar.DAY_OF_WEEK) - if(dayOfWeek == Calendar.FRIDAY || dayOfWeek == Calendar.SATURDAY || dayOfWeek == Calendar.SUNDAY) { - println("We don't run the TODO E2E test on weekends") - return - } Log.d(PREPARATION_TAG, "Seeding data.") val data = seedData(students = 1, teachers = 1, courses = 1) diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/CourseBrowserPage.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/CourseBrowserPage.kt index 5e98c063fa..e322ce3877 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/CourseBrowserPage.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/CourseBrowserPage.kt @@ -16,7 +16,6 @@ */ package com.instructure.teacher.ui.pages -import androidx.recyclerview.widget.RecyclerView import androidx.test.espresso.Espresso import androidx.test.espresso.NoMatchingViewException import androidx.test.espresso.PerformException @@ -24,7 +23,6 @@ import androidx.test.espresso.action.ViewActions import androidx.test.espresso.action.ViewActions.click import androidx.test.espresso.assertion.ViewAssertions import androidx.test.espresso.contrib.RecyclerViewActions.scrollToPosition -import androidx.test.espresso.matcher.BoundedMatcher import androidx.test.espresso.matcher.ViewMatchers import androidx.test.espresso.matcher.ViewMatchers.isDisplayingAtLeast import com.instructure.canvas.espresso.withCustomConstraints @@ -32,8 +30,6 @@ import com.instructure.espresso.* import com.instructure.espresso.page.* import com.instructure.teacher.R import com.instructure.teacher.holders.CourseBrowserViewHolder -import org.hamcrest.Description -import org.hamcrest.Matcher import org.hamcrest.Matchers.allOf class CourseBrowserPage : BasePage() { @@ -127,4 +123,8 @@ class CourseBrowserPage : BasePage() { fun assertCourseTitle(courseTitle: String) { onView(withId(R.id.courseBrowserTitle) + withText(courseTitle)).assertDisplayed() } + + fun assertTabLabelTextColor(tabTitle: String, expectedColor: String) { + onView(ViewMatchers.withText(tabTitle)).check(TextViewColorAssertion(expectedColor)) + } } diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/DashboardPage.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/DashboardPage.kt index 3492d7e598..49176acd6a 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/DashboardPage.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/DashboardPage.kt @@ -104,4 +104,8 @@ class DashboardPage : BasePage() { onView(hamburgerButtonMatcher).click() onViewWithId(R.id.navigationDrawerItem_files).click() } + + fun assertCourseLabelTextColor(expectedTextColor: String) { + onView(withId(R.id.courseLabel)).check(TextViewColorAssertion(expectedTextColor)) + } } \ No newline at end of file diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/RCEditorPage.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/RCEditorPage.kt deleted file mode 100644 index 59e5e0df8c..0000000000 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/RCEditorPage.kt +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright (C) 2017 - 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 com.instructure.espresso.OnViewWithId -import com.instructure.espresso.page.BasePage -import com.instructure.teacher.R - -class RCEditorPage: BasePage() { - private val webView by OnViewWithId(R.id.rce_webView) - private val saveButton by OnViewWithId(R.id.rce_save) - - private val actionUndo by OnViewWithId(R.id.action_undo) - private val actionRedo by OnViewWithId(R.id.action_redo) - private val actionBold by OnViewWithId(R.id.action_bold) - private val actionItalic by OnViewWithId(R.id.action_italic) - private val actionUnderline by OnViewWithId(R.id.action_underline) - - //These items may be off screen on phones, likely on screen for tablets. - private val actionTextColor by OnViewWithId(R.id.action_txt_color, autoAssert = false) - private val actionBulletList by OnViewWithId(R.id.action_insert_bullets, autoAssert = false) - private val actionUploadImage by OnViewWithId(R.id.actionUploadImage, autoAssert = false) - private val actionInsertLink by OnViewWithId(R.id.action_insert_link, autoAssert = false) -} diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/SettingsPage.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/SettingsPage.kt index 309038c3ff..8cb53c1ad8 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/SettingsPage.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/SettingsPage.kt @@ -20,8 +20,9 @@ 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.* import com.instructure.espresso.scrollTo import com.instructure.teacher.R @@ -33,6 +34,8 @@ class SettingsPage : BasePage(R.id.settingsPage) { private val legalLabel by OnViewWithId(R.id.legalButton) 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) fun openProfileSettingsPage() { profileSettingLabel.scrollTo().click() @@ -64,4 +67,21 @@ class SettingsPage : BasePage(R.id.settingsPage) { .check(ViewAssertions.matches(ViewMatchers.isDisplayed())) } } + + 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)) + } } diff --git a/apps/teacher/src/main/java/com/instructure/teacher/activities/BottomSheetActivity.kt b/apps/teacher/src/main/java/com/instructure/teacher/activities/BottomSheetActivity.kt index d762fd3e91..95b362cbae 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/activities/BottomSheetActivity.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/activities/BottomSheetActivity.kt @@ -48,15 +48,13 @@ import com.instructure.teacher.events.AssignmentDescriptionEvent import com.instructure.teacher.fragments.AddMessageFragment import com.instructure.teacher.router.RouteResolver import com.instructure.teacher.utils.getColorCompat -import instructure.rceditor.RCEConst.HTML_RESULT -import instructure.rceditor.RCEFragment import kotlinx.android.synthetic.main.activity_bottom_sheet.* import net.yslibrary.android.keyboardvisibilityevent.KeyboardVisibilityEvent import net.yslibrary.android.keyboardvisibilityevent.Unregistrar import org.greenrobot.eventbus.EventBus import retrofit2.Response -class BottomSheetActivity : BaseAppCompatActivity(), BottomSheetInteractions, RCEFragment.RCEFragmentCallbacks { +class BottomSheetActivity : BaseAppCompatActivity(), BottomSheetInteractions { private var mRoute: Route? = null private var mWindowHeight = 0 @@ -241,26 +239,10 @@ class BottomSheetActivity : BaseAppCompatActivity(), BottomSheetInteractions, RC return else super.onBackPressed() } else { - //Captures back press to prevent accidental exiting of assignment editing. - if(supportFragmentManager.findFragmentById(R.id.bottom) is RCEFragment) { - (supportFragmentManager.findFragmentById(R.id.bottom) as RCEFragment).showExitDialog() - return - } super.onBackPressed() } } - /** - * Handles RCEFragment results and passes them along - */ - override fun onResult(activityResult: Int, data: Intent?) { - val htmlResult = data?.getStringExtra(HTML_RESULT) - if (activityResult == Activity.RESULT_OK && htmlResult != null) { - EventBus.getDefault().postSticky(AssignmentDescriptionEvent(htmlResult)) - } - super.onBackPressed() - } - private fun keyboardHidden() { val params = bottom.layoutParams as PercentRelativeLayout.LayoutParams params.removeRule(RelativeLayout.ALIGN_PARENT_TOP) diff --git a/apps/teacher/src/main/java/com/instructure/teacher/activities/FullscreenActivity.kt b/apps/teacher/src/main/java/com/instructure/teacher/activities/FullscreenActivity.kt index 147a2d4148..8220b250bf 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/activities/FullscreenActivity.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/activities/FullscreenActivity.kt @@ -16,7 +16,6 @@ */ package com.instructure.teacher.activities -import android.app.Activity import android.content.Context import android.content.Intent import android.os.Bundle @@ -35,17 +34,13 @@ import com.instructure.interactions.router.Route import com.instructure.pandautils.interfaces.NavigationCallbacks import com.instructure.pandautils.utils.isCourseOrGroup import com.instructure.teacher.R -import com.instructure.teacher.events.AssignmentDescriptionEvent import com.instructure.teacher.router.RouteResolver import dagger.hilt.android.AndroidEntryPoint -import instructure.rceditor.RCEConst.HTML_RESULT -import instructure.rceditor.RCEFragment import kotlinx.android.synthetic.main.activity_fullscreen.* import kotlinx.coroutines.Job -import org.greenrobot.eventbus.EventBus @AndroidEntryPoint -class FullscreenActivity : BaseAppCompatActivity(), RCEFragment.RCEFragmentCallbacks, FullScreenInteractions { +class FullscreenActivity : BaseAppCompatActivity(), FullScreenInteractions { private var mRoute: Route? = null private var groupApiCall: Job? = null @@ -121,29 +116,12 @@ class FullscreenActivity : BaseAppCompatActivity(), RCEFragment.RCEFragmentCallb screen images will be the correct size, and the bottom bar will be easier to implement later*/ override fun onBackPressed() { - // Captures back press to prevent accidental exiting of assignment editing. - if(supportFragmentManager.findFragmentById(R.id.container) is RCEFragment) { - (supportFragmentManager.findFragmentById(R.id.container) as RCEFragment).showExitDialog() - return - } else if(supportFragmentManager.findFragmentById(R.id.container) is NavigationCallbacks) { - if((supportFragmentManager.findFragmentById(R.id.container) as NavigationCallbacks).onHandleBackPressed()) return + if (supportFragmentManager.findFragmentById(R.id.container) is NavigationCallbacks) { + if ((supportFragmentManager.findFragmentById(R.id.container) as NavigationCallbacks).onHandleBackPressed()) return } super.onBackPressed() } - /** - * Handles RCEFragment results and passes them along - */ - override fun onResult(activityResult: Int, data: Intent?) { - val htmlResult = data?.getStringExtra(HTML_RESULT) - if (activityResult == Activity.RESULT_OK && htmlResult != null) { - EventBus.getDefault().postSticky(AssignmentDescriptionEvent(htmlResult)) - super.onBackPressed() - } else { - super.onBackPressed() - } - } - companion object { fun createIntent(context: Context, route: Route): Intent { val intent = Intent(context, FullscreenActivity::class.java) 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 43a6c377de..8642112373 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 @@ -27,6 +27,8 @@ import android.view.View import android.widget.CompoundButton import android.widget.TextView import android.widget.Toast +import androidx.annotation.IdRes +import androidx.annotation.PluralsRes import androidx.appcompat.app.AlertDialog import androidx.appcompat.widget.Toolbar import androidx.core.content.ContextCompat @@ -293,8 +295,6 @@ class InitActivity : BasePresenterActivity 0) { - // Update content description - if (bottomBar.menu.size() > 1) { - val title = String.format(Locale.getDefault(), getString(R.string.todoUnreadCount), todoCount) - MenuItemCompat.setContentDescription(bottomBar.menu.getItem(1), if (selectedTab == TODO_TAB) "${getString(R.string.selected)} $title" else title) - } + updateBottomBarBadge(R.id.tab_todo, todoCount) + } - val todoCountDisplay = if (todoCount > 99) getString(R.string.max_count) else todoCount.toString() - - // First child is the imageView that we use for the bottom bar, second is a layout for the label - (view.getChildAt(2) as? TextView)?.let { - it.text = todoCountDisplay - } ?: run { - // No badge, we need to create one - val badge = LayoutInflater.from(this).inflate(R.layout.unread_count, bottomBar, false) as TextView - badge.text = todoCountDisplay - ColorUtils.colorIt(getColorCompat(R.color.textInfo), badge.background) - view.addView(badge) + override fun updateInboxUnreadCount(unreadCount: Int) { + updateBottomBarBadge(R.id.tab_inbox, unreadCount, R.plurals.a11y_inboxUnreadCount) + } + + private fun updateBottomBarBadge(@IdRes menuItemId: Int, count: Int, @PluralsRes quantityContentDescription: Int? = null) { + if (count > 0) { + bottomBar.getOrCreateBadge(menuItemId).number = count + bottomBar.getOrCreateBadge(menuItemId).backgroundColor = getColor(R.color.backgroundInfo) + bottomBar.getOrCreateBadge(menuItemId).badgeTextColor = getColor(R.color.white) + if (quantityContentDescription != null) { + bottomBar.getOrCreateBadge(menuItemId).setContentDescriptionQuantityStringsResource(quantityContentDescription) } } else { // Don't set the badge or display it, remove any badge - (view.getChildAt(2) as? TextView)?.let { view.removeView(it) } - - // Update content description - if (bottomBar.menu.size() > 1) { - MenuItemCompat.setContentDescription(bottomBar.menu.getItem(1), if (selectedTab == TODO_TAB) "${getString(R.string.selected)} ${getString(R.string.tab_todo)}" else getString(R.string.tab_todo)) - } + bottomBar.removeBadge(menuItemId) } } diff --git a/apps/teacher/src/main/java/com/instructure/teacher/activities/MasterDetailActivity.kt b/apps/teacher/src/main/java/com/instructure/teacher/activities/MasterDetailActivity.kt index 854e02aaf9..5353ea353d 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/activities/MasterDetailActivity.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/activities/MasterDetailActivity.kt @@ -42,9 +42,11 @@ import com.instructure.interactions.Identity import com.instructure.interactions.router.Route import com.instructure.teacher.router.RouteMatcher import com.instructure.teacher.router.RouteResolver +import dagger.hilt.android.AndroidEntryPoint import kotlinx.android.synthetic.main.activity_master_detail.* import retrofit2.Response +@AndroidEntryPoint class MasterDetailActivity : BaseAppCompatActivity(), MasterDetailInteractions { private var mRoute: Route? = null diff --git a/apps/teacher/src/main/java/com/instructure/teacher/di/DashboardModule.kt b/apps/teacher/src/main/java/com/instructure/teacher/di/DashboardModule.kt index ea60425fa0..1516dc4f00 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/di/DashboardModule.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/di/DashboardModule.kt @@ -16,6 +16,7 @@ package com.instructure.teacher.di +import androidx.fragment.app.FragmentActivity import com.instructure.pandautils.features.dashboard.notifications.DashboardRouter import com.instructure.teacher.features.dashboard.notifications.TeacherDashboardRouter import dagger.Module @@ -28,7 +29,7 @@ import dagger.hilt.android.components.FragmentComponent class DashboardModule { @Provides - fun provideHomeroomRouter(): DashboardRouter { - return TeacherDashboardRouter() + fun provideHomeroomRouter(activity: FragmentActivity): DashboardRouter { + return TeacherDashboardRouter(activity) } } \ No newline at end of file diff --git a/apps/teacher/src/main/java/com/instructure/teacher/features/dashboard/notifications/TeacherDashboardRouter.kt b/apps/teacher/src/main/java/com/instructure/teacher/features/dashboard/notifications/TeacherDashboardRouter.kt index 413a9b30b3..3dfef9a1b1 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/features/dashboard/notifications/TeacherDashboardRouter.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/features/dashboard/notifications/TeacherDashboardRouter.kt @@ -16,8 +16,20 @@ package com.instructure.teacher.features.dashboard.notifications +import androidx.fragment.app.FragmentActivity +import com.instructure.interactions.router.Route import com.instructure.pandautils.features.dashboard.notifications.DashboardRouter +import com.instructure.teacher.fragments.InternalWebViewFragment +import com.instructure.teacher.router.RouteMatcher -class TeacherDashboardRouter : DashboardRouter { - override fun routeToGlobalAnnouncement(subject: String, message: String) = Unit +class TeacherDashboardRouter(private val activity: FragmentActivity) : DashboardRouter { + override fun routeToGlobalAnnouncement(subject: String, message: String) { + val args = InternalWebViewFragment.makeBundle( + url ="", + title = subject, + html = message + ) + val route = Route(null, InternalWebViewFragment::class.java, null, args) + RouteMatcher.route(activity, route) + } } \ No newline at end of file diff --git a/apps/teacher/src/main/java/com/instructure/teacher/features/syllabus/edit/EditSyllabusView.kt b/apps/teacher/src/main/java/com/instructure/teacher/features/syllabus/edit/EditSyllabusView.kt index e64b2273cd..4474faaea1 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/features/syllabus/edit/EditSyllabusView.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/features/syllabus/edit/EditSyllabusView.kt @@ -120,7 +120,16 @@ class EditSyllabusView(val fragmentManager: FragmentManager, inflater: LayoutInf } fun uploadRceImage(imageUri: Uri, activity: Activity, course: Course) { - MediaUploadUtils.uploadRceImageJob(imageUri, course, activity) { text, alt -> contentRCEView?.insertImage(text, alt) } + MediaUploadUtils.uploadRceImageJob(imageUri, course, activity) { imageUrl -> insertImage(imageUrl) } + } + + private fun insertImage(imageUrl: String) { + val activity = context as? Activity + if (activity != null) { + contentRCEView?.insertImage(activity, imageUrl) + } else { + contentRCEView?.insertImage(imageUrl, "") + } } fun closeEditSyllabus() { diff --git a/apps/teacher/src/main/java/com/instructure/teacher/fragments/CoursesFragment.kt b/apps/teacher/src/main/java/com/instructure/teacher/fragments/CoursesFragment.kt index 290cd18955..b8a59cdf9c 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/fragments/CoursesFragment.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/fragments/CoursesFragment.kt @@ -25,6 +25,7 @@ import com.instructure.canvasapi2.utils.APIHelper import com.instructure.canvasapi2.utils.ApiPrefs import com.instructure.pandautils.analytics.SCREEN_VIEW_DASHBOARD import com.instructure.pandautils.analytics.ScreenView +import com.instructure.pandautils.features.dashboard.notifications.DashboardNotificationsFragment import com.instructure.pandautils.fragments.BaseSyncFragment import com.instructure.pandautils.utils.* import com.instructure.teacher.R @@ -39,6 +40,7 @@ import com.instructure.teacher.holders.CoursesViewHolder import com.instructure.teacher.presenters.CoursesPresenter import com.instructure.teacher.utils.RecyclerViewUtils import com.instructure.teacher.utils.TeacherPrefs +import com.instructure.teacher.utils.setupBackButtonAsBackPressedOnly import com.instructure.teacher.utils.setupMenu import com.instructure.teacher.viewinterface.CoursesView import kotlinx.android.synthetic.main.fragment_courses.* @@ -150,7 +152,15 @@ class CoursesFragment : BaseSyncFragment(), DiscussionListView { - @Inject - lateinit var featureFlagProvider: FeatureFlagProvider + val featureFlagProvider: FeatureFlagProvider = FeatureFlagProvider(UserManager, RemoteConfigUtils, ApiPrefs) protected var mCanvasContext: CanvasContext by ParcelableArg(default = CanvasContext.getGenericContext(CanvasContext.Type.COURSE, -1L, "")) diff --git a/apps/teacher/src/main/java/com/instructure/teacher/fragments/DiscussionsReplyFragment.kt b/apps/teacher/src/main/java/com/instructure/teacher/fragments/DiscussionsReplyFragment.kt index 91513d62db..494c3f4fc8 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/fragments/DiscussionsReplyFragment.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/fragments/DiscussionsReplyFragment.kt @@ -164,7 +164,7 @@ class DiscussionsReplyFragment : BasePresenterFragment Unit = { item -> when (item.itemId) { diff --git a/apps/teacher/src/main/java/com/instructure/teacher/fragments/EditAssignmentDetailsFragment.kt b/apps/teacher/src/main/java/com/instructure/teacher/fragments/EditAssignmentDetailsFragment.kt index 3465f6fc07..037e694b31 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/fragments/EditAssignmentDetailsFragment.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/fragments/EditAssignmentDetailsFragment.kt @@ -171,7 +171,7 @@ class EditAssignmentDetailsFragment : BaseFragment() { RequestCodes.CAMERA_PIC_REQUEST -> MediaUploadUtils.handleCameraPicResult(requireActivity(), null) else -> null }?.let { imageUri -> - rceImageUploadJob = MediaUploadUtils.uploadRceImageJob(imageUri, mCourse, requireActivity()) { text, alt -> descriptionEditor.insertImage(text, alt) } + rceImageUploadJob = MediaUploadUtils.uploadRceImageJob(imageUri, mCourse, requireActivity()) { imageUrl -> descriptionEditor.insertImage(requireActivity(), imageUrl) } } } } diff --git a/apps/teacher/src/main/java/com/instructure/teacher/fragments/EditFileFolderFragment.kt b/apps/teacher/src/main/java/com/instructure/teacher/fragments/EditFileFolderFragment.kt index eb757b190c..5a8eb1676f 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/fragments/EditFileFolderFragment.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/fragments/EditFileFolderFragment.kt @@ -118,7 +118,7 @@ class EditFileFolderFragment : BasePresenterFragment< override fun onRefreshStarted() = Unit override fun folderDeleted(deletedFileFolder: FileFolder) { - FileFolderDeletedEvent(deletedFileFolder).post() + FileFolderDeletedEvent(deletedFileFolder).postSticky() requireActivity().onBackPressed() } diff --git a/apps/teacher/src/main/java/com/instructure/teacher/fragments/EditQuizDetailsFragment.kt b/apps/teacher/src/main/java/com/instructure/teacher/fragments/EditQuizDetailsFragment.kt index 5949bb1352..b4475c9761 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/fragments/EditQuizDetailsFragment.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/fragments/EditQuizDetailsFragment.kt @@ -16,6 +16,8 @@ */ package com.instructure.teacher.fragments +import android.app.Activity +import android.content.Intent import android.graphics.Typeface import android.os.Bundle import android.os.Handler @@ -343,10 +345,25 @@ class EditQuizDetailsFragment : BasePresenterFragment< // When the RCE editor has focus we want the label to be darker so it matches the title's functionality descriptionWebView.setLabel(quizDescLabel, R.color.textDarkest, R.color.textDark) + descriptionWebView.actionUploadImageCallback = { MediaUploadUtils.showPickImageDialog(this) } + // Dismiss the progress bar descriptionProgressBar.setGone() } + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + if (resultCode == Activity.RESULT_OK) { + // Get the image Uri + when (requestCode) { + RequestCodes.PICK_IMAGE_GALLERY -> data?.data + RequestCodes.CAMERA_PIC_REQUEST -> MediaUploadUtils.handleCameraPicResult(requireActivity(), null) + else -> null + }?.let { imageUri -> + MediaUploadUtils.uploadRceImageJob(imageUri, mCourse, requireActivity()) { imageUrl -> descriptionWebView.insertImage(requireActivity(), imageUrl) } + } + } + } + override fun setupOverrides() { overrideContainer.removeAllViews() // Load in overrides diff --git a/apps/teacher/src/main/java/com/instructure/teacher/fragments/FileListFragment.kt b/apps/teacher/src/main/java/com/instructure/teacher/fragments/FileListFragment.kt index 36df5c9006..dc05caab89 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/fragments/FileListFragment.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/fragments/FileListFragment.kt @@ -220,6 +220,10 @@ class FileListFragment : BaseSyncFragment< } override fun folderCreationError() = toast(R.string.folderCreationError) + override fun folderCreationSuccess() { + checkIfEmpty() + } + private fun setupViews() { ViewStyler.themeFAB(addFab, ThemePrefs.buttonColor) ViewStyler.themeFAB(addFileFab, ThemePrefs.buttonColor) diff --git a/apps/teacher/src/main/java/com/instructure/teacher/fragments/InboxFragment.kt b/apps/teacher/src/main/java/com/instructure/teacher/fragments/InboxFragment.kt index 570953ca6c..966e89aa3f 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/fragments/InboxFragment.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/fragments/InboxFragment.kt @@ -45,6 +45,7 @@ import com.instructure.teacher.presenters.InboxPresenter import com.instructure.teacher.router.RouteMatcher import com.instructure.teacher.utils.RecyclerViewUtils import com.instructure.teacher.utils.getColorCompat +import com.instructure.teacher.utils.setupBackButtonAsBackPressedOnly import com.instructure.teacher.utils.setupMenu import com.instructure.teacher.viewinterface.InboxView import kotlinx.android.synthetic.main.fragment_inbox.* @@ -157,7 +158,14 @@ class InboxFragment : BaseSyncFragment Unit = { item -> when (item.itemId) { R.id.inboxFilter -> { diff --git a/apps/teacher/src/main/java/com/instructure/teacher/fragments/InternalWebViewFragment.kt b/apps/teacher/src/main/java/com/instructure/teacher/fragments/InternalWebViewFragment.kt index 618899fb58..2bf9c269dc 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/fragments/InternalWebViewFragment.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/fragments/InternalWebViewFragment.kt @@ -189,7 +189,7 @@ open class InternalWebViewFragment : BaseFragment() { fun loadUrl(targetUrl: String) { if (html.isNotEmpty()) { - loadHtml(html) + canvasWebView?.loadHtml(html, title) return } @@ -261,6 +261,7 @@ open class InternalWebViewFragment : BaseFragment() { fun newInstance(args: Bundle) = InternalWebViewFragment().apply { url = args.getString(URL)!! title = args.getString(TITLE)!! + html = args.getString(HTML) ?: "" darkToolbar = args.getBoolean(DARK_TOOLBAR) } diff --git a/apps/teacher/src/main/java/com/instructure/teacher/fragments/SpeedGraderGradeFragment.kt b/apps/teacher/src/main/java/com/instructure/teacher/fragments/SpeedGraderGradeFragment.kt index 57ae91ccd7..1fa910f75f 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/fragments/SpeedGraderGradeFragment.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/fragments/SpeedGraderGradeFragment.kt @@ -217,7 +217,11 @@ class SpeedGraderGradeFragment : BasePresenterFragment viewCallback?.insertImageIntoRCE(text, alt) } + rceImageUploadJob = MediaUploadUtils.uploadRceImageJob(imageUri, canvasContext, activity) { imageUrl -> viewCallback?.insertImageIntoRCE(imageUrl) } } } diff --git a/apps/teacher/src/main/java/com/instructure/teacher/presenters/CreateOrEditAnnouncementPresenter.kt b/apps/teacher/src/main/java/com/instructure/teacher/presenters/CreateOrEditAnnouncementPresenter.kt index 566ee30757..e24243f73d 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/presenters/CreateOrEditAnnouncementPresenter.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/presenters/CreateOrEditAnnouncementPresenter.kt @@ -40,10 +40,8 @@ import com.instructure.teacher.interfaces.RceMediaUploadPresenter import com.instructure.teacher.viewinterface.CreateOrEditAnnouncementView import instructure.androidblueprint.FragmentPresenter import kotlinx.coroutines.Job -import okhttp3.MediaType import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.MultipartBody -import okhttp3.RequestBody import okhttp3.RequestBody.Companion.asRequestBody import java.io.File @@ -158,7 +156,7 @@ class CreateOrEditAnnouncementPresenter( } override fun uploadRceImage(imageUri: Uri, activity: Activity) { - rceImageUploadJob = MediaUploadUtils.uploadRceImageJob(imageUri, canvasContext, activity) { text, alt -> viewCallback?.insertImageIntoRCE(text, alt) } + rceImageUploadJob = MediaUploadUtils.uploadRceImageJob(imageUri, canvasContext, activity) { imageUrl -> viewCallback?.insertImageIntoRCE(imageUrl) } } diff --git a/apps/teacher/src/main/java/com/instructure/teacher/presenters/CreateOrEditPagePresenter.kt b/apps/teacher/src/main/java/com/instructure/teacher/presenters/CreateOrEditPagePresenter.kt index 157509fcf6..763e4acd09 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/presenters/CreateOrEditPagePresenter.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/presenters/CreateOrEditPagePresenter.kt @@ -94,7 +94,7 @@ class CreateOrEditPagePresenter(private val canvasContext: CanvasContext, mPage: } override fun uploadRceImage(imageUri: Uri, activity: Activity) { - rceImageUploadJob = MediaUploadUtils.uploadRceImageJob(imageUri, canvasContext , activity) { text, alt -> viewCallback?.insertImageIntoRCE(text, alt) } + rceImageUploadJob = MediaUploadUtils.uploadRceImageJob(imageUri, canvasContext , activity) { imageUrl -> viewCallback?.insertImageIntoRCE(imageUrl) } } override fun onDestroyed() { diff --git a/apps/teacher/src/main/java/com/instructure/teacher/presenters/DiscussionsReplyPresenter.kt b/apps/teacher/src/main/java/com/instructure/teacher/presenters/DiscussionsReplyPresenter.kt index c7b2502a63..6204d83421 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/presenters/DiscussionsReplyPresenter.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/presenters/DiscussionsReplyPresenter.kt @@ -28,7 +28,6 @@ import com.instructure.canvasapi2.utils.weave.tryWeave import com.instructure.pandautils.discussions.DiscussionCaching import com.instructure.canvasapi2.models.postmodels.FileSubmitObject import com.instructure.pandautils.utils.MediaUploadUtils -import com.instructure.pandautils.utils.ProfileUtils import com.instructure.teacher.interfaces.RceMediaUploadPresenter import com.instructure.teacher.viewinterface.DiscussionsReplyView import instructure.androidblueprint.FragmentPresenter @@ -92,7 +91,7 @@ class DiscussionsReplyPresenter( fun getAttachment(): FileSubmitObject? = attachment override fun uploadRceImage(imageUri: Uri, activity: Activity) { - rceImageUploadJob = MediaUploadUtils.uploadRceImageJob(imageUri, canvasContext, activity) { text, alt -> viewCallback?.insertImageIntoRCE(text, alt) } + rceImageUploadJob = MediaUploadUtils.uploadRceImageJob(imageUri, canvasContext, activity) { imageUrl -> viewCallback?.insertImageIntoRCE(imageUrl) } } companion object { diff --git a/apps/teacher/src/main/java/com/instructure/teacher/presenters/DiscussionsUpdatePresenter.kt b/apps/teacher/src/main/java/com/instructure/teacher/presenters/DiscussionsUpdatePresenter.kt index 7faffe885a..2887c51c20 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/presenters/DiscussionsUpdatePresenter.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/presenters/DiscussionsUpdatePresenter.kt @@ -85,7 +85,7 @@ class DiscussionsUpdatePresenter( } override fun uploadRceImage(imageUri: Uri, activity: Activity) { - MediaUploadUtils.uploadRceImageJob(imageUri, canvasContext, activity) { text, alt -> viewCallback?.insertImageIntoRCE(text, alt) } + MediaUploadUtils.uploadRceImageJob(imageUri, canvasContext, activity) { imageUrl -> viewCallback?.insertImageIntoRCE(imageUrl) } } diff --git a/apps/teacher/src/main/java/com/instructure/teacher/presenters/FileListPresenter.kt b/apps/teacher/src/main/java/com/instructure/teacher/presenters/FileListPresenter.kt index 907fb475d1..db83b17a9f 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/presenters/FileListPresenter.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/presenters/FileListPresenter.kt @@ -26,6 +26,7 @@ import com.instructure.canvasapi2.utils.weave.awaitApis import com.instructure.canvasapi2.utils.weave.catch import com.instructure.canvasapi2.utils.weave.tryWeave import com.instructure.teacher.viewinterface.FileListView +import instructure.androidblueprint.ListChangeCallback import instructure.androidblueprint.SyncPresenter import kotlinx.coroutines.Job @@ -80,6 +81,7 @@ class FileListPresenter(var currentFolder: FileFolder, val mCanvasContext: Canva createFolderCall = tryWeave { val newFolder = awaitApi { FileFolderManager.createFolder(currentFolder.id, CreateFolder(folderName), it) } data.addOrUpdate(newFolder) + viewCallback?.folderCreationSuccess() } catch { viewCallback?.folderCreationError() } diff --git a/apps/teacher/src/main/java/com/instructure/teacher/presenters/InboxPresenter.kt b/apps/teacher/src/main/java/com/instructure/teacher/presenters/InboxPresenter.kt index 40c497b02c..d4f6db34c0 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/presenters/InboxPresenter.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/presenters/InboxPresenter.kt @@ -17,9 +17,14 @@ package com.instructure.teacher.presenters import com.instructure.canvasapi2.apis.InboxApi import com.instructure.canvasapi2.managers.InboxManager +import com.instructure.canvasapi2.managers.UnreadCountManager import com.instructure.canvasapi2.models.CanvasContext import com.instructure.canvasapi2.models.Conversation +import com.instructure.canvasapi2.models.UnreadConversationCount import com.instructure.canvasapi2.utils.weave.WeaveJob +import com.instructure.canvasapi2.utils.weave.awaitApi +import com.instructure.canvasapi2.utils.weave.catch +import com.instructure.canvasapi2.utils.weave.tryWeave import com.instructure.canvasapi2.utils.weave.weavePaginated import com.instructure.teacher.viewinterface.InboxView import instructure.androidblueprint.SyncPresenter @@ -34,9 +39,14 @@ class InboxPresenter : SyncPresenter(Conversation::clas var canvasContext: CanvasContext? = null var apiCall: WeaveJob? = null + var unreadCountCall: WeaveJob? = null + + private var isLoading = false override fun loadData(forceNetwork: Boolean) { - if (data.size() > 0 && !forceNetwork) return + if ((data.size() > 0 || isLoading) && !forceNetwork) return + + isLoading = true viewCallback?.onRefreshStarted() apiCall = weavePaginated> { onRequest { callback -> @@ -45,16 +55,26 @@ class InboxPresenter : SyncPresenter(Conversation::clas ?: InboxManager.getConversations(scope, forceNetwork, callback) } onResponse { response -> + isLoading = false data.addOrUpdate(response) viewCallback?.onRefreshFinished() viewCallback?.checkIfEmpty() } - onError { } + onError { + isLoading = false + } } + + unreadCountCall = tryWeave { + val inboxUnreadCount = awaitApi { UnreadCountManager.getUnreadConversationCount(it, true) } + val unreadCountInt = (inboxUnreadCount.unreadCount ?: "0").toInt() + viewCallback?.unreadCountUpdated(unreadCountInt) + } catch {} } override fun refresh(forceNetwork: Boolean) { apiCall?.cancel() + unreadCountCall?.cancel() clearData() loadData(forceNetwork) } diff --git a/apps/teacher/src/main/java/com/instructure/teacher/presenters/InitActivityPresenter.kt b/apps/teacher/src/main/java/com/instructure/teacher/presenters/InitActivityPresenter.kt index 8f33ad1e5b..dbd890c8df 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/presenters/InitActivityPresenter.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/presenters/InitActivityPresenter.kt @@ -19,9 +19,11 @@ package com.instructure.teacher.presenters import com.instructure.canvasapi2.CanvasRestAdapter import com.instructure.canvasapi2.managers.LaunchDefinitionsManager import com.instructure.canvasapi2.managers.ToDoManager +import com.instructure.canvasapi2.managers.UnreadCountManager import com.instructure.canvasapi2.managers.UserManager import com.instructure.canvasapi2.models.LaunchDefinition import com.instructure.canvasapi2.models.ToDo +import com.instructure.canvasapi2.models.UnreadConversationCount import com.instructure.canvasapi2.utils.weave.WeaveJob import com.instructure.canvasapi2.utils.weave.awaitApi import com.instructure.canvasapi2.utils.weave.catch @@ -65,6 +67,9 @@ class InitActivityPresenter : Presenter { view?.gotLaunchDefinitions(definitions) } + val inboxUnreadCount = awaitApi { UnreadCountManager.getUnreadConversationCount(it, true) } + val unreadCountInt = (inboxUnreadCount.unreadCount ?: "0").toInt() + view?.updateInboxUnreadCount(unreadCountInt) } catch { it.printStackTrace() } 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 2912c9bd96..258e817962 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 @@ -51,7 +51,6 @@ import com.instructure.teacher.features.postpolicies.ui.PostPolicyFragment import com.instructure.teacher.features.syllabus.ui.SyllabusFragment import com.instructure.teacher.fragments.* import com.instructure.teacher.fragments.FileListFragment -import instructure.rceditor.RCEFragment import java.util.Locale object RouteMatcher : BaseRouteMatcher() { @@ -128,7 +127,6 @@ object RouteMatcher : BaseRouteMatcher() { bottomSheetFragments.add(AssigneeListFragment::class.java) bottomSheetFragments.add(EditFavoritesFragment::class.java) bottomSheetFragments.add(CourseSettingsFragment::class.java) - bottomSheetFragments.add(RCEFragment::class.java) bottomSheetFragments.add(EditQuizDetailsFragment::class.java) bottomSheetFragments.add(QuizPreviewWebviewFragment::class.java) bottomSheetFragments.add(AddMessageFragment::class.java) @@ -337,7 +335,6 @@ object RouteMatcher : BaseRouteMatcher() { CourseSettingsFragment::class.java.isAssignableFrom(cls) -> fragment = CourseSettingsFragment.newInstance((canvasContext as Course?)!!) QuizListFragment::class.java.isAssignableFrom(cls) -> fragment = QuizListFragment.newInstance(canvasContext!!) QuizDetailsFragment::class.java.isAssignableFrom(cls) -> fragment = getQuizDetailsFragment(canvasContext, route) - RCEFragment::class.java.isAssignableFrom(cls) -> fragment = RCEFragment.newInstance(route.arguments) EditQuizDetailsFragment::class.java.isAssignableFrom(cls) -> fragment = EditQuizDetailsFragment.newInstance((canvasContext as Course?)!!, route.arguments) QuizPreviewWebviewFragment::class.java.isAssignableFrom(cls) -> fragment = QuizPreviewWebviewFragment.newInstance(route.arguments) EditQuizDetailsFragment::class.java.isAssignableFrom(cls) -> fragment = EditQuizDetailsFragment.newInstance((canvasContext as Course?)!!, route.arguments) 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 d5399f9049..06d72bd9c5 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 @@ -19,7 +19,6 @@ import com.instructure.teacher.features.postpolicies.ui.PostPolicyFragment import com.instructure.teacher.features.syllabus.edit.EditSyllabusFragment import com.instructure.teacher.features.syllabus.ui.SyllabusFragment import com.instructure.teacher.fragments.* -import instructure.rceditor.RCEFragment object RouteResolver { @@ -96,8 +95,6 @@ object RouteResolver { fragment = getModuleListFragment(canvasContext, route) } else if (QuizDetailsFragment::class.java.isAssignableFrom(cls)) { fragment = getQuizDetailsFragment(canvasContext, route) - } else if (RCEFragment::class.java.isAssignableFrom(cls)) { - fragment = RCEFragment.newInstance(route.arguments) } else if (EditQuizDetailsFragment::class.java.isAssignableFrom(cls)) { fragment = EditQuizDetailsFragment.newInstance((canvasContext as Course?)!!, route.arguments) } else if (QuizPreviewWebviewFragment::class.java.isAssignableFrom(cls)) { diff --git a/apps/teacher/src/main/java/com/instructure/teacher/viewinterface/FileListView.kt b/apps/teacher/src/main/java/com/instructure/teacher/viewinterface/FileListView.kt index 1030de974a..e2342334ee 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/viewinterface/FileListView.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/viewinterface/FileListView.kt @@ -21,4 +21,5 @@ import instructure.androidblueprint.SyncManager interface FileListView : SyncManager { fun folderCreationError() + fun folderCreationSuccess() } \ No newline at end of file diff --git a/apps/teacher/src/main/java/com/instructure/teacher/viewinterface/InboxView.kt b/apps/teacher/src/main/java/com/instructure/teacher/viewinterface/InboxView.kt index 8e8060878c..ac3ab549d2 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/viewinterface/InboxView.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/viewinterface/InboxView.kt @@ -18,4 +18,6 @@ package com.instructure.teacher.viewinterface import com.instructure.canvasapi2.models.Conversation import instructure.androidblueprint.SyncManager -interface InboxView : SyncManager +interface InboxView : SyncManager { + fun unreadCountUpdated(unreadCount: Int) +} diff --git a/apps/teacher/src/main/java/com/instructure/teacher/viewinterface/InitActivityView.kt b/apps/teacher/src/main/java/com/instructure/teacher/viewinterface/InitActivityView.kt index 68799bcbf8..8988f6c216 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/viewinterface/InitActivityView.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/viewinterface/InitActivityView.kt @@ -22,4 +22,5 @@ interface InitActivityView { fun updateTodoCount(todoCount: Int) fun gotLaunchDefinitions(launchDefinitions: List?) fun updateColorOverlaySwitch(isChecked: Boolean, isFailed: Boolean) + fun updateInboxUnreadCount(unreadCount: Int) } diff --git a/apps/teacher/src/main/res/layout-sw760dp/view_floating_media_recorder_video.xml b/apps/teacher/src/main/res/layout-sw760dp/view_floating_media_recorder_video.xml index bfd6809b1a..b1aa7337a6 100644 --- a/apps/teacher/src/main/res/layout-sw760dp/view_floating_media_recorder_video.xml +++ b/apps/teacher/src/main/res/layout-sw760dp/view_floating_media_recorder_video.xml @@ -16,19 +16,20 @@ --> - + android:keepScreenOn="true" + app:cameraFacing="front" + app:cameraMode="video" /> - - diff --git a/apps/teacher/src/main/res/values-de/strings.xml b/apps/teacher/src/main/res/values-de/strings.xml index 9c370dd4c8..a80b3cab5c 100644 --- a/apps/teacher/src/main/res/values-de/strings.xml +++ b/apps/teacher/src/main/res/values-de/strings.xml @@ -690,7 +690,7 @@ Neue Diskussion Optionen Anmelden - Gelistete Antworten zulassen + Antworten aller Diskussionsstränge zulassen Benutzer müssen gepostet haben, bevor sie Antworten sehen können. Benutzern Kommentare erlauben Ein Diskussionstitel muss festgelegt werden. diff --git a/apps/teacher/src/main/res/values/strings.xml b/apps/teacher/src/main/res/values/strings.xml index 0039c33bd6..69de0f07e9 100644 --- a/apps/teacher/src/main/res/values/strings.xml +++ b/apps/teacher/src/main/res/values/strings.xml @@ -875,7 +875,6 @@ -%s point -%s points - To do %d unread Profile Settings Downloading file… File downloaded successfully. diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/MockCanvas.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/MockCanvas.kt index 177258bc1a..0a6671f9fc 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/MockCanvas.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/MockCanvas.kt @@ -19,7 +19,6 @@ package com.instructure.canvas.espresso.mockCanvas import android.util.Log -import com.github.javafaker.Bool import com.github.javafaker.Faker import com.instructure.canvas.espresso.mockCanvas.utils.Randomizer import com.instructure.canvasapi2.apis.EnrollmentAPI @@ -1360,9 +1359,9 @@ fun MockCanvas.addDiscussionTopicToCourse( var topicHeader = prePopulatedTopicHeader if(topicHeader == null) { topicHeader = DiscussionTopicHeader( - title = topicTitle, - discussionType = "side_comment", - message = topicDescription + title = topicTitle, + discussionType = "side_comment", + message = topicDescription ) } @@ -1374,7 +1373,7 @@ fun MockCanvas.addDiscussionTopicToCourse( topicHeader.id = newItemId() topicHeader.postedDate = Calendar.getInstance().time if(attachment != null) { - topicHeader.attachments = mutableListOf(attachment) + topicHeader.attachments = mutableListOf(attachment) } topicHeader.announcement = isAnnouncement topicHeader.sections = sections @@ -1383,7 +1382,7 @@ fun MockCanvas.addDiscussionTopicToCourse( var topicHeaderList = if(groupId != null) groupDiscussionTopicHeaders[groupId] else courseDiscussionTopicHeaders[course.id] if(topicHeaderList == null) { - topicHeaderList = mutableListOf() + topicHeaderList = mutableListOf() if(groupId != null) { groupDiscussionTopicHeaders[groupId] = topicHeaderList } @@ -1395,9 +1394,9 @@ fun MockCanvas.addDiscussionTopicToCourse( topicHeaderList.add(topicHeader) val topic = DiscussionTopic( - participants = mutableListOf( - DiscussionParticipant(id = user.id, displayName = user.name) - ) + participants = mutableListOf( + DiscussionParticipant(id = user.id, displayName = user.name) + ) ) discussionTopics[topicHeader.id] = topic @@ -1771,6 +1770,8 @@ fun MockCanvas.addGroupToCourse( isFavorite = isFavorite ) + result.permissions = CanvasContextPermission(canCreateAnnouncement = true) + groups[result.id] = result return result diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/endpoints/ApiEndpoint.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/endpoints/ApiEndpoint.kt index 349196d55c..62d81e572c 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/endpoints/ApiEndpoint.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/endpoints/ApiEndpoint.kt @@ -17,7 +17,9 @@ package com.instructure.canvas.espresso.mockCanvas.endpoints import android.util.Log +import com.google.gson.Gson import com.instructure.canvas.espresso.mockCanvas.Endpoint +import com.instructure.canvas.espresso.mockCanvas.addDiscussionTopicToCourse import com.instructure.canvas.espresso.mockCanvas.endpoint import com.instructure.canvas.espresso.mockCanvas.utils.* import com.instructure.canvasapi2.models.* @@ -218,6 +220,30 @@ object GroupsEndpoint : Endpoint ( request.unauthorizedResponse() } } + POST { + val jsonObject = grabJsonFromMultiPartBody(request.body!!) + var newHeader = Gson().fromJson(jsonObject, DiscussionTopicHeader::class.java) + var group = data.groups.values.find { it.id == pathVars.groupId } + var course = data.courses.values.find {it.id == group!!.courseId} + var user = request.user!! + + newHeader = data.addDiscussionTopicToCourse( + course = course!!, + groupId = group!!.id, + user = user, + prePopulatedTopicHeader = newHeader, + topicTitle = newHeader.title!!, + topicDescription = newHeader.message!!, + allowRating = data.discussionRatingsEnabled, + allowReplies = data.discussionRepliesEnabled, + allowAttachments = data.discussionAttachmentsEnabled, + isAnnouncement = newHeader.announcement + ) + Log.d("<--", "new discussion topic request body: $jsonObject") + Log.d("<--", "new header: $newHeader") + + request.successResponse(newHeader) + } } ), diff --git a/automation/espresso/src/main/kotlin/com/instructure/espresso/CustomViewAssertions.kt b/automation/espresso/src/main/kotlin/com/instructure/espresso/CustomViewAssertions.kt index d063d0c46f..a6248e7e21 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/espresso/CustomViewAssertions.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/espresso/CustomViewAssertions.kt @@ -16,14 +16,18 @@ */ package com.instructure.espresso +import android.graphics.Color +import android.view.View +import android.widget.TextView +import androidx.annotation.IdRes +import androidx.recyclerview.widget.RecyclerView import androidx.test.espresso.NoMatchingViewException import androidx.test.espresso.ViewAssertion import androidx.test.espresso.matcher.ViewMatchers import androidx.viewpager.widget.ViewPager -import androidx.recyclerview.widget.RecyclerView -import android.view.View +import com.google.android.material.bottomnavigation.BottomNavigationView import org.hamcrest.Matchers -import java.lang.ClassCastException +import org.junit.Assert.assertEquals class RecyclerViewItemCountAssertion(private val expectedCount: Int) : ViewAssertion { override fun check(view: View, noViewFoundException: NoMatchingViewException?) { @@ -51,3 +55,22 @@ class ViewPagerItemCountAssertion(private val expectedCount: Int) : ViewAssertio ViewMatchers.assertThat(count, Matchers.`is`(expectedCount)) } } + +class TextViewColorAssertion(private val colorHexCode: String) : ViewAssertion { + override fun check(view: View, noViewFoundException: NoMatchingViewException?) { + noViewFoundException?.let { throw it } + val item = (view as? TextView) + ?: throw ClassCastException("View of type ${view.javaClass.simpleName} must be a TextView") + assertEquals(item.currentTextColor, Color.parseColor(colorHexCode)) + } +} + +class NotificationBadgeAssertion(@IdRes private val menuItemId: Int, private val expectedCount: Int) : ViewAssertion { + override fun check(view: View, noViewFoundException: NoMatchingViewException?) { + noViewFoundException?.let { throw it } + val bottomNavigationView = (view as? BottomNavigationView) + ?: throw ClassCastException("View of type ${view.javaClass.simpleName} must be a BottomNavigationView") + val badgeCount = bottomNavigationView.getBadge(menuItemId)?.number ?: -1 + assertEquals(badgeCount, expectedCount) + } +} diff --git a/buildSrc/src/main/java/GlobalDependencies.kt b/buildSrc/src/main/java/GlobalDependencies.kt index 1116329649..d2d6159123 100644 --- a/buildSrc/src/main/java/GlobalDependencies.kt +++ b/buildSrc/src/main/java/GlobalDependencies.kt @@ -154,7 +154,7 @@ object Libs { const val PAPERDB = "io.github.pilgr:paperdb:2.7.1" const val KEYBOARD_VISIBILITY_LISTENER = "net.yslibrary.keyboardvisibilityevent:keyboardvisibilityevent:2.2.1" const val APACHE_COMMONS_TEXT = "org.apache.commons:commons-text:1.6" - const val WONDERKILN_CAMERA_KIT = "com.github.CameraKit:camerakit-android:v0.13.4" + const val CAMERA_VIEW = "com.otaliastudios:cameraview:2.7.2" } object Plugins { diff --git a/libs/annotations/src/main/java/com/instructure/annotations/PdfSubmissionView.kt b/libs/annotations/src/main/java/com/instructure/annotations/PdfSubmissionView.kt index 600246d2af..742a899645 100644 --- a/libs/annotations/src/main/java/com/instructure/annotations/PdfSubmissionView.kt +++ b/libs/annotations/src/main/java/com/instructure/annotations/PdfSubmissionView.kt @@ -39,7 +39,11 @@ import com.instructure.canvasapi2.models.ApiValues import com.instructure.canvasapi2.models.DocSession import com.instructure.canvasapi2.models.canvadocs.CanvaDocAnnotation import com.instructure.canvasapi2.models.canvadocs.CanvaDocAnnotationResponse -import com.instructure.canvasapi2.utils.* +import com.instructure.canvasapi2.utils.APIHelper +import com.instructure.canvasapi2.utils.ApiPrefs +import com.instructure.canvasapi2.utils.extractCanvaDocsDomain +import com.instructure.canvasapi2.utils.extractSessionId +import com.instructure.canvasapi2.utils.isValid import com.instructure.canvasapi2.utils.weave.StatusCallbackError import com.instructure.canvasapi2.utils.weave.awaitApi import com.instructure.canvasapi2.utils.weave.catch @@ -84,7 +88,7 @@ import java.io.File import java.util.* @SuppressLint("ViewConstructor") -abstract class PdfSubmissionView(context: Context) : FrameLayout(context), AnnotationManager.OnAnnotationCreationModeChangeListener, AnnotationManager.OnAnnotationEditingModeChangeListener { +abstract class PdfSubmissionView(context: Context, private val studentAnnotationView: Boolean = false) : FrameLayout(context), AnnotationManager.OnAnnotationCreationModeChangeListener, AnnotationManager.OnAnnotationEditingModeChangeListener { protected lateinit var docSession: DocSession protected lateinit var apiValues: ApiValues @@ -298,6 +302,10 @@ abstract class PdfSubmissionView(context: Context) : FrameLayout(context), Annot protected fun openComments() { // Get current annotation in both forms + if (pdfFragment?.selectedAnnotations?.isNullOrEmpty() == true) { + toast(R.string.noAnnotationSelected) + return + } val currentPdfAnnotation = pdfFragment?.selectedAnnotations?.get(0) val currentAnnotation = currentPdfAnnotation?.convertPDFAnnotationToCanvaDoc(docSession.documentId) // Assuming neither is null, continue @@ -461,14 +469,15 @@ abstract class PdfSubmissionView(context: Context) : FrameLayout(context), Annot } private fun handlePageRotation(pdfDocument: PdfDocument, rotationMap: HashMap) { + // Removing the listener prevents an infinite loop with onDocumentLoaded, which is triggered + // by the calls to setRotationOffset() + pdfFragment?.removeDocumentListener(documentListener) + rotationMap.forEach { pageRotation -> pageRotation.key.toIntOrNull()?.let { pageIndex -> pdfDocument.setRotationOffset(calculateRotationOffset(pdfDocument.getPageRotation(pageIndex), pageRotation.value), pageIndex) } } - // Removing the listener prevents an infinite loop with onDocumentLoaded, which is triggered - // by the calls to setRotationOffset() - pdfFragment?.removeDocumentListener(documentListener) } @Suppress("EXPERIMENTAL_FEATURE_WARNING") @@ -595,7 +604,7 @@ abstract class PdfSubmissionView(context: Context) : FrameLayout(context), Annot setIsCurrentlyAnnotating(true) } - if (annotation.type != AnnotationType.FREETEXT && annotation.name.isValid()) { + if (annotation.type != AnnotationType.FREETEXT && annotation.name.isValid() && (!studentAnnotationView || hasComments(annotation))) { // if the annotation is an existing annotation (has an ID) and is NOT freetext // we want to display the button to view/make comments commentsButton.setVisible() @@ -605,6 +614,13 @@ abstract class PdfSubmissionView(context: Context) : FrameLayout(context), Annot } } + private fun hasComments(annotation: Annotation): Boolean { + val currentAnnotation = annotation.convertPDFAnnotationToCanvaDoc(docSession.documentId) + return currentAnnotation != null + && commentRepliesHashMap[currentAnnotation.annotationId] != null + && commentRepliesHashMap[currentAnnotation.annotationId]?.isNotEmpty() == true + } + val mAnnotationDeselectedListener = AnnotationManager.OnAnnotationDeselectedListener { _, _ -> commentsButton.setGone() } diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/models/User.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/models/User.kt index a1480a6eff..ae4939858c 100644 --- a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/models/User.kt +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/models/User.kt @@ -47,7 +47,9 @@ data class User( val effective_locale: String? = null, val pronouns: String? = null, @SerializedName("k5_user") - val k5User: Boolean = false + val k5User: Boolean = false, + @SerializedName("root_account") + val rootAccount: String? = null ) : CanvasContext() { override val comparisonString get() = name override val type get() = CanvasContext.Type.USER diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/utils/Analytics.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/utils/Analytics.kt index 08a931fdc8..503e40fdda 100644 --- a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/utils/Analytics.kt +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/utils/Analytics.kt @@ -103,6 +103,8 @@ object AnalyticsEventConstants { const val WHAT_IF_GRADES = "what_if_grades_used" + const val CHANGED_C4E_MODE = "c4e_changed" + /* QR Code Login */ const val QR_CODE_LOGIN_CLICKED = "qr_code_login_clicked" const val QR_CODE_LOGIN_SUCCESS = "qr_code_login_success" @@ -133,4 +135,7 @@ object AnalyticsParamConstants { const val CANVAS_CONTEXT_ID = FirebaseAnalytics.Param.GROUP_ID const val ASSIGNMENT_ID = FirebaseAnalytics.Param.ITEM_ID const val SCREEN_OF_ORIGIN = FirebaseAnalytics.Param.ORIGIN + + //custom + const val MANUAL_C4E_STATE = "manual_c4e_state" } diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/utils/MasqueradeHelper.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/utils/MasqueradeHelper.kt index bf3d896bca..676f16ceb3 100644 --- a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/utils/MasqueradeHelper.kt +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/utils/MasqueradeHelper.kt @@ -97,6 +97,9 @@ object MasqueradeHelper { cleanupMasquerading(ContextKeeper.appContext) ApiPrefs.user = response.body() ApiPrefs.masqueradeId = response.body()!!.id + response.body()?.rootAccount?.let { + ApiPrefs.domain = it + } restartApplication(startingClass) } } diff --git a/libs/pandares/src/main/res/values-ar/strings.xml b/libs/pandares/src/main/res/values-ar/strings.xml index 0eba1ea79a..15b0ad4877 100644 --- a/libs/pandares/src/main/res/values-ar/strings.xml +++ b/libs/pandares/src/main/res/values-ar/strings.xml @@ -1316,6 +1316,13 @@ نسق التطبيق فاتح داكن - النظام الافتراضي - + مثل الجهاز + Canvas متاح الآن في النسق الداكن + اختر نسق التطبيق + النسق الفاتح + النسق الداكن + مثل نسق الجهاز + حفظ + يمكنك تغييرها لاحقًا في إعدادات التطبيق + تحديد diff --git a/libs/pandares/src/main/res/values-b+da+instk12/strings.xml b/libs/pandares/src/main/res/values-b+da+instk12/strings.xml index bcb582c646..c975f0e076 100644 --- a/libs/pandares/src/main/res/values-b+da+instk12/strings.xml +++ b/libs/pandares/src/main/res/values-b+da+instk12/strings.xml @@ -1261,6 +1261,13 @@ App-tema Lys Mørk - Systemstandard - + Samme som enhed + Canvas er nu tilgængelig i mørkt tema + Vælg app-tema + Lyst tema + Mørkt tema + Samme tema som på enhed + Gem + Du kan ændre det senere i app-indstillinger + Tag fat diff --git a/libs/pandares/src/main/res/values-b+en+AU+unimelb/strings.xml b/libs/pandares/src/main/res/values-b+en+AU+unimelb/strings.xml index b494aa11a8..cf3e1bf89f 100644 --- a/libs/pandares/src/main/res/values-b+en+AU+unimelb/strings.xml +++ b/libs/pandares/src/main/res/values-b+en+AU+unimelb/strings.xml @@ -1261,6 +1261,13 @@ App Theme Light Dark - System default - + Same as device + Canvas is now available in dark theme + Choose app theme + Light theme + Dark theme + Same as device theme + Save + You can change it later in app settings + Grab diff --git a/libs/pandares/src/main/res/values-b+en+GB+instukhe/strings.xml b/libs/pandares/src/main/res/values-b+en+GB+instukhe/strings.xml index e8c43256d2..9b7f1fc049 100644 --- a/libs/pandares/src/main/res/values-b+en+GB+instukhe/strings.xml +++ b/libs/pandares/src/main/res/values-b+en+GB+instukhe/strings.xml @@ -1261,6 +1261,13 @@ App Theme Light Dark - System default - + Same as device + Canvas is now available in dark theme + Choose app theme + Light theme + Dark theme + Same as device theme + Save + You can change it later in app settings + Grab diff --git a/libs/pandares/src/main/res/values-b+nb+instk12/strings.xml b/libs/pandares/src/main/res/values-b+nb+instk12/strings.xml index fd918c305e..16816624d0 100644 --- a/libs/pandares/src/main/res/values-b+nb+instk12/strings.xml +++ b/libs/pandares/src/main/res/values-b+nb+instk12/strings.xml @@ -1262,6 +1262,13 @@ App-tema Lys Mørk - Systemstandard - + Samme som enhet + Canvas er nå tilgjengelig med mørkt tema + Velg app-tema + Lyst tema + Mørkt tema + Samme som enhetstema + Lagre + Du kan endre det senere i appinnstillinger + Grip diff --git a/libs/pandares/src/main/res/values-b+sv+instk12/strings.xml b/libs/pandares/src/main/res/values-b+sv+instk12/strings.xml index 5ad94b0667..fd001e51fb 100644 --- a/libs/pandares/src/main/res/values-b+sv+instk12/strings.xml +++ b/libs/pandares/src/main/res/values-b+sv+instk12/strings.xml @@ -1261,6 +1261,13 @@ Apptema Ljus Mörk - Systemstandard - + Samma som enhet + Canvas finns inte tillgängligt med ett mörkt tema + Välj apptema + Ljust tema + Mörkt tema + Samma tema som enheten + Spara + Du kan ändra det sedan i appinställningarna + Ta tag i diff --git a/libs/pandares/src/main/res/values-b+zh+Hans/strings.xml b/libs/pandares/src/main/res/values-b+zh+Hans/strings.xml index 30e9cbeba0..b5ea856bd0 100644 --- a/libs/pandares/src/main/res/values-b+zh+Hans/strings.xml +++ b/libs/pandares/src/main/res/values-b+zh+Hans/strings.xml @@ -1247,6 +1247,13 @@ 应用主题 浅色的、轻的 深色的、黑的 - 系统默认 - + 与设备相同 + Canvas 已推出深色主题 + 选择应用程序主题 + 浅色主题 + 深色主题 + 与设备主题相同 + 保存 + 您可以后在应用设置中更改 + 抓取图像 diff --git a/libs/pandares/src/main/res/values-b+zh+Hant/strings.xml b/libs/pandares/src/main/res/values-b+zh+Hant/strings.xml index 49c9019e06..6808be99dc 100644 --- a/libs/pandares/src/main/res/values-b+zh+Hant/strings.xml +++ b/libs/pandares/src/main/res/values-b+zh+Hant/strings.xml @@ -1247,6 +1247,13 @@ 應用程式主題 亮色 暗色 - 系統預設 - + 和裝置相同 + Canvas 現在可在暗色主題中使用 + 選擇應用程式主題 + 淡色主題 + 暗色主題 + 和裝置主題相同 + 儲存 + 您可以稍後在應用桯式設定中變更 + 擷取 diff --git a/libs/pandares/src/main/res/values-ca/strings.xml b/libs/pandares/src/main/res/values-ca/strings.xml index 8509046ba7..84f6eaac65 100644 --- a/libs/pandares/src/main/res/values-ca/strings.xml +++ b/libs/pandares/src/main/res/values-ca/strings.xml @@ -1262,6 +1262,13 @@ Tema de l’aplicació Clar Fosc - Valor predeterminat del sistema - + El mateix que el dispositiu + Ara, el Canvas està disponible en tema fosc + Trieu el tema de l’aplicació + Tema clar + Tema fosc + El mateix tema que el del dispositiu + Desa + Podeu canviar-lo més endavant a la configuració de l’aplicació + Agafa diff --git a/libs/pandares/src/main/res/values-cy/strings.xml b/libs/pandares/src/main/res/values-cy/strings.xml index ef9bafd0a4..95b5760db0 100644 --- a/libs/pandares/src/main/res/values-cy/strings.xml +++ b/libs/pandares/src/main/res/values-cy/strings.xml @@ -1261,6 +1261,13 @@ Thema Ap Golau Tywyll - Rhagosodiad system - + Yn un fath a’r ddyfais + Mae Canvas bellach ar gael mewn thema dywyll + Dewis thema ap + Thema golau + Thema dywyll + Yr un fath a thema’r ddyfais + Cadw + Gallwch chi ei newid yn nes ymlaen yng ngosodiadau’r ap + Gafael diff --git a/libs/pandares/src/main/res/values-da/strings.xml b/libs/pandares/src/main/res/values-da/strings.xml index 47a16ce4c7..c6f82e0923 100644 --- a/libs/pandares/src/main/res/values-da/strings.xml +++ b/libs/pandares/src/main/res/values-da/strings.xml @@ -1261,6 +1261,13 @@ App-tema Lys Mørk - Systemstandard - + Samme som enhed + Canvas er nu tilgængelig i mørkt tema + Vælg app-tema + Lyst tema + Mørkt tema + Samme tema som på enhed + Gem + Du kan ændre det senere i app-indstillinger + Tag fat diff --git a/libs/pandares/src/main/res/values-de/strings.xml b/libs/pandares/src/main/res/values-de/strings.xml index 88ad68c03b..16b28eec4b 100644 --- a/libs/pandares/src/main/res/values-de/strings.xml +++ b/libs/pandares/src/main/res/values-de/strings.xml @@ -113,7 +113,7 @@ %1$s von %2$s Punkten Hypothetische Punktzahl eingeben Gering: %s - Mittel: %s + Mittelwert: %s Hoch: %s @@ -484,7 +484,7 @@ Entschuldigung. Sie sind nicht zu Ankündigungen in diesem Kurs berechtigt. Entschuldigung. Sie sind nicht zu Diskussionen in diesem Kurs berechtigt. - Announcements + Ankündigungen Der Titel darf nicht leer sein. @@ -733,7 +733,7 @@ Neues Ereignis erstellen Panda ausstellen - Announcements + Ankündigungen Konto hinzufügen Benutzer wechseln @@ -797,7 +797,7 @@ Benachrichtigung erhalten, wenn es eine neue Ankündigung in Ihrem Kurs gibt. Benachrichtigung erhalten, wenn jemand auf eine Ankündigung von Ihnen antwortet. Benachrichtigung erhalten, wenn eine Aufgabe/Abgabe benotet/geändert wurde und wenn eine Notengewichtung geändert wurde. - Benachrichtigung erhalten bei Einladungen zu Webkonferenzen, Gruppen, Kooperationen, Bewertung durch Mitstudenten und Erinnerungen. + Benachrichtigung erhalten bei Einladungen zu Webkonferenzen, Gruppen, Kooperationen, Peer Review und Erinnerungen. Nur für Kursleiter und Administratoren. Benachrichtigung erhalten, wenn eine Aufgabe abgegeben oder erneut abgegeben wird. Nur für Kursleiter und Administratoren. Benachrichtigung erhalten, wenn eine Aufgabe zu spät abgegeben wird. Benachrichtigung erhalten, wenn zu Ihrer Einreichung ein Kommentar abgegeben wird. @@ -999,7 +999,7 @@ Konferenzen werden auf Mobilgeräten noch nicht unterstützt. Dateivorschaubild Fehler beim Versuch diese PDF-Datei zu laden - Tut uns sehr leid! Diese Funktion steht für die Studentenansicht nicht erlaubt. + Tut uns sehr leid! Diese Funktion steht für die Studierendenansicht nicht erlaubt. Hier gibt es nichts zu sehen Nicht unterstützte Funktion @@ -1042,7 +1042,7 @@ Note außer Kraft setzen Aktuelle Note Öffnet in Canvas Student - Studentenansicht + Studierendenansicht @@ -1233,7 +1233,7 @@ Tippen Sie hier, um fortzufahren. Entwurf verfügbar Fehler bei, Laden der Abgabe - Tippen Sie hier, um den vollständigen Content anzuzeigen + Tippen Sie hier, um den vollständigen Inhalt anzuzeigen Wichtige Termine Keine wichtigen Termine Wichtige Termine @@ -1261,6 +1261,13 @@ App-Design Hell Dunkel - Systemstandard - + Wie Gerät + Canvas ist jetzt in dunklem Design verfügbar + App-Design auswählen + Helles Design + Dunkles Design + Entspricht dem Gerätedesign + Speichern + Sie können es später in den App-Einstellungen ändern. + Greifen diff --git a/libs/pandares/src/main/res/values-en-rAU/strings.xml b/libs/pandares/src/main/res/values-en-rAU/strings.xml index 5e317cf5e8..e6e3c9557a 100644 --- a/libs/pandares/src/main/res/values-en-rAU/strings.xml +++ b/libs/pandares/src/main/res/values-en-rAU/strings.xml @@ -1261,6 +1261,13 @@ App Theme Light Dark - System default - + Same as device + Canvas is now available in dark theme + Choose app theme + Light theme + Dark theme + Same as device theme + Save + You can change it later in app settings + Grab diff --git a/libs/pandares/src/main/res/values-en-rCA/strings.xml b/libs/pandares/src/main/res/values-en-rCA/strings.xml index 8d0d17f9a4..33b713916c 100644 --- a/libs/pandares/src/main/res/values-en-rCA/strings.xml +++ b/libs/pandares/src/main/res/values-en-rCA/strings.xml @@ -1,4 +1,4 @@ - - + android:keepScreenOn="true" + app:cameraFacing="front" + app:cameraMode="video"/> Diskussionen Es sind keine Diskussionen zum Anzeigen in diesem Kursabschnitt vorhanden. Beschreibung - Gelistete Antworten zulassen + Antworten aller Diskussionsstränge zulassen Teilnehmer müssen gepostet haben, bevor sie Antworten sehen können. Für Kommentare geschlossen Für Kommentare öffnen diff --git a/libs/rceditor/src/main/AndroidManifest.xml b/libs/rceditor/src/main/AndroidManifest.xml index 6d9b1af89b..70fb8526a2 100644 --- a/libs/rceditor/src/main/AndroidManifest.xml +++ b/libs/rceditor/src/main/AndroidManifest.xml @@ -18,13 +18,4 @@ - - - - - - diff --git a/libs/rceditor/src/main/java/instructure/rceditor/RCEActivity.kt b/libs/rceditor/src/main/java/instructure/rceditor/RCEActivity.kt deleted file mode 100644 index e5f3d29de6..0000000000 --- a/libs/rceditor/src/main/java/instructure/rceditor/RCEActivity.kt +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright (C) 2017 - 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 instructure.rceditor - -import android.app.Activity -import android.content.Context -import android.content.Intent -import android.graphics.Color -import android.os.Bundle -import androidx.annotation.ColorInt -import androidx.appcompat.app.AppCompatActivity -import androidx.fragment.app.Fragment -import instructure.rceditor.RCEFragment.RCEFragmentCallbacks - -class RCEActivity : AppCompatActivity(), RCEFragmentCallbacks { - private var fragment: RCEFragment? = null - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setResult(Activity.RESULT_CANCELED) - setContentView(R.layout.rce_activity_layout) - } - - override fun onAttachFragment(fragment: Fragment) { - if (fragment is RCEFragment) { - this.fragment = fragment - fragment.loadArguments( - intent.getStringExtra(RCEConst.HTML_CONTENT), - intent.getStringExtra(RCEConst.HTML_TITLE), - intent.getStringExtra(RCEConst.HTML_ACCESSIBILITY_TITLE), - intent.getIntExtra(RCEConst.THEME_COLOR, Color.BLACK), - intent.getIntExtra(RCEConst.BUTTON_COLOR, Color.BLACK) - ) - } - super.onAttachFragment(fragment) - } - - override fun onBackPressed() { - fragment?.showExitDialog() - } - - override fun onResult(activityResult: Int, data: Intent?) { - if (activityResult == Activity.RESULT_OK && data != null) { - setResult(activityResult, data) - } else { - setResult(activityResult) - } - finish() - } - - companion object { - fun createIntent(context: Context?, html: String?, title: String?, accessibilityTitle: String?, @ColorInt themeColor: Int, @ColorInt buttonColor: Int): Intent { - val intent = Intent(context, RCEActivity::class.java) - intent.putExtra(RCEConst.HTML_CONTENT, html) - intent.putExtra(RCEConst.HTML_TITLE, title) - intent.putExtra(RCEConst.HTML_ACCESSIBILITY_TITLE, accessibilityTitle) - intent.putExtra(RCEConst.THEME_COLOR, themeColor) - intent.putExtra(RCEConst.BUTTON_COLOR, buttonColor) - return intent - } - } -} diff --git a/libs/rceditor/src/main/java/instructure/rceditor/RCEConst.kt b/libs/rceditor/src/main/java/instructure/rceditor/RCEConst.kt deleted file mode 100644 index b340c3f4fa..0000000000 --- a/libs/rceditor/src/main/java/instructure/rceditor/RCEConst.kt +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright (C) 2017 - 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 instructure.rceditor - -object RCEConst { - const val HTML_RESULT = "HTML_CONTENT_RESULT" - const val HTML_CONTENT = "HTML_CONTENT" - const val HTML_TITLE = "HTML_TITLE" - const val HTML_ACCESSIBILITY_TITLE = "HTML_ACCESSIBILITY_TITLE" - const val THEME_COLOR = "THEME_COLOR" - const val BUTTON_COLOR = "BUTTON_COLOR" -} diff --git a/libs/rceditor/src/main/java/instructure/rceditor/RCEFragment.kt b/libs/rceditor/src/main/java/instructure/rceditor/RCEFragment.kt deleted file mode 100644 index a5de3c1b5d..0000000000 --- a/libs/rceditor/src/main/java/instructure/rceditor/RCEFragment.kt +++ /dev/null @@ -1,246 +0,0 @@ -/* - * Copyright (C) 2017 - 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 instructure.rceditor - -import android.animation.Animator -import android.animation.ObjectAnimator -import android.app.Activity.RESULT_CANCELED -import android.app.Activity.RESULT_OK -import android.content.Context -import android.content.Intent -import android.graphics.Color -import android.os.Build -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.annotation.ColorInt -import androidx.appcompat.app.AlertDialog -import androidx.appcompat.widget.Toolbar -import androidx.core.content.ContextCompat -import androidx.fragment.app.Fragment -import instructure.rceditor.RCEConst.BUTTON_COLOR -import instructure.rceditor.RCEConst.HTML_ACCESSIBILITY_TITLE -import instructure.rceditor.RCEConst.HTML_CONTENT -import instructure.rceditor.RCEConst.HTML_RESULT -import instructure.rceditor.RCEConst.HTML_TITLE -import instructure.rceditor.RCEConst.THEME_COLOR -import kotlinx.android.synthetic.main.rce_color_picker.* -import kotlinx.android.synthetic.main.rce_controller.* -import kotlinx.android.synthetic.main.rce_fragment_layout.* - -class RCEFragment : Fragment() { - - private var callback: RCEFragmentCallbacks? = null - - private val onColorChosen = View.OnClickListener { v -> - when (v.id) { - R.id.rce_colorPickerWhite -> rcEditor.setTextColor(Color.WHITE) - R.id.rce_colorPickerBlack -> rcEditor.setTextColor(Color.BLACK) - R.id.rce_colorPickerGray -> rcEditor.setTextColor(ContextCompat.getColor(requireContext(), R.color.rce_pickerGray)) - R.id.rce_colorPickerRed -> rcEditor.setTextColor(ContextCompat.getColor(requireContext(), R.color.rce_pickerRed)) - R.id.rce_colorPickerOrange -> rcEditor.setTextColor(ContextCompat.getColor(requireContext(), R.color.rce_pickerOrange)) - R.id.rce_colorPickerYellow -> rcEditor.setTextColor(ContextCompat.getColor(requireContext(), R.color.rce_pickerYellow)) - R.id.rce_colorPickerGreen -> rcEditor.setTextColor(ContextCompat.getColor(requireContext(), R.color.rce_pickerGreen)) - R.id.rce_colorPickerBlue -> rcEditor.setTextColor(ContextCompat.getColor(requireContext(), R.color.rce_pickerBlue)) - R.id.rce_colorPickerPurple -> rcEditor.setTextColor(ContextCompat.getColor(requireContext(), R.color.rce_pickerPurple)) - } - - toggleColorPicker() - } - - private val onTextColor = View.OnClickListener { toggleColorPicker() } - private val onUndo = View.OnClickListener { rcEditor.undo() } - private val onRedo = View.OnClickListener { rcEditor.redo() } - private val onBold = View.OnClickListener { rcEditor.setBold() } - private val onItalic = View.OnClickListener { rcEditor.setItalic() } - private val onUnderline = View.OnClickListener { rcEditor.setUnderline() } - private val onInsertBulletList = View.OnClickListener { rcEditor.setBullets() } - - private val onUploadPicture = View.OnClickListener { - val dialog = RCEInsertDialog.newInstance( - getString(R.string.rce_insertImage), - requireArguments().getInt(THEME_COLOR, Color.BLACK), - requireArguments().getInt(BUTTON_COLOR, Color.BLACK)) - dialog.setListener { url, alt -> rcEditor.insertImage(url, alt) }.show(requireFragmentManager(), RCEInsertDialog::class.java.simpleName) - } - - private val onInsertLink = View.OnClickListener { - val dialog = RCEInsertDialog.newInstance( - getString(R.string.rce_insertLink), - requireArguments().getInt(THEME_COLOR, Color.BLACK), - requireArguments().getInt(BUTTON_COLOR, Color.BLACK)) - dialog.setListener { url, alt -> rcEditor.insertLink(url, alt) }.show(requireFragmentManager(), RCEInsertDialog::class.java.simpleName) - } - - interface RCEFragmentCallbacks { - fun onResult(activityResult: Int, data: Intent?) - } - - init { - if (arguments == null) { - arguments = Bundle() - } - } - - override fun onAttach(context: Context) { - super.onAttach(context) - if (context is RCEFragmentCallbacks) { - callback = context - } else { - throw IllegalStateException("Context must implement RCEFragment.RCEFragmentCallbacks()") - } - } - - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? = - inflater.inflate(R.layout.rce_fragment_layout, container, false) - - override fun onActivityCreated(savedInstanceState: Bundle?) { - super.onActivityCreated(savedInstanceState) - setupViews() - } - - private fun setupViews() { - rcEditor.setPadding(10, 10, 10, 10) - rcEditor.applyHtml( - requireArguments().getString(HTML_CONTENT) ?: "", - requireArguments().getString(HTML_ACCESSIBILITY_TITLE) ?: "") - - with(rceToolbar) { - title = requireArguments().getString(HTML_TITLE) - inflateMenu(R.menu.rce_save_menu) - setNavigationIcon(R.drawable.ic_rce_cancel) - setNavigationContentDescription(R.string.rce_cancel) - setNavigationOnClickListener(View.OnClickListener { - // Check to see if we made any changes. If we haven't, just close the fragment - if (rcEditor.html != null && requireArguments().getString(HTML_CONTENT) != null) { - if (rcEditor.html == requireArguments().getString(HTML_CONTENT)) { - callback?.onResult(RESULT_CANCELED, null) - return@OnClickListener - } - } - showExitDialog() - }) - - setOnMenuItemClickListener(Toolbar.OnMenuItemClickListener { item -> - if (item.itemId == R.id.rce_save) { - val data = Intent() - data.putExtra(HTML_RESULT, if (rcEditor.html == null) - requireArguments().getString(HTML_CONTENT) - else - rcEditor.html) - callback?.onResult(RESULT_OK, data) - return@OnMenuItemClickListener true - } - false - }) - } - - requireActivity().window.statusBarColor = ContextCompat.getColor(requireContext(), R.color.rce_dimStatusBarGray) - requireActivity().window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR - - rce_colorPickerWhite.setOnClickListener(onColorChosen) - rce_colorPickerBlack.setOnClickListener(onColorChosen) - rce_colorPickerGray.setOnClickListener(onColorChosen) - rce_colorPickerRed.setOnClickListener(onColorChosen) - rce_colorPickerOrange.setOnClickListener(onColorChosen) - rce_colorPickerYellow.setOnClickListener(onColorChosen) - rce_colorPickerGreen.setOnClickListener(onColorChosen) - rce_colorPickerBlue.setOnClickListener(onColorChosen) - rce_colorPickerPurple.setOnClickListener(onColorChosen) - - action_undo.setOnClickListener(onUndo) - action_redo.setOnClickListener(onRedo) - action_bold.setOnClickListener(onBold) - action_italic.setOnClickListener(onItalic) - action_underline.setOnClickListener(onUnderline) - action_insert_bullets.setOnClickListener(onInsertBulletList) - actionUploadImage.setOnClickListener(onUploadPicture) - action_insert_link.setOnClickListener(onInsertLink) - action_txt_color.setOnClickListener(onTextColor) - } - - private fun toggleColorPicker() { - if (rceColorPickerWrapper.visibility == View.VISIBLE) { - val animator = ObjectAnimator.ofFloat(rceColorPickerWrapper, "translationY", rceColorPickerWrapper.height * -1f, 0f) - animator.duration = 200 - animator.addListener(object : RCEAnimationListener() { - override fun onAnimationFinish(animation: Animator) { - rceColorPickerWrapper?.visibility = View.INVISIBLE - } - }) - animator.start() - } else { - val animator = ObjectAnimator.ofFloat(rceColorPickerWrapper, "translationY", 0f, rceColorPickerWrapper!!.height * -1f) - animator.duration = 230 - animator.addListener(object : RCEAnimationListener() { - override fun onAnimationBegin(animation: Animator) { - rceColorPickerWrapper?.post { rceColorPickerWrapper?.visibility = View.VISIBLE } - } - }) - animator.start() - } - } - - fun loadArguments(html: String?, title: String?, accessibilityTitle: String?, @ColorInt themeColor: Int, @ColorInt buttonColor: Int) { - with (requireArguments()) { - putString(HTML_CONTENT, html) - putString(HTML_TITLE, title) - putString(HTML_ACCESSIBILITY_TITLE, accessibilityTitle) - putInt(THEME_COLOR, themeColor) - putInt(BUTTON_COLOR, buttonColor) - } - } - - fun showExitDialog() { - AlertDialog.Builder(requireContext()) - .setTitle(R.string.rce_dialog_exit_title) - .setMessage(R.string.rce_dialog_exit_message) - .setPositiveButton(R.string.rce_exit) { dialog, _ -> - dialog.dismiss() - callback?.onResult(RESULT_CANCELED, null) - } - .setNegativeButton(R.string.rce_cancel) { dialog, _ -> dialog.dismiss() } - .create() - .show() - } - - companion object { - - fun newInstance(html: String, title: String, accessibilityTitle: String, @ColorInt themeColor: Int, @ColorInt buttonColor: Int): RCEFragment { - val fragment = RCEFragment() - fragment.arguments = makeBundle(html, title, accessibilityTitle, themeColor, buttonColor) - return fragment - } - - fun newInstance(args: Bundle): RCEFragment { - val fragment = RCEFragment() - fragment.arguments = args - return fragment - } - - fun makeBundle(html: String, title: String, accessibilityTitle: String, @ColorInt themeColor: Int, @ColorInt buttonColor: Int): Bundle { - val args = Bundle() - args.putString(HTML_CONTENT, html) - args.putString(HTML_TITLE, title) - args.putString(HTML_ACCESSIBILITY_TITLE, accessibilityTitle) - args.putInt(THEME_COLOR, themeColor) - args.putInt(BUTTON_COLOR, buttonColor) - return args - } - } -} diff --git a/libs/rceditor/src/main/java/instructure/rceditor/RCEInsertDialog.kt b/libs/rceditor/src/main/java/instructure/rceditor/RCEInsertDialog.kt index d541ef3d25..7585d8bc89 100644 --- a/libs/rceditor/src/main/java/instructure/rceditor/RCEInsertDialog.kt +++ b/libs/rceditor/src/main/java/instructure/rceditor/RCEInsertDialog.kt @@ -53,9 +53,10 @@ class RCEInsertDialog : AppCompatDialogFragment() { builder.setTitle(arguments?.getString(TITLE)) builder.setPositiveButton(R.string.rce_dialogDone, null) // Override listener in onShow builder.setNegativeButton(R.string.rce_dialogCancel) { _, _ -> dismiss() } - val themeColor = arguments?.getInt(THEME_COLOR, Color.BLACK) ?: Color.BLACK + val defaultColor = context?.getColor(R.color.rce_defaultTextColor) ?: Color.BLACK + val themeColor = arguments?.getInt(THEME_COLOR, defaultColor) ?: defaultColor val highlightColor = increaseAlpha(themeColor) - val colorStateList = makeEditTextColorStateList(Color.BLACK, themeColor) + val colorStateList = makeEditTextColorStateList(defaultColor, themeColor) altEditText = root.findViewById(R.id.altEditText) urlEditText = root.findViewById(R.id.urlEditText) altEditText.highlightColor = highlightColor @@ -64,7 +65,7 @@ class RCEInsertDialog : AppCompatDialogFragment() { urlEditText.supportBackgroundTintList = colorStateList val dialog = builder.create() dialog.setOnShowListener { - val buttonColor = arguments?.getInt(BUTTON_COLOR, Color.BLACK) ?: Color.BLACK + val buttonColor = arguments?.getInt(BUTTON_COLOR, defaultColor) ?: defaultColor dialog.getButton(DialogInterface.BUTTON_POSITIVE).setTextColor(buttonColor) dialog.getButton(DialogInterface.BUTTON_NEGATIVE).setTextColor(buttonColor) val button = dialog.getButton(AlertDialog.BUTTON_POSITIVE) diff --git a/libs/rceditor/src/main/java/instructure/rceditor/RCETextEditorView.kt b/libs/rceditor/src/main/java/instructure/rceditor/RCETextEditorView.kt index be398c4e53..59017b7a79 100644 --- a/libs/rceditor/src/main/java/instructure/rceditor/RCETextEditorView.kt +++ b/libs/rceditor/src/main/java/instructure/rceditor/RCETextEditorView.kt @@ -19,6 +19,7 @@ package instructure.rceditor import android.animation.Animator import android.animation.ObjectAnimator +import android.app.Activity import android.content.Context import android.content.DialogInterface import android.content.res.Resources @@ -41,6 +42,7 @@ import android.widget.RelativeLayout import android.widget.TextView import kotlinx.android.synthetic.main.rce_color_picker.view.* import kotlinx.android.synthetic.main.rce_controller.view.* +import kotlinx.android.synthetic.main.rce_dialog_alt_text.view.* import kotlinx.android.synthetic.main.rce_text_editor_view.view.rce_bottomDivider as bottomDivider import kotlinx.android.synthetic.main.rce_text_editor_view.view.rce_colorPickerWrapper as colorPickerView import kotlinx.android.synthetic.main.rce_text_editor_view.view.rce_controller as controller @@ -195,10 +197,45 @@ class RCETextEditorView @JvmOverloads constructor( editor.setPadding(left, top, right, bottom) } + fun insertImage(activity: Activity, imageUrl: String) { + showAltTextDialog(activity, { altText -> + editor.insertImage(imageUrl, altText) + }, { + editor.insertImage(imageUrl, "") + }) + } + fun insertImage(url: String, alt: String) { editor.insertImage(url, alt) } + private fun showAltTextDialog(activity: Activity, onPositiveClick: (String) -> Unit, onNegativeClick: () -> Unit) { + val view = View.inflate(activity, R.layout.rce_dialog_alt_text, null) + val altTextInput = view?.altText + + var buttonClicked = false + + val altTextDialog = AlertDialog.Builder(activity) + .setTitle(activity.getString(R.string.rce_dialogAltText)) + .setView(view) + .setPositiveButton(activity.getString(android.R.string.ok)) { _, _ -> + buttonClicked = true + onPositiveClick(altTextInput?.text.toString()) + } + .setNegativeButton(activity.getString(android.R.string.cancel), { _, _ -> + buttonClicked = true + onNegativeClick() + }) + .setOnDismissListener { + if (!buttonClicked) { + onNegativeClick() + } + } + .create() + + altTextDialog.show() + } + fun setHtml( html: String?, accessibilityTitle: String, @@ -208,7 +245,7 @@ class RCETextEditorView @JvmOverloads constructor( ) { editor.applyHtml(html.orEmpty(), accessibilityTitle) editor.setPlaceholder(hint) - setThemeColor(themeColor) + this.themeColor = themeColor this.buttonColor = buttonColor } @@ -241,10 +278,6 @@ class RCETextEditorView @JvmOverloads constructor( } } - fun setThemeColor(@ColorInt color: Int) { - themeColor = color - } - private fun toggleColorPicker() { if (colorPickerView.visibility == View.VISIBLE) { val animator = ObjectAnimator.ofFloat(colorPickerView, "translationY", colorPickerView.height * -1f, 0f) diff --git a/libs/rceditor/src/main/res/layout/rce_activity_layout.xml b/libs/rceditor/src/main/res/layout/rce_activity_layout.xml deleted file mode 100644 index d11072361c..0000000000 --- a/libs/rceditor/src/main/res/layout/rce_activity_layout.xml +++ /dev/null @@ -1,31 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/libs/rceditor/src/main/res/layout/rce_dialog_alt_text.xml b/libs/rceditor/src/main/res/layout/rce_dialog_alt_text.xml new file mode 100644 index 0000000000..7c435c0263 --- /dev/null +++ b/libs/rceditor/src/main/res/layout/rce_dialog_alt_text.xml @@ -0,0 +1,28 @@ + + + + + + \ No newline at end of file diff --git a/libs/rceditor/src/main/res/layout/rce_fragment_layout.xml b/libs/rceditor/src/main/res/layout/rce_fragment_layout.xml deleted file mode 100644 index 2e4650f13a..0000000000 --- a/libs/rceditor/src/main/res/layout/rce_fragment_layout.xml +++ /dev/null @@ -1,71 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/libs/rceditor/src/main/res/menu/rce_save_menu.xml b/libs/rceditor/src/main/res/menu/rce_save_menu.xml deleted file mode 100644 index 06c3ad902c..0000000000 --- a/libs/rceditor/src/main/res/menu/rce_save_menu.xml +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/libs/rceditor/src/main/res/values/rce_strings.xml b/libs/rceditor/src/main/res/values/rce_strings.xml index 4754bc1623..65c0243a76 100644 --- a/libs/rceditor/src/main/res/values/rce_strings.xml +++ b/libs/rceditor/src/main/res/values/rce_strings.xml @@ -69,4 +69,5 @@ Url cannot be blank Only https urls are accepted + Add alt text for screen readers