From f200177f7ffe08542b2d3f2a236536b70607fbd7 Mon Sep 17 00:00:00 2001 From: Kristof Deak <92309696+kdeakinstructure@users.noreply.github.com> Date: Wed, 20 Jul 2022 15:30:41 +0200 Subject: [PATCH 01/49] Fix breaking multi_api_level tests (#1653) * fix profileSettingsE2E on nightly (we are using different device than in local thats what caused the flakyness) refs: affects: release note: test plan: * attempt to fix testAnnouncement_reply test case (it may do not wait enough for the webview to be loaded) refs: affects: release note: test plan: * stub testFilters method on landscape mode because on lowres device its too narrow to scroll properly. Will be checked/refactored when we migrated from lowres device in nightly runs. refs: affects: Student release note: none * put back todoe2e tests on weekends. (to test on bitrise) refs: affects: Student, Teacher release note: none * put back todoe2e tests on weekends. (to test on bitrise) refs: affects: Student, Teacher release note: none * Fix breaking render tests on 'multi_api_level' jobs. (It was stubbed on Teacher because of the same issue, now, both Teacher and Student were fixed so Stubs has removed as well.) refs: affects: release note: test plan: --- .../instructure/student/ui/renderTests/SyllabusRenderTest.kt | 2 ++ .../instructure/teacher/ui/renderTests/SyllabusRenderTest.kt | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/SyllabusRenderTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/SyllabusRenderTest.kt index aa83cfa912..66b6355a37 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/SyllabusRenderTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/SyllabusRenderTest.kt @@ -29,6 +29,7 @@ import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Before import org.junit.Test import org.junit.runner.RunWith +import java.lang.Thread.sleep @HiltAndroidTest @RunWith(AndroidJUnit4::class) @@ -140,6 +141,7 @@ class SyllabusRenderTest : StudentRenderTest() { loopMod = { it.effectRunner { emptyEffectRunner } } } activityRule.activity.loadFragment(fragment) + sleep(3000) // Need to wait here a bit because loadFragment needs some time. } } diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/renderTests/SyllabusRenderTest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/renderTests/SyllabusRenderTest.kt index 90b5195be2..448872748b 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/renderTests/SyllabusRenderTest.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/renderTests/SyllabusRenderTest.kt @@ -29,6 +29,7 @@ import com.spotify.mobius.runners.WorkRunner import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Before import org.junit.Test +import java.lang.Thread.sleep @HiltAndroidTest class SyllabusRenderTest : TeacherRenderTest() { @@ -102,7 +103,6 @@ class SyllabusRenderTest : TeacherRenderTest() { } @Test - @Stub fun tappingEventsDisplaysEvents() { val model = baseModel.copy(events = DataResult.Success(List(3) { ScheduleItem(title = it.toString()) })) loadPageWithModel(model) @@ -113,7 +113,6 @@ class SyllabusRenderTest : TeacherRenderTest() { } @Test - @Stub fun swipingToEventsDisplaysEvents() { val model = baseModel.copy(events = DataResult.Success(List(3) { ScheduleItem(title = it.toString()) })) loadPageWithModel(model) @@ -153,5 +152,6 @@ class SyllabusRenderTest : TeacherRenderTest() { loopMod = { it.effectRunner { emptyEffectRunner } } } activityRule.activity.loadFragment(fragment) + sleep(3000) // Need to wait here a bit because loadFragment needs some time. } } \ No newline at end of file From 36079a0d1b81985f68326fdc8db4a02ceb0325b9 Mon Sep 17 00:00:00 2001 From: Tamas Kozmer Date: Wed, 20 Jul 2022 15:35:44 +0200 Subject: [PATCH 02/49] Updated version. --- apps/teacher/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/teacher/build.gradle b/apps/teacher/build.gradle index b3baa306d5..c63e8215b4 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 = 51 - versionName = '1.18.3' + versionCode = 52 + versionName = '1.19.0' vectorDrawables.useSupportLibrary = true multiDexEnabled true testInstrumentationRunner 'com.instructure.teacher.ui.espresso.TeacherHiltTestRunner' From 5662c96320c6bfb7c90cff51dd3346780f6c84d2 Mon Sep 17 00:00:00 2001 From: Kristof Deak <92309696+kdeakinstructure@users.noreply.github.com> Date: Thu, 21 Jul 2022 16:00:09 +0200 Subject: [PATCH 03/49] Create Low Resolution device E2E test flank files. (#1658) refs: MBL-16164 affects: Teacher, Student release note: test plan: --- apps/student/flank_e2e_lowres.yml | 26 ++++++++++++++++++++++++++ apps/teacher/flank_e2e_lowres.yml | 26 ++++++++++++++++++++++++++ 2 files changed, 52 insertions(+) create mode 100644 apps/student/flank_e2e_lowres.yml create mode 100644 apps/teacher/flank_e2e_lowres.yml diff --git a/apps/student/flank_e2e_lowres.yml b/apps/student/flank_e2e_lowres.yml new file mode 100644 index 0000000000..d7862027cb --- /dev/null +++ b/apps/student/flank_e2e_lowres.yml @@ -0,0 +1,26 @@ +gcloud: + project: delta-essence-114723 +# Use the next two lines to run locally +# app: ./build/outputs/apk/qa/debug/student-qa-debug.apk +# test: ./build/outputs/apk/androidTest/qa/debug/student-qa-debug-androidTest.apk + app: ./apps/student/build/outputs/apk/qa/debug/student-qa-debug.apk + test: ./apps/student/build/outputs/apk/androidTest/qa/debug/student-qa-debug-androidTest.apk + results-bucket: android-student + auto-google-login: true + use-orchestrator: true + performance-metrics: false + record-video: true + timeout: 60m + test-targets: + - 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 + version: 26 + locale: en_US + orientation: portrait + +flank: + testShards: 1 + testRuns: 1 + diff --git a/apps/teacher/flank_e2e_lowres.yml b/apps/teacher/flank_e2e_lowres.yml new file mode 100644 index 0000000000..8e6947ac6d --- /dev/null +++ b/apps/teacher/flank_e2e_lowres.yml @@ -0,0 +1,26 @@ +gcloud: + project: delta-essence-114723 + # Use the next two lines to run locally + # app: ./build/outputs/apk/qa/debug/teacher-qa-debug.apk + # test: ./build/outputs/apk/androidTest/qa/debug/teacher-qa-debug-androidTest.apk + app: ./apps/teacher/build/outputs/apk/qa/debug/teacher-qa-debug.apk + test: ./apps/teacher/build/outputs/apk/androidTest/qa/debug/teacher-qa-debug-androidTest.apk + results-bucket: android-teacher + auto-google-login: true + use-orchestrator: true + performance-metrics: false + record-video: true + timeout: 60m + test-targets: + - 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 + version: 26 + locale: en_US + orientation: portrait + +flank: + testShards: 1 + testRuns: 1 + From f415161e2288f5a499d4a454ab764fd9484fb1ce Mon Sep 17 00:00:00 2001 From: Kristof Deak <92309696+kdeakinstructure@users.noreply.github.com> Date: Fri, 22 Jul 2022 11:46:19 +0200 Subject: [PATCH 04/49] [MBL-16100] [Student] - Refactor and extend LoginE2E tests (#1650) * Refactor and extend LoginE2ETest in Student app. refs: MBL-16100 affects: Student release note: none * Simplify and refactor validation method to be clearer. refs: MBL-16100 affects: Student release note: none --- .../student/ui/e2e/LoginE2ETest.kt | 148 ++++++++++-------- .../student/ui/pages/LoginLandingPage.kt | 26 ++- 2 files changed, 109 insertions(+), 65 deletions(-) diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/LoginE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/LoginE2ETest.kt index 62d522e87a..b0b9bb3748 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/LoginE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/LoginE2ETest.kt @@ -17,7 +17,6 @@ package com.instructure.student.ui.e2e import android.util.Log -import androidx.test.espresso.Espresso import com.instructure.canvas.espresso.E2E import com.instructure.dataseeding.api.CoursesApi import com.instructure.dataseeding.api.EnrollmentsApi @@ -28,8 +27,12 @@ import com.instructure.dataseeding.model.CourseApiModel import com.instructure.dataseeding.model.EnrollmentTypes.STUDENT_ENROLLMENT import com.instructure.dataseeding.model.EnrollmentTypes.TEACHER_ENROLLMENT import com.instructure.dataseeding.util.CanvasNetworkAdapter -import com.instructure.panda_annotations.* +import com.instructure.panda_annotations.FeatureCategory +import com.instructure.panda_annotations.Priority +import com.instructure.panda_annotations.TestCategory +import com.instructure.panda_annotations.TestMetaData import com.instructure.student.ui.utils.StudentTest +import com.instructure.student.ui.utils.ViewUtils import com.instructure.student.ui.utils.seedData import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test @@ -54,38 +57,20 @@ class LoginE2ETest : StudentTest() { val student1 = data.studentsList[0] val student2 = data.studentsList[1] - Log.d(STEP_TAG,"Click 'Find My School' button.") - loginLandingPage.clickFindMySchoolButton() - - Log.d(STEP_TAG,"Enter domain: ${student1.domain}.") - loginFindSchoolPage.enterDomain(student1.domain) - - Log.d(STEP_TAG,"Click on 'Next' button on the Toolbar.") - loginFindSchoolPage.clickToolbarNextMenuItem() - Log.d(STEP_TAG,"Login with user: ${student1.name}, login id: ${student1.loginId} , password: ${student1.password}") - loginSignInPage.loginAs(student1) + loginWithUser(student1) Log.d(STEP_TAG,"Assert that the Dashboard Page is the landing page and it is loaded successfully.") - verifyDashboardPage(student1) + assertDashboardPageDisplayed(student1) Log.d(STEP_TAG,"Log out with ${student1.name} student.") dashboardPage.logOut() - Log.d(STEP_TAG,"Click 'Find My School' button.") - loginLandingPage.clickFindMySchoolButton() - - Log.d(STEP_TAG,"Enter domain: ${student2.domain}.") - loginFindSchoolPage.enterDomain(student2.domain) - - Log.d(STEP_TAG,"Click on 'Next' button on the Toolbar.") - loginFindSchoolPage.clickToolbarNextMenuItem() - Log.d(STEP_TAG,"Login with user: ${student2.name}, login id: ${student2.loginId} , password: ${student2.password}") - loginSignInPage.loginAs(student2) + loginWithUser(student2) Log.d(STEP_TAG,"Assert that the Dashboard Page is the landing page and it is loaded successfully.") - verifyDashboardPage(student2) + assertDashboardPageDisplayed(student2) Log.d(STEP_TAG,"Click on 'Change User' button on the left-side menu.") dashboardPage.pressChangeUser() @@ -93,32 +78,44 @@ class LoginE2ETest : StudentTest() { Log.d(STEP_TAG,"Assert that the previously logins has been displayed.") loginLandingPage.assertDisplaysPreviousLogins() - Log.d(STEP_TAG,"Login MANUALLY. Click 'Find My School' button.") - loginLandingPage.clickFindMySchoolButton() - - Log.d(STEP_TAG,"Enter domain: ${student1.domain}.") - loginFindSchoolPage.enterDomain(student1.domain) - - Log.d(STEP_TAG,"Click on 'Next' button on the Toolbar.") - loginFindSchoolPage.clickToolbarNextMenuItem() - Log.d(STEP_TAG,"Login with user: ${student1.name}, login id: ${student1.loginId} , password: ${student1.password}") - loginSignInPage.loginAs(student1) + loginWithUser(student1) Log.d(STEP_TAG,"Assert that the Dashboard Page is the landing page and it is loaded successfully.") - verifyDashboardPage(student1) + assertDashboardPageDisplayed(student1) Log.d(STEP_TAG,"Click on 'Change User' button on the left-side menu.") dashboardPage.pressChangeUser() - Log.d(STEP_TAG,"Assert that the previously logins has been displayed.") + Log.d(STEP_TAG,"Assert that the previously logins has been displayed. Assert that ${student1.name} and ${student2.name} students are displayed within the previous login section.") loginLandingPage.assertDisplaysPreviousLogins() + loginLandingPage.assertPreviousLoginUserDisplayed(student1.name) + loginLandingPage.assertPreviousLoginUserDisplayed(student2.name) + + Log.d(STEP_TAG,"Remove ${student1.name} student from the previous login section.") + loginLandingPage.removeUserFromPreviousLogins(student1.name) Log.d(STEP_TAG,"Login with the previous user, ${student2.name}, with one click, by clicking on the user's name on the bottom.") loginLandingPage.loginWithPreviousUser(student2) Log.d(STEP_TAG,"Assert that the Dashboard Page is the landing page and it is loaded successfully.") - verifyDashboardPage(student2) + assertDashboardPageDisplayed(student2) + + Log.d(STEP_TAG,"Click on 'Change User' button on the left-side menu.") + dashboardPage.pressChangeUser() + + Log.d(STEP_TAG,"Assert that the previously logins has been displayed. Assert that ${student1.name} and ${student2.name} students are displayed within the previous login section.") + loginLandingPage.assertDisplaysPreviousLogins() + loginLandingPage.assertPreviousLoginUserDisplayed(student2.name) + + Log.d(STEP_TAG,"Remove ${student2.name} student from the previous login section.") + loginLandingPage.removeUserFromPreviousLogins(student2.name) + + Log.d(STEP_TAG,"Assert that none of the students, ${student1.name} and ${student2.name} are displayed and not even the 'Previous Logins' label is displayed.") + loginLandingPage.assertPreviousLoginUserNotExist(student1.name) + loginLandingPage.assertPreviousLoginUserNotExist(student2.name) + loginLandingPage.assertNotDisplaysPreviousLogins() + } @E2E @@ -137,34 +134,51 @@ class LoginE2ETest : StudentTest() { val teacher = data.teachersList[0] val ta = data.taList[0] val course = data.coursesList[0] + val parent = parentData.parentsList[0] //Test with Parent user. parents don't show up in the "People" page so we can't verify their role. + + Log.d(STEP_TAG,"Login with user: ${student.name}, login id: ${student.loginId} , password: ${student.password}") + loginWithUser(student) Log.d(STEP_TAG,"Validate ${student.name} user's role as a Student.") validateUserAndRole(student, course, "Student") + Log.d(STEP_TAG,"Navigate back to Dashboard Page.") + ViewUtils.pressBackButton(2) + + Log.d(STEP_TAG,"Log out with ${student.name} student.") + dashboardPage.logOut() + + Log.d(STEP_TAG,"Login with user: ${teacher.name}, login id: ${teacher.loginId} , password: ${teacher.password}") + loginWithUser(teacher) + Log.d(STEP_TAG,"Validate ${teacher.name} user's role as a Teacher.") validateUserAndRole(teacher, course, "Teacher") + Log.d(STEP_TAG,"Navigate back to Dashboard Page.") + ViewUtils.pressBackButton(2) + + Log.d(STEP_TAG,"Log out with ${teacher.name} teacher.") + dashboardPage.logOut() + + Log.d(STEP_TAG,"Login with user: ${ta.name}, login id: ${ta.loginId} , password: ${ta.password}") + loginWithUser(ta) + Log.d(STEP_TAG,"Validate ${ta.name} user's role as a TA.") validateUserAndRole(ta, course, "TA") - // Test with Parent user. parents don't show up in the "People" page so we can't verify their role. - val parent = parentData.parentsList[0] - Log.d(STEP_TAG,"Click 'Find My School' button.") - loginLandingPage.clickFindMySchoolButton() - - Log.d(STEP_TAG,"Enter domain: ${parent.domain}.") - loginFindSchoolPage.enterDomain(parent.domain) + Log.d(STEP_TAG,"Navigate back to Dashboard Page.") + ViewUtils.pressBackButton(2) - Log.d(STEP_TAG,"Enter domain: ${parent.domain}.") - loginFindSchoolPage.clickToolbarNextMenuItem() + Log.d(STEP_TAG,"Log out with ${ta.name} teacher assistant.") + dashboardPage.logOut() Log.d(STEP_TAG,"Login with user: ${parent.name}, login id: ${parent.loginId} , password: ${parent.password}") - loginSignInPage.loginAs(parent) + loginWithUser(parent) Log.d(STEP_TAG,"Assert that the Dashboard Page is the landing page and it is loaded successfully.") - verifyDashboardPage(parent) + assertDashboardPageDisplayed(parent) - Log.d(STEP_TAG,"Log out with ${parent.name} student.") + Log.d(STEP_TAG,"Log out with ${parent.name} parent.") dashboardPage.logOut() } @@ -203,34 +217,46 @@ class LoginE2ETest : StudentTest() { enrollmentService = enrollmentsService ) + Log.d(STEP_TAG,"Login with user: ${student.name}, login id: ${student.loginId} , password: ${student.password}") + loginWithUser(student) + Log.d(STEP_TAG,"Attempt to sign into our vanity domain, and validate ${student.name} user's role as a Student.") validateUserAndRole(student, course,"Student" ) + + Log.d(STEP_TAG,"Navigate back to Dashboard Page.") + ViewUtils.pressBackButton(2) + + Log.d(STEP_TAG,"Log out with ${student.name} student.") + dashboardPage.logOut() } - // Repeated logic from the testUserRolesLoginE2E test. - // Assumes that you start at the login landing page, and logs you out before completing. - private fun validateUserAndRole(user: CanvasUserApiModel, course: CourseApiModel, role: String) { + private fun loginWithUser(user: CanvasUserApiModel) { + Log.d(STEP_TAG,"Click 'Find My School' button.") loginLandingPage.clickFindMySchoolButton() + + Log.d(STEP_TAG,"Enter domain: ${user.domain}.") loginFindSchoolPage.enterDomain(user.domain) + + Log.d(STEP_TAG,"Click on 'Next' button on the Toolbar.") loginFindSchoolPage.clickToolbarNextMenuItem() loginSignInPage.loginAs(user) + } + + private fun validateUserAndRole(user: CanvasUserApiModel, course: CourseApiModel, role: String) { - // Verify that we are signed in as the user - verifyDashboardPage(user) + Log.d(STEP_TAG,"Assert that the Dashboard Page is the landing page and it is loaded successfully.") + assertDashboardPageDisplayed(user) - // Verify that our role is correct + Log.d(STEP_TAG,"Navigate to 'People' Page of ${course.name} course.") dashboardPage.selectCourse(course) courseBrowserPage.selectPeople() - peopleListPage.assertPersonListed(user, role) - Espresso.pressBack() // to course browser page - Espresso.pressBack() // to dashboard page - // Sign the user out - dashboardPage.logOut() + Log.d(STEP_TAG,"Assert that ${user.name} user's role is: $role.") + peopleListPage.assertPersonListed(user, role) } - private fun verifyDashboardPage(user: CanvasUserApiModel) + private fun assertDashboardPageDisplayed(user: CanvasUserApiModel) { dashboardPage.waitForRender() dashboardPage.assertUserLoggedIn(user) diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/LoginLandingPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/LoginLandingPage.kt index 19c5b5e2e8..1fe4cc35ad 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/LoginLandingPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/LoginLandingPage.kt @@ -16,16 +16,18 @@ */ package com.instructure.student.ui.pages +import androidx.test.espresso.assertion.ViewAssertions.doesNotExist +import androidx.test.espresso.matcher.ViewMatchers.hasSibling +import androidx.test.espresso.matcher.ViewMatchers.withChild import com.instructure.canvasapi2.models.User -import com.instructure.canvasapi2.utils.RemoteConfigParam -import com.instructure.canvasapi2.utils.RemoteConfigUtils import com.instructure.dataseeding.model.CanvasUserApiModel import com.instructure.espresso.OnViewWithId 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.onViewWithText +import com.instructure.espresso.page.* import com.instructure.student.R +import org.hamcrest.CoreMatchers.allOf @Suppress("unused") class LoginLandingPage : BasePage() { @@ -65,6 +67,22 @@ class LoginLandingPage : BasePage() { previousLoginTitleText.assertDisplayed() } + fun assertNotDisplaysPreviousLogins() { + previousLoginTitleText.assertNotDisplayed() + } + + fun assertPreviousLoginUserDisplayed(userName: String) { + onView(withText(userName)).assertDisplayed() + } + + fun assertPreviousLoginUserNotExist(userName: String) { + onView(withText(userName)).check(doesNotExist()) + } + + fun removeUserFromPreviousLogins(userName: String) { + onView(allOf(withId(R.id.removePreviousUser), hasSibling(withChild(withText(userName))))).click() + } + fun loginWithPreviousUser(previousUser: CanvasUserApiModel) { onViewWithText(previousUser.name).click() } From 527a6d2fec5161baf4a1f0ff01628a5b3caa3777 Mon Sep 17 00:00:00 2001 From: Kristof Deak <92309696+kdeakinstructure@users.noreply.github.com> Date: Mon, 25 Jul 2022 13:16:31 +0200 Subject: [PATCH 05/49] [MBL-16101][Teacher] - Implement Login E2E test cases to Teacher app (#1652) * Implement Login E2E test cases to Teacher app. Add some page object methods and members to classes Some refactor refs: MBL-16101 affects: Teacher release note: test plan: * Rewrite retry number (unintentionally pushed) refs: MBL-16101 affects: Teacher release note: test plan: * Add private-data directory (fix git error) refs: MBL-16101 affects: release note: test plan: * Rewrite retry login to 5. (unintentionally pushed) refs: MBL-16101 affects: Teacher release note: test plan: * Copyright year fix. Replace empty methods with Units. Reduce timeout time to 10 secs of waitForMatcherWithSleeps method. refs: MBL-16101 affects: Teacher release note: test plan: --- .../teacher/ui/e2e/LoginE2ETest.kt | 202 ++++++++++++++++++ .../teacher/ui/pages/DashboardPage.kt | 44 ++++ .../teacher/ui/pages/LoginLandingPage.kt | 15 ++ .../teacher/ui/pages/NotATeacherPage.kt | 9 +- .../teacher/ui/pages/PeopleListPage.kt | 49 +++++ .../espresso/ScreenshotTestRule.kt | 6 +- 6 files changed, 320 insertions(+), 5 deletions(-) create mode 100644 apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/LoginE2ETest.kt diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/LoginE2ETest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/LoginE2ETest.kt new file mode 100644 index 0000000000..4467a12511 --- /dev/null +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/LoginE2ETest.kt @@ -0,0 +1,202 @@ +/* + * Copyright (C) 2022 - 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.e2e + +import android.util.Log +import com.instructure.canvas.espresso.E2E +import com.instructure.dataseeding.api.SeedApi +import com.instructure.dataseeding.model.CanvasUserApiModel +import com.instructure.dataseeding.model.CourseApiModel +import com.instructure.espresso.ViewUtils +import com.instructure.panda_annotations.FeatureCategory +import com.instructure.panda_annotations.Priority +import com.instructure.panda_annotations.TestCategory +import com.instructure.panda_annotations.TestMetaData +import com.instructure.teacher.ui.utils.TeacherTest +import com.instructure.teacher.ui.utils.seedData +import dagger.hilt.android.testing.HiltAndroidTest +import org.junit.Test + +@HiltAndroidTest +class LoginE2ETest : TeacherTest() { + override fun displaysPageObjects() = Unit + + override fun enableAndConfigureAccessibilityChecks() = Unit + + @E2E + @Test + @TestMetaData(Priority.MANDATORY, FeatureCategory.LOGIN, TestCategory.E2E) + fun testLoginE2E() { + + Log.d(PREPARATION_TAG, "Seeding data.") + val data = seedData(students = 1, teachers = 2, courses = 1) + val teacher1 = data.teachersList[0] + val teacher2 = data.teachersList[1] + val course = data.coursesList[0] + + Log.d(STEP_TAG,"Click 'Find My School' button.") + loginLandingPage.clickFindMySchoolButton() + + Log.d(STEP_TAG,"Enter domain: ${teacher1.domain}.") + loginFindSchoolPage.enterDomain(teacher1.domain) + + Log.d(STEP_TAG,"Click on 'Next' button on the Toolbar.") + loginFindSchoolPage.clickToolbarNextMenuItem() + + Log.d(STEP_TAG,"Login with user: ${teacher1.name}, login id: ${teacher1.loginId} , password: ${teacher1.password}") + loginSignInPage.loginAs(teacher1) + + Log.d(STEP_TAG,"Assert that the Dashboard Page is the landing page and it is loaded successfully.") + verifyDashboardPage(teacher1) + + Log.d(STEP_TAG,"Validate ${teacher1.name} user's role as a Teacher.") + validateUserRole(teacher1, course, "Teacher") + + Log.d(STEP_TAG,"Log out with ${teacher1.name} student.") + dashboardPage.logOut() + + Log.d(STEP_TAG,"Click 'Find My School' button.") + loginLandingPage.clickFindMySchoolButton() + + Log.d(STEP_TAG,"Enter domain: ${teacher2.domain}.") + loginFindSchoolPage.enterDomain(teacher2.domain) + + Log.d(STEP_TAG,"Click on 'Next' button on the Toolbar.") + loginFindSchoolPage.clickToolbarNextMenuItem() + + Log.d(STEP_TAG,"Login with user: ${teacher2.name}, login id: ${teacher2.loginId} , password: ${teacher2.password}") + loginSignInPage.loginAs(teacher2) + + Log.d(STEP_TAG,"Assert that the Dashboard Page is the landing page and it is loaded successfully.") + verifyDashboardPage(teacher2) + + Log.d(STEP_TAG,"Click on 'Change User' button on the left-side menu.") + dashboardPage.pressChangeUser() + + Log.d(STEP_TAG,"Assert that the previously logins has been displayed.") + loginLandingPage.assertDisplaysPreviousLogins() + + Log.d(STEP_TAG,"Login MANUALLY. Click 'Find My School' button.") + loginLandingPage.clickFindMySchoolButton() + + Log.d(STEP_TAG,"Enter domain: ${teacher1.domain}.") + loginFindSchoolPage.enterDomain(teacher1.domain) + + Log.d(STEP_TAG,"Click on 'Next' button on the Toolbar.") + loginFindSchoolPage.clickToolbarNextMenuItem() + + Log.d(STEP_TAG,"Login with user: ${teacher1.name}, login id: ${teacher1.loginId} , password: ${teacher1.password}") + loginSignInPage.loginAs(teacher1) + + Log.d(STEP_TAG,"Assert that the Dashboard Page is the landing page and it is loaded successfully.") + verifyDashboardPage(teacher1) + + Log.d(STEP_TAG,"Click on 'Change User' button on the left-side menu.") + dashboardPage.pressChangeUser() + + Log.d(STEP_TAG,"Assert that the previously logins has been displayed.") + loginLandingPage.assertDisplaysPreviousLogins() + + Log.d(STEP_TAG,"Login with the previous user, ${teacher2.name}, with one click, by clicking on the user's name on the bottom.") + loginLandingPage.loginWithPreviousUser(teacher2) + + Log.d(STEP_TAG,"Assert that the Dashboard Page is the landing page and it is loaded successfully.") + verifyDashboardPage(teacher2) + } + + @E2E + @Test + @TestMetaData(Priority.MANDATORY, FeatureCategory.LOGIN, TestCategory.E2E) + fun testLoginWithNotTeacherRole() { + + Log.d(PREPARATION_TAG, "Seeding data.") + val data = seedData(students = 1, teachers = 1, courses = 1) + val parentData = SeedApi.seedParentData( + SeedApi.SeedParentDataRequest( + courses=1, students=1, parents=1 + ) + ) + val student = data.studentsList[0] + val parent = parentData.parentsList[0] + + Log.d(STEP_TAG,"Click 'Find My School' button.") + loginLandingPage.clickFindMySchoolButton() + + Log.d(STEP_TAG,"Enter domain: ${parent.domain}.") + loginFindSchoolPage.enterDomain(parent.domain) + + Log.d(STEP_TAG,"Enter domain: ${parent.domain}.") + loginFindSchoolPage.clickToolbarNextMenuItem() + + Log.d(STEP_TAG,"Login with user: ${student.name}, login id: ${student.loginId} , password: ${student.password}") + loginSignInPage.loginAs(student) + + Log.d(STEP_TAG,"Assert that the user has been landed on 'Not a teacher?' Page.") + notATeacherPage.assertPageObjects() + + Log.d(STEP_TAG,"Navigate back to the Teacher app's Login Landing Page's screen.") + notATeacherPage.clickOnLoginButton() + + Log.d(STEP_TAG,"Assert the Teacher app's Login Landing Page's screen is displayed.") + loginLandingPage.assertPageObjects() + + Log.d(STEP_TAG,"Click 'Find My School' button.") + loginLandingPage.clickFindMySchoolButton() + + Log.d(STEP_TAG,"Enter domain: ${parent.domain}.") + loginFindSchoolPage.enterDomain(parent.domain) + + Log.d(STEP_TAG,"Enter domain: ${parent.domain}.") + loginFindSchoolPage.clickToolbarNextMenuItem() + + Log.d(STEP_TAG,"Assert that the Login page has been displayed.") + loginSignInPage.assertPageObjects() + + Log.d(STEP_TAG,"Login with user: ${parent.name}, login id: ${parent.loginId} , password: ${parent.password}") + loginSignInPage.loginAs(parent) + + Log.d(STEP_TAG,"Assert that the user has been landed on 'Not a teacher?' Page.") + notATeacherPage.assertPageObjects() + + Log.d(STEP_TAG,"Navigate back to the Teacher app's login screen.") + notATeacherPage.clickOnLoginButton() + + Log.d(STEP_TAG,"Assert that the user has landed on Teacher app's Login Landing Page's screen.") + loginLandingPage.assertPageObjects() + } + + private fun validateUserRole(user: CanvasUserApiModel, course: CourseApiModel, role: String) { + + Log.d(STEP_TAG,"Navigate to 'People' Page of ${course.name} course.") + dashboardPage.selectCourse(course) + courseBrowserPage.openPeopleTab() + + Log.d(STEP_TAG,"Assert that ${user.name} user's role is $role.") + peopleListPage.assertPersonListed(user, role) + + Log.d(STEP_TAG,"Navigate back to Dashboard Page.") + ViewUtils.pressBackButton(2) + } + + private fun verifyDashboardPage(user: CanvasUserApiModel) + { + dashboardPage.waitForRender() + dashboardPage.assertUserLoggedIn(user) + dashboardPage.assertDisplaysCourses() + dashboardPage.assertPageObjects() + } +} \ No newline at end of file diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/DashboardPage.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/DashboardPage.kt index 49176acd6a..4b3cc0e4de 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/DashboardPage.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/DashboardPage.kt @@ -17,9 +17,14 @@ package com.instructure.teacher.ui.pages import android.view.View +import androidx.test.espresso.Espresso import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.matcher.ViewMatchers import androidx.test.espresso.matcher.ViewMatchers.isDisplayed import androidx.test.espresso.matcher.ViewMatchers.withContentDescription +import com.instructure.canvas.espresso.waitForMatcherWithSleeps +import com.instructure.canvasapi2.models.User +import com.instructure.dataseeding.model.CanvasUserApiModel import com.instructure.dataseeding.model.CourseApiModel import com.instructure.espresso.* import com.instructure.espresso.page.* @@ -41,6 +46,7 @@ class DashboardPage : BasePage() { private val coursesTab by WaitForViewWithId(R.id.tab_courses) private val todoTab by WaitForViewWithId(R.id.tab_todo) private val inboxTab by WaitForViewWithId(R.id.tab_inbox) + private val previousLoginTitleText by OnViewWithId(R.id.previousLoginTitleText, autoAssert = false) private val hamburgerButtonMatcher = allOf(withContentDescription(R.string.navigation_drawer_open), isDisplayed()) @@ -108,4 +114,42 @@ class DashboardPage : BasePage() { fun assertCourseLabelTextColor(expectedTextColor: String) { onView(withId(R.id.courseLabel)).check(TextViewColorAssertion(expectedTextColor)) } + + fun logOut() { + onView(hamburgerButtonMatcher).click() + onViewWithId(R.id.navigationDrawerItem_logout).scrollTo().click() + onViewWithText(android.R.string.yes).click() + // It can potentially take a long time for the sign-out to take effect, especially on + // slow FTL devices. So let's pause for a bit until we see the canvas logo. + waitForMatcherWithSleeps(ViewMatchers.withId(R.id.canvasLogo), 10000).check(matches(isDisplayed())) + } + + fun assertUserLoggedIn(user: CanvasUserApiModel) { + onView(hamburgerButtonMatcher).click() + onViewWithText(user.shortName).assertDisplayed() + Espresso.pressBack() + } + + fun assertUserLoggedIn(user: User) { + onView(hamburgerButtonMatcher).click() + onViewWithText(user.shortName!!).assertDisplayed() + Espresso.pressBack() + } + + fun assertUserLoggedIn(userName: String) { + onView(hamburgerButtonMatcher).click() + onViewWithText(userName).assertDisplayed() + Espresso.pressBack() + } + + fun pressChangeUser() { + onView(hamburgerButtonMatcher).click() + onViewWithId(R.id.navigationDrawerItem_changeUser).scrollTo().click() + } + + fun selectCourse(course: CourseApiModel) { + assertDisplaysCourse(course) + onView(withText(course.name)).click() + } + } \ No newline at end of file diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/LoginLandingPage.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/LoginLandingPage.kt index e37cf0b0f4..c5ff9e17a5 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/LoginLandingPage.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/LoginLandingPage.kt @@ -1,9 +1,12 @@ package com.instructure.teacher.ui.pages +import com.instructure.canvasapi2.models.User +import com.instructure.dataseeding.model.CanvasUserApiModel import com.instructure.espresso.OnViewWithId import com.instructure.espresso.assertDisplayed import com.instructure.espresso.click import com.instructure.espresso.page.BasePage +import com.instructure.espresso.page.onViewWithText import com.instructure.teacher.R @Suppress("unused") @@ -34,4 +37,16 @@ class LoginLandingPage : BasePage() { fun assertDisplaysAppDescriptionType() { appDescriptionTypeTextView.assertDisplayed() } + + fun assertDisplaysPreviousLogins() { + previousLoginTitleText.assertDisplayed() + } + + fun loginWithPreviousUser(previousUser: CanvasUserApiModel) { + onViewWithText(previousUser.name).click() + } + + fun loginWithPreviousUser(previousUser: User) { + onViewWithText(previousUser.name).click() + } } diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/NotATeacherPage.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/NotATeacherPage.kt index c4461b6619..65bd64e4f2 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/NotATeacherPage.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/NotATeacherPage.kt @@ -16,13 +16,20 @@ package com.instructure.teacher.ui.pages import com.instructure.espresso.WaitForViewWithId +import com.instructure.espresso.click import com.instructure.espresso.page.BasePage import com.instructure.teacher.R class NotATeacherPage : BasePage() { private val notATeacherTitle by WaitForViewWithId(R.id.not_a_teacher_header, autoAssert = true) - private val explanation by WaitForViewWithId(R.id.explanation) + private val explanation by WaitForViewWithId(R.id.explanation, autoAssert = true) private val studentLink by WaitForViewWithId(R.id.studentLink) private val parentLink by WaitForViewWithId(R.id.parentLink) + private val loginButton by WaitForViewWithId(R.id.login) + + + fun clickOnLoginButton() { + loginButton.click() + } } diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/PeopleListPage.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/PeopleListPage.kt index 28c8b78644..d135d1367c 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/PeopleListPage.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/PeopleListPage.kt @@ -14,11 +14,21 @@ * limitations under the License. */ package com.instructure.teacher.ui.pages +import android.view.View +import androidx.recyclerview.widget.RecyclerView +import androidx.test.espresso.Espresso +import androidx.test.espresso.contrib.RecyclerViewActions +import androidx.test.espresso.matcher.ViewMatchers import com.instructure.dataseeding.model.CanvasUserApiModel +import com.instructure.espresso.assertDisplayed import com.instructure.espresso.click import com.instructure.espresso.page.BasePage import com.instructure.espresso.page.waitForViewWithText +import com.instructure.espresso.page.withAncestor +import com.instructure.espresso.page.withId import com.instructure.teacher.R +import org.hamcrest.Matcher +import org.hamcrest.Matchers class PeopleListPage : BasePage(R.id.peopleListPage) { @@ -26,4 +36,43 @@ class PeopleListPage : BasePage(R.id.peopleListPage) { waitForViewWithText(user.name).click() } + fun assertPersonListed(person: CanvasUserApiModel, role: String? = null) + { + var matcher : Matcher? = null + if(role == null) { + matcher = Matchers.allOf(ViewMatchers.withText(person.name), withId(R.id.title)) + } + else { + matcher = Matchers.allOf( + ViewMatchers.withText(person.name), + withId(R.id.userName), + ViewMatchers.hasSibling( + Matchers.allOf( + withId(R.id.userRole), + ViewMatchers.withText(role) + ) + + ) + ) + } + scrollToMatch(matcher) + Espresso.onView(matcher).assertDisplayed() + } + + + private fun scrollToMatch(matcher: Matcher) { + Espresso.onView( + Matchers.allOf( + withId(R.id.recyclerView), + ViewMatchers.isDisplayed(), + withAncestor(R.id.peopleListPage) + ) + ) + .perform( + RecyclerViewActions.scrollTo( + ViewMatchers.hasDescendant( + matcher + ) + )) + } } diff --git a/automation/espresso/src/main/kotlin/com/instructure/espresso/ScreenshotTestRule.kt b/automation/espresso/src/main/kotlin/com/instructure/espresso/ScreenshotTestRule.kt index d89c46dc97..aeeb44a087 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/espresso/ScreenshotTestRule.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/espresso/ScreenshotTestRule.kt @@ -18,17 +18,15 @@ package com.instructure.espresso -import androidx.test.platform.app.InstrumentationRegistry import androidx.test.espresso.Espresso import androidx.test.espresso.base.DefaultFailureHandler +import androidx.test.platform.app.InstrumentationRegistry import com.instructure.espresso.matchers.WaitForCheckMatcher import com.instructure.espresso.matchers.WaitForViewMatcher - import org.junit.rules.TestRule import org.junit.runner.Description import org.junit.runners.model.Statement - -import java.util.concurrent.atomic.AtomicBoolean +import java.util.concurrent.atomic.* class ScreenshotTestRule : TestRule { From b629be28a5e5cecf1891543033ee41414628f573 Mon Sep 17 00:00:00 2001 From: Kristof Deak <92309696+kdeakinstructure@users.noreply.github.com> Date: Mon, 25 Jul 2022 13:36:50 +0200 Subject: [PATCH 06/49] [MBL-14127][Student] - Rewrite conferences e2e from webview to native (#1648) * Major refactor (aka. re-write) on Student Conferences E2E test. Adding new pages and filling up skeleton classes Add id to a layout in fragment_conference_details.xml Create Conferences API (there is no documentation for it) Update android vault refs: MBL-14127 affects: Student release note: none * format + add comment on api wrapper class. refs: MBL-14127 affects: Student release note: none * copyright year refresh. change duration type from double to int. fix some typo remove nullable from long_running parameter refs: MBL-14127 affects: Student release note: none * refactor api models. refs: MBL-14127 affects: Student release note: none * Remove unused (GET) function from ConferencesApi. refs: MBL-14127 affects: Student release note: none --- .../student/ui/e2e/ConferencesE2ETest.kt | 47 ++++++++++++++----- .../student/ui/pages/ConferenceDetailsPage.kt | 18 ++++++- .../student/ui/pages/ConferenceListPage.kt | 37 ++++++++++++++- .../student/ui/utils/StudentTest.kt | 2 + .../layout/fragment_conference_details.xml | 3 +- .../dataseeding/api/ConferencesApi.kt | 35 ++++++++++++++ .../model/ConferencesRequestApiModel.kt | 46 ++++++++++++++++++ .../model/ConferencesResponseApiModel.kt | 38 +++++++++++++++ 8 files changed, 211 insertions(+), 15 deletions(-) create mode 100644 automation/dataseedingapi/src/main/kotlin/com/instructure/dataseeding/api/ConferencesApi.kt create mode 100644 automation/dataseedingapi/src/main/kotlin/com/instructure/dataseeding/model/ConferencesRequestApiModel.kt create mode 100644 automation/dataseedingapi/src/main/kotlin/com/instructure/dataseeding/model/ConferencesResponseApiModel.kt diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/ConferencesE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/ConferencesE2ETest.kt index a100993eaf..8beae2e7d6 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/ConferencesE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/ConferencesE2ETest.kt @@ -2,12 +2,12 @@ package com.instructure.student.ui.e2e import android.util.Log import com.instructure.canvas.espresso.E2E -import com.instructure.canvas.espresso.Stub +import com.instructure.canvas.espresso.refresh +import com.instructure.dataseeding.api.ConferencesApi import com.instructure.panda_annotations.FeatureCategory import com.instructure.panda_annotations.Priority import com.instructure.panda_annotations.TestCategory import com.instructure.panda_annotations.TestMetaData -import com.instructure.student.ui.pages.ConferencesPage import com.instructure.student.ui.utils.StudentTest import com.instructure.student.ui.utils.seedData import com.instructure.student.ui.utils.tokenLogin @@ -32,15 +32,15 @@ class ConferencesE2ETest: StudentTest() { // Re-stubbing for now because the interface has changed from webview to native // and this test no longer passes. MBL-14127 is being tracked to re-write this // test against the new native interface. - @Stub @E2E @Test - @TestMetaData(Priority.MANDATORY, FeatureCategory.CONFERENCES, TestCategory.E2E, true) + @TestMetaData(Priority.MANDATORY, FeatureCategory.CONFERENCES, TestCategory.E2E) fun testConferencesE2E() { 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(STEP_TAG,"Login with user: ${student.name}, login id: ${student.loginId} , password: ${student.password}") @@ -51,13 +51,38 @@ class ConferencesE2ETest: StudentTest() { dashboardPage.selectCourse(course) courseBrowserPage.selectConferences() - val title = "Awesome Conference!" - var description = "Awesome! Spectacular! Mind-blowing!" - Log.d(STEP_TAG,"Create a new conference with $title title and $description description.") - ConferencesPage.createConference(title, description) + Log.d(STEP_TAG,"Assert that the empty view is displayed since we did not make any conference yet.") + conferenceListPage.assertEmptyView() + + val testConferenceTitle = "E2E test conference" + val testConferenceDescription = "Nightly E2E Test conference description" + Log.d(PREPARATION_TAG,"Create a conference with '$testConferenceTitle' title and '$testConferenceDescription' description.") + ConferencesApi.createCourseConference(teacher.token, + testConferenceTitle, testConferenceDescription,"BigBlueButton",false,70, + listOf(student.id),course.id) + + val testConferenceTitle2 = "E2E test conference 2" + val testConferenceDescription2 = "Nightly E2E Test conference description 2" + ConferencesApi.createCourseConference(teacher.token, + testConferenceTitle2, testConferenceDescription2,"BigBlueButton",true,120, + listOf(student.id),course.id) + + Log.d(STEP_TAG,"Refresh the page. Assert that $testConferenceTitle conference is displayed on the Conference List Page with the corresponding status.") + refresh() + conferenceListPage.assertConferenceDisplayed(testConferenceTitle) + conferenceListPage.assertConferenceStatus(testConferenceTitle,"Not Started") + + Log.d(STEP_TAG,"Assert that $testConferenceTitle2 conference is displayed on the Conference List Page with the corresponding status.") + conferenceListPage.assertConferenceDisplayed(testConferenceTitle2) + conferenceListPage.assertConferenceStatus(testConferenceTitle2,"Not Started") + + Log.d(STEP_TAG,"Open '$testConferenceTitle' conference details page.") + conferenceListPage.openConferenceDetails(testConferenceTitle) + + Log.d(STEP_TAG,"Assert that the proper conference title '$testConferenceTitle', status and description '$testConferenceDescription' are displayed.") + conferenceDetailsPage.assertConferenceTitleDisplayed() + conferenceDetailsPage.assertConferenceStatus("Not Started") + conferenceDetailsPage.assertDescription(testConferenceDescription) - Log.d(STEP_TAG,"Assert that the previously created conference is displayed with $title title and $description description.") - ConferencesPage.assertConferenceTitlePresent(title) - ConferencesPage.assertConferenceDescriptionPresent(description) } } \ No newline at end of file diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/ConferenceDetailsPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/ConferenceDetailsPage.kt index 000c33630b..556eee1ea6 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/ConferenceDetailsPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/ConferenceDetailsPage.kt @@ -16,9 +16,23 @@ */ package com.instructure.student.ui.pages -import com.instructure.espresso.page.BasePage +import androidx.test.espresso.matcher.ViewMatchers.hasSibling +import com.instructure.espresso.assertDisplayed +import com.instructure.espresso.page.* import com.instructure.student.R +import org.hamcrest.CoreMatchers.allOf open class ConferenceDetailsPage : BasePage(R.id.conferenceDetailsPage) { - // For future use + + fun assertConferenceTitleDisplayed() { + onView(allOf(withId(R.id.title), hasSibling(withId(R.id.statusDetails)))).assertDisplayed() + } + + fun assertConferenceStatus(expectedStatus: String) { + onView(allOf(withId(R.id.status), withText(expectedStatus), withParent(R.id.statusDetails))).assertDisplayed() + } + + fun assertDescription(expectedDescription: String) { + onView(allOf(withId(R.id.description) + withText(expectedDescription), hasSibling(withId(R.id.statusDetails)))).assertDisplayed() + } } diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/ConferenceListPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/ConferenceListPage.kt index 976da7af61..e41269af5c 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/ConferenceListPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/ConferenceListPage.kt @@ -16,9 +16,44 @@ */ package com.instructure.student.ui.pages +import androidx.test.espresso.assertion.ViewAssertions.doesNotExist +import androidx.test.espresso.matcher.ViewMatchers.hasSibling +import com.instructure.espresso.assertDisplayed +import com.instructure.espresso.click import com.instructure.espresso.page.BasePage +import com.instructure.espresso.page.onView +import com.instructure.espresso.page.withId +import com.instructure.espresso.page.withText import com.instructure.student.R +import org.hamcrest.CoreMatchers.allOf open class ConferenceListPage : BasePage(R.id.conferenceListPage) { - // For future use + + fun assertEmptyView() { + onView(withId(R.id.conferenceListEmptyView)).assertDisplayed() + onView(allOf(withId(R.id.emptyTitle), withText(R.string.noConferencesTitle))).assertDisplayed() + onView(allOf(withId(R.id.emptyMessage), withText(R.string.noConferencesMessage))).assertDisplayed() + + } + + fun assertConferenceStatus(conferenceTitle: String, expectedStatus: String) { + onView(allOf(withId(R.id.statusLabel), withText(expectedStatus), hasSibling(allOf(withId(R.id.title), withText(conferenceTitle))))) + } + + fun assertConferenceDisplayed(conferenceTitle: String) { + onView(allOf(withId(R.id.title), withText(conferenceTitle))).assertDisplayed() + } + + fun clickOnOpenExternallyButton() { + onView(withId(R.id.openExternallyButton)).click() + } + + fun assertOpenExternallyButtonNotDisplayed() { + onView(withId(R.id.openExternallyButton)).check(doesNotExist()) + } + + fun openConferenceDetails(conferenceTitle: String) { + onView(withText(conferenceTitle)).click() + } + } diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/utils/StudentTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/utils/StudentTest.kt index 1ca87195e4..d37fe8b4e8 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/utils/StudentTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/utils/StudentTest.kt @@ -68,6 +68,8 @@ abstract class StudentTest : CanvasTest() { val calendarEventPage = CalendarEventPage() val canvasWebViewPage = CanvasWebViewPage() val courseBrowserPage = CourseBrowserPage() + val conferenceListPage = ConferenceListPage() + val conferenceDetailsPage = ConferenceDetailsPage() val elementaryCoursePage = ElementaryCoursePage() val courseGradesPage = CourseGradesPage() val dashboardPage = DashboardPage() diff --git a/apps/student/src/main/res/layout/fragment_conference_details.xml b/apps/student/src/main/res/layout/fragment_conference_details.xml index adccb103bb..ffad413cc6 100644 --- a/apps/student/src/main/res/layout/fragment_conference_details.xml +++ b/apps/student/src/main/res/layout/fragment_conference_details.xml @@ -62,7 +62,8 @@ + android:orientation="horizontal" + android:id="@+id/statusDetails"> + } + + private fun conferencesService(token: String): ConferencesService + = CanvasNetworkAdapter.retrofitWithToken(token).create(ConferencesService::class.java) + + fun createCourseConference(token: String, title: String, description: String, conferenceType: String, longRunning: Boolean, duration: Int, userIds: List, courseId: Long): ConferencesResponseApiModel { + val conference = WebConferenceWrapper(webConference = ConferencesRequestApiModel( + title, + description, + conferenceType, + longRunning, + duration, + userIds) + ) + + return conferencesService(token).createCourseConference(courseId, conference).execute().body()!! + } + +} diff --git a/automation/dataseedingapi/src/main/kotlin/com/instructure/dataseeding/model/ConferencesRequestApiModel.kt b/automation/dataseedingapi/src/main/kotlin/com/instructure/dataseeding/model/ConferencesRequestApiModel.kt new file mode 100644 index 0000000000..f76dea60af --- /dev/null +++ b/automation/dataseedingapi/src/main/kotlin/com/instructure/dataseeding/model/ConferencesRequestApiModel.kt @@ -0,0 +1,46 @@ +// +// Copyright (C) 2022-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.dataseeding.model + +import com.google.gson.annotations.SerializedName + +/** + * Used to create conferences. + */ +data class ConferencesRequestApiModel( + @SerializedName("title") + val title: String = "", + @SerializedName("description") + val description: String? = null, + @SerializedName("conference_type") + val conferenceType: String = "", + @SerializedName("long_running") + val longRunning: Boolean = false, + @SerializedName("duration") + val duration: Int = 60, + @SerializedName("users") + val userIds: List? = null +) + +/** + * Wrapper class above ConferencesRequestApiModel because it is wrapped within a 'web_conference' object in the request. + */ +data class WebConferenceWrapper( + @SerializedName("web_conference") + val webConference: ConferencesRequestApiModel +) \ No newline at end of file diff --git a/automation/dataseedingapi/src/main/kotlin/com/instructure/dataseeding/model/ConferencesResponseApiModel.kt b/automation/dataseedingapi/src/main/kotlin/com/instructure/dataseeding/model/ConferencesResponseApiModel.kt new file mode 100644 index 0000000000..945f87bdd5 --- /dev/null +++ b/automation/dataseedingapi/src/main/kotlin/com/instructure/dataseeding/model/ConferencesResponseApiModel.kt @@ -0,0 +1,38 @@ +// +// Copyright (C) 2022-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.dataseeding.model + +import com.google.gson.annotations.SerializedName + +/** + * Used to get conferences. + */ +data class ConferencesResponseApiModel( + @SerializedName("id") + val id: Long, + @SerializedName("description") + val description: String = "", + @SerializedName("conference_type") + val conferenceType: String = "", + @SerializedName("long_running") + val longRunning: Int? = null, + @SerializedName("duration") + val duration: Int, + @SerializedName("user_ids") + val userIds: List = listOf() +) \ No newline at end of file From dca89da1df650a8b6b74476ded3128386d8fec99 Mon Sep 17 00:00:00 2001 From: Kristof Nemere <109959688+kristofnemere@users.noreply.github.com> Date: Fri, 29 Jul 2022 15:56:31 +0200 Subject: [PATCH 07/49] [MBL-16175][Teacher][Student] Validation is missing from image Alt dialog (#1665) refs: MBL-16175 affects: Student, Teacher release note: none * Added text validation to alt dialog * Fixed initial button state --- .../instructure/rceditor/RCETextEditorView.kt | 30 ++++++++++++++----- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/libs/rceditor/src/main/java/instructure/rceditor/RCETextEditorView.kt b/libs/rceditor/src/main/java/instructure/rceditor/RCETextEditorView.kt index 59017b7a79..b086ca8c15 100644 --- a/libs/rceditor/src/main/java/instructure/rceditor/RCETextEditorView.kt +++ b/libs/rceditor/src/main/java/instructure/rceditor/RCETextEditorView.kt @@ -26,11 +26,8 @@ import android.content.res.Resources import android.graphics.Color import android.os.Parcel import android.os.Parcelable -import androidx.annotation.ColorInt -import androidx.annotation.StringRes -import androidx.fragment.app.FragmentActivity -import androidx.core.content.ContextCompat -import androidx.appcompat.app.AlertDialog +import android.text.Editable +import android.text.TextWatcher import android.util.AttributeSet import android.view.View import android.view.View.OnFocusChangeListener @@ -40,6 +37,11 @@ import android.webkit.URLUtil import android.widget.ImageButton import android.widget.RelativeLayout import android.widget.TextView +import androidx.annotation.ColorInt +import androidx.annotation.StringRes +import androidx.appcompat.app.AlertDialog +import androidx.core.content.ContextCompat +import androidx.fragment.app.FragmentActivity 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.* @@ -222,16 +224,28 @@ class RCETextEditorView @JvmOverloads constructor( buttonClicked = true onPositiveClick(altTextInput?.text.toString()) } - .setNegativeButton(activity.getString(android.R.string.cancel), { _, _ -> + .setNegativeButton(activity.getString(android.R.string.cancel)) { _, _ -> buttonClicked = true onNegativeClick() - }) + } .setOnDismissListener { if (!buttonClicked) { onNegativeClick() } } - .create() + .create().apply { + setOnShowListener { + (it as? AlertDialog)?.getButton(AlertDialog.BUTTON_POSITIVE)?.isEnabled = false + } + } + + altTextInput?.addTextChangedListener(object : TextWatcher { + override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {} + override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {} + override fun afterTextChanged(s: Editable?) { + altTextDialog.getButton(AlertDialog.BUTTON_POSITIVE).isEnabled = !s.isNullOrEmpty() + } + }) altTextDialog.show() } From 3e0e7c93a6cd2fd9c6b3f0bfc543a87a4ab3768d Mon Sep 17 00:00:00 2001 From: Tamas Kozmer <72397075+tamaskozmer@users.noreply.github.com> Date: Mon, 1 Aug 2022 11:52:45 +0200 Subject: [PATCH 08/49] [MBL-15351][Student][Teacher] Email notification preferences (#1663) refs: MBL-15351 affects: Student, Teacher release note: Added email notification preferences to the settings. * Extracted common code from push notification settings. * Added email notification preferences request and ui. * Added strings to API string mappings and refactoring. * Added change action and redesign. * Fixed push notification unit tests. * Added unit tests. * Minor refactoring. --- .../student/activity/NavigationActivity.kt | 9 +- .../fragment/ApplicationSettingsFragment.kt | 9 +- .../student/router/RouteResolver.kt | 6 +- .../layout/fragment_application_settings.xml | 12 + .../teacher/fragments/SettingsFragment.kt | 6 +- .../teacher/router/RouteResolver.kt | 9 +- .../src/main/res/layout/fragment_settings.xml | 12 + .../apis/NotificationPreferencesAPI.kt | 2 +- .../instructure/canvasapi2/di/ApiModule.kt | 3 +- .../NotificationPreferencesManager.kt | 21 +- libs/pandares/src/main/res/values/strings.xml | 7 + .../EmailNotificationPreferencesFragment.kt | 93 +++++ .../EmailNotificationPreferencesViewModel.kt | 81 ++++ .../NotificationPreferencesViewData.kt | 7 +- .../NotificationPreferencesViewModel.kt | 62 +-- ...=> PushNotificationPreferencesFragment.kt} | 14 +- .../PushNotificationPreferencesViewModel.kt | 84 +++++ .../EmailNotificationCategoryItemViewModel.kt | 42 +++ ...NotificationCategoryHeaderItemViewModel.kt | 1 - .../NotificationCategoryItemViewModel.kt | 41 +- .../PushNotificationCategoryItemViewModel.kt | 42 +++ ... => fragment_notification_preferences.xml} | 8 +- .../item_email_notification_preference.xml | 65 ++++ .../res/layout/item_notification_header.xml | 11 +- ... => item_push_notification_preference.xml} | 2 +- .../EmailNotificationSettingsViewModelTest.kt | 355 ++++++++++++++++++ ...shNotificationPreferencesViewModelTest.kt} | 20 +- 27 files changed, 900 insertions(+), 124 deletions(-) create mode 100644 libs/pandautils/src/main/java/com/instructure/pandautils/features/notification/preferences/EmailNotificationPreferencesFragment.kt create mode 100644 libs/pandautils/src/main/java/com/instructure/pandautils/features/notification/preferences/EmailNotificationPreferencesViewModel.kt rename libs/pandautils/src/main/java/com/instructure/pandautils/features/notification/preferences/{NotificationPreferencesFragment.kt => PushNotificationPreferencesFragment.kt} (81%) create mode 100644 libs/pandautils/src/main/java/com/instructure/pandautils/features/notification/preferences/PushNotificationPreferencesViewModel.kt create mode 100644 libs/pandautils/src/main/java/com/instructure/pandautils/features/notification/preferences/itemviewmodels/EmailNotificationCategoryItemViewModel.kt create mode 100644 libs/pandautils/src/main/java/com/instructure/pandautils/features/notification/preferences/itemviewmodels/PushNotificationCategoryItemViewModel.kt rename libs/pandautils/src/main/res/layout/{fragment_push_preferences.xml => fragment_notification_preferences.xml} (93%) create mode 100644 libs/pandautils/src/main/res/layout/item_email_notification_preference.xml rename libs/pandautils/src/main/res/layout/{item_notification_preference.xml => item_push_notification_preference.xml} (98%) create mode 100644 libs/pandautils/src/test/java/com/instructure/pandautils/features/notification/preferences/EmailNotificationSettingsViewModelTest.kt rename libs/pandautils/src/test/java/com/instructure/pandautils/features/notification/preferences/{NotificationPreferencesViewModelTest.kt => PushNotificationPreferencesViewModelTest.kt} (94%) 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 768f7c9380..a76f11f695 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 @@ -26,7 +26,6 @@ import android.graphics.Color import android.graphics.Typeface import android.os.Bundle import android.os.Handler -import android.view.LayoutInflater import android.view.MenuItem import android.view.View import android.view.ViewGroup @@ -48,8 +47,6 @@ import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentManager import com.airbnb.lottie.LottieAnimationView import com.bumptech.glide.Glide -import com.google.android.material.bottomnavigation.BottomNavigationItemView -import com.google.android.material.bottomnavigation.BottomNavigationMenuView import com.google.android.material.bottomnavigation.BottomNavigationView import com.instructure.canvasapi2.CanvasRestAdapter import com.instructure.canvasapi2.managers.CourseManager @@ -70,7 +67,7 @@ import com.instructure.loginapi.login.dialog.MasqueradingDialog import com.instructure.loginapi.login.tasks.LogoutTask import com.instructure.pandautils.dialogs.UploadFilesDialog import com.instructure.pandautils.features.help.HelpDialogFragment -import com.instructure.pandautils.features.notification.preferences.NotificationPreferencesFragment +import com.instructure.pandautils.features.notification.preferences.PushNotificationPreferencesFragment import com.instructure.pandautils.features.themeselector.ThemeSelectorBottomSheet import com.instructure.pandautils.models.PushNotification import com.instructure.pandautils.receivers.PushExternalReceiver @@ -751,8 +748,8 @@ class NavigationActivity : BaseRouterActivity(), Navigation, MasqueradingDialog. } } RouteContext.NOTIFICATION_PREFERENCES == route.routeContext -> { - Analytics.trackAppFlow(this@NavigationActivity, NotificationPreferencesFragment::class.java) - RouteMatcher.route(this@NavigationActivity, Route(NotificationPreferencesFragment::class.java, null)) + Analytics.trackAppFlow(this@NavigationActivity, PushNotificationPreferencesFragment::class.java) + RouteMatcher.route(this@NavigationActivity, Route(PushNotificationPreferencesFragment::class.java, null)) } else -> { //fetch the CanvasContext 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 5e9131f1f8..8279d05085 100644 --- a/apps/student/src/main/java/com/instructure/student/fragment/ApplicationSettingsFragment.kt +++ b/apps/student/src/main/java/com/instructure/student/fragment/ApplicationSettingsFragment.kt @@ -28,7 +28,8 @@ import com.instructure.canvasapi2.utils.pageview.PageView import com.instructure.loginapi.login.dialog.NoInternetConnectionDialog import com.instructure.pandautils.analytics.SCREEN_VIEW_APPLICATION_SETTINGS import com.instructure.pandautils.analytics.ScreenView -import com.instructure.pandautils.features.notification.preferences.NotificationPreferencesFragment +import com.instructure.pandautils.features.notification.preferences.EmailNotificationPreferencesFragment +import com.instructure.pandautils.features.notification.preferences.PushNotificationPreferencesFragment import com.instructure.pandautils.fragments.RemoteConfigParamsFragment import com.instructure.pandautils.utils.* import com.instructure.student.BuildConfig @@ -94,7 +95,11 @@ class ApplicationSettingsFragment : ParentFragment() { } pushNotifications.onClick { - addFragment(NotificationPreferencesFragment.newInstance()) + addFragment(PushNotificationPreferencesFragment.newInstance()) + } + + emailNotifications.onClick { + addFragment(EmailNotificationPreferencesFragment.newInstance()) } about.onClick { diff --git a/apps/student/src/main/java/com/instructure/student/router/RouteResolver.kt b/apps/student/src/main/java/com/instructure/student/router/RouteResolver.kt index 0fabd50ace..360efc5f8a 100644 --- a/apps/student/src/main/java/com/instructure/student/router/RouteResolver.kt +++ b/apps/student/src/main/java/com/instructure/student/router/RouteResolver.kt @@ -4,7 +4,8 @@ import androidx.fragment.app.Fragment import com.instructure.canvasapi2.models.CanvasContext import com.instructure.interactions.router.Route import com.instructure.pandautils.features.discussion.details.DiscussionDetailsWebViewFragment -import com.instructure.pandautils.features.notification.preferences.NotificationPreferencesFragment +import com.instructure.pandautils.features.notification.preferences.EmailNotificationPreferencesFragment +import com.instructure.pandautils.features.notification.preferences.PushNotificationPreferencesFragment import com.instructure.pandautils.utils.Const import com.instructure.student.AnnotationComments.AnnotationCommentListFragment import com.instructure.student.activity.NothingToSeeHereFragment @@ -121,7 +122,8 @@ object RouteResolver { cls.isA() -> AnnotationCommentListFragment.newInstance(route) cls.isA() -> NothingToSeeHereFragment.newInstance() cls.isA() -> AnnotationSubmissionUploadFragment.newInstance(route) - cls.isA() -> NotificationPreferencesFragment.newInstance() + cls.isA() -> PushNotificationPreferencesFragment.newInstance() + cls.isA() -> EmailNotificationPreferencesFragment.newInstance() cls.isA() -> DiscussionDetailsWebViewFragment.newInstance(route) cls.isA() -> InternalWebviewFragment.newInstance(route) // Keep this at the end else -> null diff --git a/apps/student/src/main/res/layout/fragment_application_settings.xml b/apps/student/src/main/res/layout/fragment_application_settings.xml index c6f446c829..88462a8078 100644 --- a/apps/student/src/main/res/layout/fragment_application_settings.xml +++ b/apps/student/src/main/res/layout/fragment_application_settings.xml @@ -162,6 +162,18 @@ android:text="@string/pushNotifications" android:textSize="16sp"/> + + + + { updatePreferenceCategory(categoryName, channelId, frequency, it) } +} +enum class NotificationPreferencesFrequency(val apiString: String, @StringRes val stringRes: Int) { + IMMEDIATELY("immediately", R.string.emailNotificationsImmediately), + DAILY("daily", R.string.emailNotificationsDaily), + WEEKLY("weekly", R.string.emailNotificationsWeekly), + NEVER("never", R.string.emailNotificationsNever); + + companion object { + fun fromApiString(apiString: String) = values().find { apiString == it.apiString } ?: IMMEDIATELY + } } diff --git a/libs/pandares/src/main/res/values/strings.xml b/libs/pandares/src/main/res/values/strings.xml index f38b138119..ec14442ee5 100644 --- a/libs/pandares/src/main/res/values/strings.xml +++ b/libs/pandares/src/main/res/values/strings.xml @@ -1283,4 +1283,11 @@ No annotation selected + + Email Notifications + Immediately + Daily + Weekly + Never + Select frequency diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/notification/preferences/EmailNotificationPreferencesFragment.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/notification/preferences/EmailNotificationPreferencesFragment.kt new file mode 100644 index 0000000000..71b0c8cff1 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/notification/preferences/EmailNotificationPreferencesFragment.kt @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2022 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.pandautils.features.notification.preferences + +import android.content.DialogInterface +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.appcompat.app.AlertDialog +import androidx.fragment.app.Fragment +import androidx.fragment.app.viewModels +import com.google.android.material.snackbar.Snackbar +import com.instructure.canvasapi2.managers.NotificationPreferencesFrequency +import com.instructure.pandautils.R +import com.instructure.pandautils.databinding.FragmentNotificationPreferencesBinding +import com.instructure.pandautils.utils.ViewStyler +import com.instructure.pandautils.utils.setupAsBackButton +import dagger.hilt.android.AndroidEntryPoint + +@AndroidEntryPoint +class EmailNotificationPreferencesFragment : Fragment() { + + private val viewModel: EmailNotificationPreferencesViewModel by viewModels() + + private lateinit var binding: FragmentNotificationPreferencesBinding + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle?): View { + binding = FragmentNotificationPreferencesBinding.inflate(inflater, container, false) + binding.lifecycleOwner = this.viewLifecycleOwner + binding.viewModel = viewModel + binding.title = resources.getString(R.string.emailNotifications) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + setupToolbar() + + viewModel.events.observe(viewLifecycleOwner) { event -> + event.getContentIfNotHandled()?.let { + handleAction(it) + } + } + } + + private fun handleAction(action: NotificationPreferencesAction) { + when (action) { + is NotificationPreferencesAction.ShowSnackbar -> Snackbar.make(requireView(), action.snackbar, Snackbar.LENGTH_LONG).show() + is NotificationPreferencesAction.ShowFrequencySelectionDialog -> showFrequencySelectionDialog(action.categoryName, action.selectedFrequency) + } + } + + private fun showFrequencySelectionDialog(categoryName: String, selectedFrequency: NotificationPreferencesFrequency) { + val items = NotificationPreferencesFrequency.values().map { resources.getString(it.stringRes) }.toTypedArray() + val selectedIndex = NotificationPreferencesFrequency.values().indexOf(selectedFrequency) + AlertDialog.Builder(requireContext(), R.style.AccentDialogTheme) + .setTitle(R.string.selectFrequency) + .setSingleChoiceItems(items, selectedIndex, { dialog, index -> frequencySelected(dialog, index, categoryName)}) + .setNegativeButton(R.string.sortByDialogCancel) { dialog, _ -> dialog.dismiss() } + .show() + } + + private fun frequencySelected(dialog: DialogInterface, index: Int, categoryName: String) { + val selectedFrequency = NotificationPreferencesFrequency.values()[index] + viewModel.updateFrequency(categoryName, selectedFrequency) + dialog.dismiss() + } + + private fun setupToolbar() { + binding.toolbar.setupAsBackButton { requireActivity().onBackPressed() } + ViewStyler.themeToolbarLight(requireActivity(), binding.toolbar) + } + + companion object { + fun newInstance() = EmailNotificationPreferencesFragment() + } +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/notification/preferences/EmailNotificationPreferencesViewModel.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/notification/preferences/EmailNotificationPreferencesViewModel.kt new file mode 100644 index 0000000000..d9b1dc0b57 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/notification/preferences/EmailNotificationPreferencesViewModel.kt @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2022 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.pandautils.features.notification.preferences + +import android.content.res.Resources +import androidx.lifecycle.viewModelScope +import com.instructure.canvasapi2.managers.CommunicationChannelsManager +import com.instructure.canvasapi2.managers.NotificationPreferencesFrequency +import com.instructure.canvasapi2.managers.NotificationPreferencesManager +import com.instructure.canvasapi2.utils.ApiPrefs +import com.instructure.pandautils.BR +import com.instructure.pandautils.R +import com.instructure.pandautils.features.notification.preferences.itemviewmodels.EmailNotificationCategoryItemViewModel +import com.instructure.pandautils.features.notification.preferences.itemviewmodels.NotificationCategoryItemViewModel +import com.instructure.pandautils.mvvm.Event +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class EmailNotificationPreferencesViewModel @Inject constructor( + communicationChannelsManager: CommunicationChannelsManager, + notificationPreferencesManager: NotificationPreferencesManager, + apiPrefs: ApiPrefs, + notificationPreferenceUtils: NotificationPreferenceUtils, + resources: Resources +): NotificationPreferencesViewModel(communicationChannelsManager, notificationPreferencesManager, apiPrefs, notificationPreferenceUtils, resources) { + + override val notificationChannelType: String + get() = "email" + + override fun createCategoryItemViewModel(viewData: NotificationCategoryViewData): NotificationCategoryItemViewModel { + return EmailNotificationCategoryItemViewModel(viewData, resources, ::notificationCategorySelected) + } + + private fun notificationCategorySelected(categoryName: String, frequency: NotificationPreferencesFrequency) { + _events.postValue(Event(NotificationPreferencesAction.ShowFrequencySelectionDialog(categoryName, frequency))) + } + + fun updateFrequency(categoryName: String, selectedFrequency: NotificationPreferencesFrequency) { + val selectedItem = _data.value?.items?.flatMap { it.itemViewModels }?.find { it.data.categoryName == categoryName } as? EmailNotificationCategoryItemViewModel + if (selectedItem == null) return + + val previousFrequency = selectedItem.data.frequency + updateItemFrequency(selectedItem, selectedFrequency) + + viewModelScope.launch { + try { + val channel = communicationChannel + if (channel != null) { + notificationPreferencesManager.updatePreferenceCategoryAsync(categoryName, channel.id, selectedFrequency.apiString).await().dataOrThrow + } else { + _events.postValue(Event(NotificationPreferencesAction.ShowSnackbar(resources.getString(R.string.errorOccurred)))) + } + } catch (e: Exception) { + e.printStackTrace() + updateItemFrequency(selectedItem, previousFrequency) + _events.postValue(Event(NotificationPreferencesAction.ShowSnackbar(resources.getString(R.string.errorOccurred)))) + } + } + } + + private fun updateItemFrequency(item: NotificationCategoryItemViewModel, frequency: NotificationPreferencesFrequency) { + item.data.frequency = frequency + item.notifyPropertyChanged(BR.frequency) + } +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/notification/preferences/NotificationPreferencesViewData.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/notification/preferences/NotificationPreferencesViewData.kt index 905b0db23f..c64c203a4c 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/notification/preferences/NotificationPreferencesViewData.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/notification/preferences/NotificationPreferencesViewData.kt @@ -16,6 +16,7 @@ package com.instructure.pandautils.features.notification.preferences +import com.instructure.canvasapi2.managers.NotificationPreferencesFrequency import com.instructure.pandautils.features.notification.preferences.itemviewmodels.NotificationCategoryHeaderItemViewModel data class NotificationPreferencesViewData(val items: List) @@ -26,7 +27,7 @@ data class NotificationCategoryViewData( val name: String, val title: String?, val description: String?, - var frequency: String, + var frequency: NotificationPreferencesFrequency, val position: Int, val notification: String? ) { @@ -36,9 +37,11 @@ data class NotificationCategoryViewData( enum class NotificationPreferencesViewType(val viewType: Int) { HEADER(0), - CATEGORY(1) + PUSH_CATEGORY(1), + EMAIL_CATEGORY(2) } sealed class NotificationPreferencesAction { data class ShowSnackbar(val snackbar: String): NotificationPreferencesAction() + data class ShowFrequencySelectionDialog(val categoryName: String, val selectedFrequency: NotificationPreferencesFrequency): NotificationPreferencesAction() } \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/notification/preferences/NotificationPreferencesViewModel.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/notification/preferences/NotificationPreferencesViewModel.kt index 572ea68a1f..31388183c0 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/notification/preferences/NotificationPreferencesViewModel.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/notification/preferences/NotificationPreferencesViewModel.kt @@ -22,27 +22,24 @@ import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.instructure.canvasapi2.managers.CommunicationChannelsManager +import com.instructure.canvasapi2.managers.NotificationPreferencesFrequency import com.instructure.canvasapi2.managers.NotificationPreferencesManager import com.instructure.canvasapi2.models.CommunicationChannel import com.instructure.canvasapi2.models.NotificationPreference import com.instructure.canvasapi2.utils.ApiPrefs -import com.instructure.pandautils.BR import com.instructure.pandautils.R import com.instructure.pandautils.features.notification.preferences.itemviewmodels.NotificationCategoryHeaderItemViewModel import com.instructure.pandautils.features.notification.preferences.itemviewmodels.NotificationCategoryItemViewModel import com.instructure.pandautils.mvvm.Event import com.instructure.pandautils.mvvm.ViewState -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.launch -import javax.inject.Inject -@HiltViewModel -class NotificationPreferencesViewModel @Inject constructor( +abstract class NotificationPreferencesViewModel ( private val communicationChannelsManager: CommunicationChannelsManager, - private val notificationPreferencesManager: NotificationPreferencesManager, + protected val notificationPreferencesManager: NotificationPreferencesManager, private val apiPrefs: ApiPrefs, private val notificationPreferenceUtils: NotificationPreferenceUtils, - private val resources: Resources + protected val resources: Resources ) : ViewModel() { val state: LiveData get() = _state @@ -50,13 +47,15 @@ class NotificationPreferencesViewModel @Inject constructor( val data: LiveData get() = _data - private val _data = MutableLiveData() + protected val _data = MutableLiveData() val events: LiveData> get() = _events - private val _events = MutableLiveData>() + protected val _events = MutableLiveData>() - private var pushChannel: CommunicationChannel? = null + protected var communicationChannel: CommunicationChannel? = null + + abstract val notificationChannelType: String init { _state.postValue(ViewState.Loading) @@ -73,8 +72,8 @@ class NotificationPreferencesViewModel @Inject constructor( try { apiPrefs.user?.let { val communicationChannels = communicationChannelsManager.getCommunicationChannelsAsync(it.id, true).await().dataOrThrow - pushChannel = communicationChannels.first { "push".equals(it.type, true) } - pushChannel?.let { channel -> + communicationChannel = communicationChannels.first { notificationChannelType.equals(it.type, true) } + communicationChannel?.let { channel -> val notificationPreferences = notificationPreferencesManager.getNotificationPreferencesAsync(channel.userId, channel.id, true).await().dataOrThrow val items = groupNotifications(notificationPreferences.notificationPreferences) @@ -106,16 +105,15 @@ class NotificationPreferencesViewModel @Inject constructor( val categoryHelper = categoryHelperMap[categoryName] ?: continue val header = groupHeaderMap[categoryHelper.categoryGroup] ?: continue - val categoryItemViewModel = NotificationCategoryItemViewModel( - data = NotificationCategoryViewData( + val categoryItemViewModel = createCategoryItemViewModel( + NotificationCategoryViewData( categoryName, titleMap[categoryName], descriptionMap[categoryName], - prefs[0].frequency, + NotificationPreferencesFrequency.fromApiString(prefs[0].frequency), categoryHelper.position, prefs[0].notification - ), - toggle = this::toggleNotification + ) ) if (categories[header] == null) { categories[header] = arrayListOf(categoryItemViewModel) @@ -132,33 +130,5 @@ class NotificationPreferencesViewModel @Inject constructor( }.sortedBy { it.data.position } } - private fun toggleNotification(enabled: Boolean, categoryName: String) { - viewModelScope.launch { - try { - pushChannel?.let { - notificationPreferencesManager.updatePreferenceCategoryAsync( - categoryName, - it.id, - enabled.frequency, - ).await().dataOrThrow - } ?: throw IllegalStateException() - } catch (e: Exception) { - e.printStackTrace() - _data.value?.items?.forEach { - val itemViewModel = it.itemViewModels.firstOrNull { it.data.categoryName == categoryName } - itemViewModel?.let { - it.apply { - data.frequency = enabled.not().frequency - notifyPropertyChanged(BR.checked) - } - return@forEach - } - } - _events.postValue(Event(NotificationPreferencesAction.ShowSnackbar(resources.getString(R.string.errorOccurred)))) - } - } - } - - private val Boolean.frequency: String - get() = if (this) NotificationPreferencesManager.IMMEDIATELY else NotificationPreferencesManager.NEVER + abstract fun createCategoryItemViewModel(viewData: NotificationCategoryViewData): NotificationCategoryItemViewModel } \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/notification/preferences/NotificationPreferencesFragment.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/notification/preferences/PushNotificationPreferencesFragment.kt similarity index 81% rename from libs/pandautils/src/main/java/com/instructure/pandautils/features/notification/preferences/NotificationPreferencesFragment.kt rename to libs/pandautils/src/main/java/com/instructure/pandautils/features/notification/preferences/PushNotificationPreferencesFragment.kt index bf65f0f9e2..5a680066ae 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/notification/preferences/NotificationPreferencesFragment.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/notification/preferences/PushNotificationPreferencesFragment.kt @@ -24,26 +24,28 @@ import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels import com.google.android.material.snackbar.Snackbar import com.instructure.canvasapi2.utils.pageview.PageView +import com.instructure.pandautils.R import com.instructure.pandautils.analytics.SCREEN_VIEW_NOTIFICATION_PREFERENCES import com.instructure.pandautils.analytics.ScreenView -import com.instructure.pandautils.databinding.FragmentPushPreferencesBinding +import com.instructure.pandautils.databinding.FragmentNotificationPreferencesBinding import com.instructure.pandautils.utils.ViewStyler import com.instructure.pandautils.utils.setupAsBackButton import dagger.hilt.android.AndroidEntryPoint -import kotlinx.android.synthetic.main.fragment_push_preferences.* +import kotlinx.android.synthetic.main.fragment_notification_preferences.* @ScreenView(SCREEN_VIEW_NOTIFICATION_PREFERENCES) @PageView(url = "profile/communication") @AndroidEntryPoint -class NotificationPreferencesFragment : Fragment() { +class PushNotificationPreferencesFragment : Fragment() { - private val viewModel: NotificationPreferencesViewModel by viewModels() + private val viewModel: PushNotificationPreferencesViewModel by viewModels() override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { - val binding = FragmentPushPreferencesBinding.inflate(inflater, container, false) + val binding = FragmentNotificationPreferencesBinding.inflate(inflater, container, false) binding.lifecycleOwner = this.viewLifecycleOwner binding.viewModel = viewModel + binding.title = resources.getString(R.string.pushNotifications) return binding.root } @@ -70,6 +72,6 @@ class NotificationPreferencesFragment : Fragment() { } companion object { - fun newInstance() = NotificationPreferencesFragment() + fun newInstance() = PushNotificationPreferencesFragment() } } \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/notification/preferences/PushNotificationPreferencesViewModel.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/notification/preferences/PushNotificationPreferencesViewModel.kt new file mode 100644 index 0000000000..ab9603eedc --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/notification/preferences/PushNotificationPreferencesViewModel.kt @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2022 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.pandautils.features.notification.preferences + +import android.content.res.Resources +import androidx.lifecycle.viewModelScope +import com.instructure.canvasapi2.managers.CommunicationChannelsManager +import com.instructure.canvasapi2.managers.NotificationPreferencesFrequency +import com.instructure.canvasapi2.managers.NotificationPreferencesFrequency.* +import com.instructure.canvasapi2.managers.NotificationPreferencesManager +import com.instructure.canvasapi2.utils.ApiPrefs +import com.instructure.pandautils.BR +import com.instructure.pandautils.R +import com.instructure.pandautils.features.notification.preferences.itemviewmodels.NotificationCategoryItemViewModel +import com.instructure.pandautils.features.notification.preferences.itemviewmodels.PushNotificationCategoryItemViewModel +import com.instructure.pandautils.mvvm.Event +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class PushNotificationPreferencesViewModel @Inject constructor( + communicationChannelsManager: CommunicationChannelsManager, + notificationPreferencesManager: NotificationPreferencesManager, + apiPrefs: ApiPrefs, + notificationPreferenceUtils: NotificationPreferenceUtils, + resources: Resources +) : NotificationPreferencesViewModel(communicationChannelsManager, notificationPreferencesManager, apiPrefs, notificationPreferenceUtils, resources) { + + override val notificationChannelType: String + get() = "push" + + override fun createCategoryItemViewModel(viewData: NotificationCategoryViewData): NotificationCategoryItemViewModel { + return PushNotificationCategoryItemViewModel(viewData, ::toggleNotification) + } + + private fun toggleNotification(enabled: Boolean, categoryName: String) { + viewModelScope.launch { + try { + communicationChannel?.let { + notificationPreferencesManager.updatePreferenceCategoryAsync( + categoryName, + it.id, + enabled.frequency.apiString, + ).await().dataOrThrow + } ?: throw IllegalStateException() + } catch (e: Exception) { + e.printStackTrace() + _data.value?.items?.forEach { + val itemViewModel = it.itemViewModels.firstOrNull { it.data.categoryName == categoryName } + itemViewModel?.let { + it.apply { + data.frequency = enabled.not().frequency + notifyPropertyChanged(BR.checked) + } + return@forEach + } + } + _events.postValue( + Event(NotificationPreferencesAction.ShowSnackbar(resources.getString( + R.string.errorOccurred))) + ) + } + } + } + + private val Boolean.frequency: NotificationPreferencesFrequency + get() = if (this) IMMEDIATELY else NEVER + +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/notification/preferences/itemviewmodels/EmailNotificationCategoryItemViewModel.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/notification/preferences/itemviewmodels/EmailNotificationCategoryItemViewModel.kt new file mode 100644 index 0000000000..dc8b3df22b --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/notification/preferences/itemviewmodels/EmailNotificationCategoryItemViewModel.kt @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2022 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.pandautils.features.notification.preferences.itemviewmodels + +import android.content.res.Resources +import androidx.databinding.Bindable +import com.instructure.canvasapi2.managers.NotificationPreferencesFrequency +import com.instructure.pandautils.R +import com.instructure.pandautils.features.notification.preferences.NotificationCategoryViewData +import com.instructure.pandautils.features.notification.preferences.NotificationPreferencesViewType + +class EmailNotificationCategoryItemViewModel( + data: NotificationCategoryViewData, + val resources: Resources, + val onClick: (String, NotificationPreferencesFrequency) -> Unit +) : NotificationCategoryItemViewModel(data) { + override val layoutId: Int = R.layout.item_email_notification_preference + + override val viewType: Int = NotificationPreferencesViewType.EMAIL_CATEGORY.viewType + + @get:Bindable + val frequency: String + get() = resources.getString(data.frequency.stringRes) + + fun onClick() { + onClick(data.categoryName, data.frequency) + } +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/notification/preferences/itemviewmodels/NotificationCategoryHeaderItemViewModel.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/notification/preferences/itemviewmodels/NotificationCategoryHeaderItemViewModel.kt index 6c30c578a2..5cad5a1598 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/notification/preferences/itemviewmodels/NotificationCategoryHeaderItemViewModel.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/notification/preferences/itemviewmodels/NotificationCategoryHeaderItemViewModel.kt @@ -20,7 +20,6 @@ import com.instructure.pandautils.R import com.instructure.pandautils.binding.GroupItemViewModel import com.instructure.pandautils.features.notification.preferences.NotificationCategoryHeaderViewData import com.instructure.pandautils.features.notification.preferences.NotificationPreferencesViewType -import com.instructure.pandautils.mvvm.ItemViewModel class NotificationCategoryHeaderItemViewModel( val data: NotificationCategoryHeaderViewData, diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/notification/preferences/itemviewmodels/NotificationCategoryItemViewModel.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/notification/preferences/itemviewmodels/NotificationCategoryItemViewModel.kt index c4a80cbdd6..4d3ce20d79 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/notification/preferences/itemviewmodels/NotificationCategoryItemViewModel.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/notification/preferences/itemviewmodels/NotificationCategoryItemViewModel.kt @@ -1,42 +1,23 @@ /* * Copyright (C) 2022 - present Instructure, Inc. * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, version 3 of the License. + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . */ - package com.instructure.pandautils.features.notification.preferences.itemviewmodels import androidx.databinding.BaseObservable -import androidx.databinding.Bindable -import com.instructure.canvasapi2.managers.NotificationPreferencesManager -import com.instructure.pandautils.R import com.instructure.pandautils.features.notification.preferences.NotificationCategoryViewData -import com.instructure.pandautils.features.notification.preferences.NotificationPreferencesViewType import com.instructure.pandautils.mvvm.ItemViewModel -class NotificationCategoryItemViewModel( - val data: NotificationCategoryViewData, - val toggle: (Boolean, String) -> Unit -) : ItemViewModel, BaseObservable() { - override val layoutId: Int = R.layout.item_notification_preference - - override val viewType: Int = NotificationPreferencesViewType.CATEGORY.viewType - - @get:Bindable val isChecked: Boolean - get() = !data.frequency.equals(NotificationPreferencesManager.NEVER, ignoreCase = true) - - fun onCheckedChanged(checked: Boolean) { - data.frequency = if (checked) NotificationPreferencesManager.IMMEDIATELY else NotificationPreferencesManager.NEVER - toggle(checked, data.categoryName) - } -} \ No newline at end of file +abstract class NotificationCategoryItemViewModel(val data: NotificationCategoryViewData) : ItemViewModel, BaseObservable() \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/notification/preferences/itemviewmodels/PushNotificationCategoryItemViewModel.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/notification/preferences/itemviewmodels/PushNotificationCategoryItemViewModel.kt new file mode 100644 index 0000000000..66c01e0317 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/notification/preferences/itemviewmodels/PushNotificationCategoryItemViewModel.kt @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2022 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.instructure.pandautils.features.notification.preferences.itemviewmodels + +import androidx.databinding.Bindable +import com.instructure.canvasapi2.managers.NotificationPreferencesFrequency.IMMEDIATELY +import com.instructure.canvasapi2.managers.NotificationPreferencesFrequency.NEVER +import com.instructure.pandautils.R +import com.instructure.pandautils.features.notification.preferences.NotificationCategoryViewData +import com.instructure.pandautils.features.notification.preferences.NotificationPreferencesViewType + +class PushNotificationCategoryItemViewModel( + data: NotificationCategoryViewData, + val toggle: (Boolean, String) -> Unit +) : NotificationCategoryItemViewModel(data) { + override val layoutId: Int = R.layout.item_push_notification_preference + + override val viewType: Int = NotificationPreferencesViewType.PUSH_CATEGORY.viewType + + @get:Bindable + val isChecked: Boolean + get() = data.frequency != NEVER + + fun onCheckedChanged(checked: Boolean) { + data.frequency = if (checked) IMMEDIATELY else NEVER + toggle(checked, data.categoryName) + } +} \ No newline at end of file diff --git a/libs/pandautils/src/main/res/layout/fragment_push_preferences.xml b/libs/pandautils/src/main/res/layout/fragment_notification_preferences.xml similarity index 93% rename from libs/pandautils/src/main/res/layout/fragment_push_preferences.xml rename to libs/pandautils/src/main/res/layout/fragment_notification_preferences.xml index 65b53a7fe6..b2937441c4 100644 --- a/libs/pandautils/src/main/res/layout/fragment_push_preferences.xml +++ b/libs/pandautils/src/main/res/layout/fragment_notification_preferences.xml @@ -23,12 +23,16 @@ + + + tools:context=".features.notification.preferences.PushNotificationPreferencesFragment"> diff --git a/libs/pandautils/src/main/res/layout/item_email_notification_preference.xml b/libs/pandautils/src/main/res/layout/item_email_notification_preference.xml new file mode 100644 index 0000000000..64971571b2 --- /dev/null +++ b/libs/pandautils/src/main/res/layout/item_email_notification_preference.xml @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/libs/pandautils/src/main/res/layout/item_notification_header.xml b/libs/pandautils/src/main/res/layout/item_notification_header.xml index 914dc471da..7705647472 100644 --- a/libs/pandautils/src/main/res/layout/item_notification_header.xml +++ b/libs/pandautils/src/main/res/layout/item_notification_header.xml @@ -27,9 +27,7 @@ android:layout_width="match_parent" android:layout_height="48dp" android:paddingStart="16dp" - android:paddingTop="8dp" - android:paddingEnd="16dp" - android:paddingBottom="8dp"> + android:paddingEnd="16dp"> + + diff --git a/libs/pandautils/src/main/res/layout/item_notification_preference.xml b/libs/pandautils/src/main/res/layout/item_push_notification_preference.xml similarity index 98% rename from libs/pandautils/src/main/res/layout/item_notification_preference.xml rename to libs/pandautils/src/main/res/layout/item_push_notification_preference.xml index 511deb4212..2f6be324b2 100644 --- a/libs/pandautils/src/main/res/layout/item_notification_preference.xml +++ b/libs/pandautils/src/main/res/layout/item_push_notification_preference.xml @@ -25,7 +25,7 @@ + type="com.instructure.pandautils.features.notification.preferences.itemviewmodels.PushNotificationCategoryItemViewModel" /> . + * + */ +package com.instructure.pandautils.features.notification.preferences + +import android.content.res.Resources +import androidx.arch.core.executor.testing.InstantTaskExecutorRule +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.LifecycleRegistry +import com.instructure.canvasapi2.managers.CommunicationChannelsManager +import com.instructure.canvasapi2.managers.NotificationPreferencesFrequency +import com.instructure.canvasapi2.managers.NotificationPreferencesManager +import com.instructure.canvasapi2.models.CommunicationChannel +import com.instructure.canvasapi2.models.NotificationPreference +import com.instructure.canvasapi2.models.NotificationPreferenceResponse +import com.instructure.canvasapi2.models.User +import com.instructure.canvasapi2.utils.ApiPrefs +import com.instructure.canvasapi2.utils.DataResult +import com.instructure.pandautils.R +import com.instructure.pandautils.features.notification.preferences.itemviewmodels.EmailNotificationCategoryItemViewModel +import com.instructure.pandautils.features.notification.preferences.itemviewmodels.PushNotificationCategoryItemViewModel +import com.instructure.pandautils.mvvm.ViewState +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import junit.framework.Assert.assertEquals +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.TestCoroutineDispatcher +import kotlinx.coroutines.test.setMain +import org.junit.Before +import org.junit.Rule +import org.junit.Test + +@ExperimentalCoroutinesApi +class EmailNotificationPreferencesViewModelTest { + + @get:Rule + var instantExecutorRule = InstantTaskExecutorRule() + + private val lifecycleOwner: LifecycleOwner = mockk(relaxed = true) + private val lifecycleRegistry = LifecycleRegistry(lifecycleOwner) + + private val testDispatcher = TestCoroutineDispatcher() + + private val communicationChannelsManager: CommunicationChannelsManager = mockk(relaxed = true) + private val notificationPreferencesManager: NotificationPreferencesManager = mockk(relaxed = true) + private val apiPrefs: ApiPrefs = mockk(relaxed = true) + private val resources: Resources = mockk(relaxed = true) + private lateinit var notificationPreferenceUtils: NotificationPreferenceUtils + + @Before + fun setUp() { + lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE) + Dispatchers.setMain(testDispatcher) + + every { apiPrefs.user } returns User(id = 1) + + every { communicationChannelsManager.getCommunicationChannelsAsync(any(), any()) } returns mockk { + coEvery { await() } returns DataResult.Success(listOf(CommunicationChannel(id = 1, userId = 1, type = "email"))) + } + + setupStrings() + notificationPreferenceUtils = NotificationPreferenceUtils(resources) + } + + @Test + fun `Notification categories map correctly`() { + val notificationResponse = NotificationPreferenceResponse( + notificationPreferences = listOf( + NotificationPreference(notification = "notification1", category = "due_date", frequency = "immediately"), + NotificationPreference(notification = "notification2", category = "membership_update", frequency = "daily"), + NotificationPreference(notification = "notification3", category = "discussion", frequency = "weekly"), + NotificationPreference(notification = "notification4", category = "announcement_created_by_you", frequency = "never") + ) + ) + + every { notificationPreferencesManager.getNotificationPreferencesAsync(any(), any(), any()) } returns mockk { + coEvery { await() } returns DataResult.Success(notificationResponse) + } + + val viewModel = createViewModel() + + viewModel.data.observe(lifecycleOwner) {} + + val data = viewModel.data.value + + assertEquals(3, viewModel.data.value?.items?.size) + + //Course Activities + val courseActivitiesHeader = data?.items?.get(0) + assertEquals("Course Activities", courseActivitiesHeader?.data?.title) + assertEquals(0, courseActivitiesHeader?.data?.position) + assertEquals(2, courseActivitiesHeader?.itemViewModels?.size) + + //Due Date + val courseActivitiesItems = courseActivitiesHeader?.itemViewModels as? List + assertEquals(2, courseActivitiesItems?.size) + assertEquals("Due Date", courseActivitiesItems?.get(0)?.data?.title) + assertEquals("Get notified when an assignment due date changes.", courseActivitiesItems?.get(0)?.data?.description) + assertEquals(1, courseActivitiesItems?.get(0)?.data?.position) + assertEquals("Immediately", courseActivitiesItems?.get(0)?.frequency) + + //Announcement Created By You + assertEquals("Announcement Created By You", courseActivitiesItems?.get(1)?.data?.title) + assertEquals("Get notified when you create an announcement and when somebody replies to your announcement.", courseActivitiesItems?.get(1)?.data?.description) + assertEquals(6, courseActivitiesItems?.get(1)?.data?.position) + assertEquals("Never", courseActivitiesItems?.get(1)?.frequency) + + //Discussions + val discussionsHeader = data?.items?.get(1) + assertEquals("Discussions", discussionsHeader?.data?.title) + assertEquals(1, discussionsHeader?.data?.position) + assertEquals(1, discussionsHeader?.itemViewModels?.size) + + //Discussion + val discussionItems = discussionsHeader?.itemViewModels as? List + assertEquals(1, discussionItems?.size) + assertEquals("Discussion", discussionItems?.get(0)?.data?.title) + assertEquals("Get notified when there’s a new discussion topic in your course.", discussionItems?.get(0)?.data?.description) + assertEquals(1, discussionItems?.get(0)?.data?.position) + assertEquals("Weekly", discussionItems?.get(0)?.frequency) + + //Groups + val groupsHeader = data?.items?.get(2) + assertEquals("Groups", groupsHeader?.data?.title) + assertEquals(4, groupsHeader?.data?.position) + assertEquals(1, groupsHeader?.itemViewModels?.size) + + //Membership update + val groupsItems = groupsHeader?.itemViewModels as? List + assertEquals(1, groupsItems?.size) + assertEquals("Membership Update", groupsItems?.get(0)?.data?.title) + assertEquals("Admin only, pending enrollment activated. Get notified when a group enrollment is accepted or rejected.", groupsItems?.get(0)?.data?.description) + assertEquals(1, groupsItems?.get(0)?.data?.position) + assertEquals("Daily", groupsItems?.get(0)?.frequency) + } + + @Test + fun `Error when cannot fetch notification preferences`() { + every { notificationPreferencesManager.getNotificationPreferencesAsync(any(), any(), any()) } returns mockk { + coEvery { await() } returns DataResult.Fail() + } + + val viewModel = createViewModel() + + viewModel.state.observe(lifecycleOwner) {} + + assertEquals(ViewState.Error("An unexpected error occurred."), viewModel.state.value) + } + + @Test + fun `Error when user is null`() { + every { apiPrefs.user } returns null + + val viewModel = createViewModel() + + viewModel.state.observe(lifecycleOwner) {} + + assertEquals(ViewState.Error("An unexpected error occurred."), viewModel.state.value) + } + + @Test + fun `Error when cannot fetch notification channels`() { + every { communicationChannelsManager.getCommunicationChannelsAsync(any(), any()) } returns mockk { + coEvery { await() } returns DataResult.Fail() + } + + val viewModel = createViewModel() + + viewModel.state.observe(lifecycleOwner) {} + + assertEquals(ViewState.Error("An unexpected error occurred."), viewModel.state.value) + } + + @Test + fun `Empty state`() { + val notificationResponse = NotificationPreferenceResponse(emptyList()) + + every { notificationPreferencesManager.getNotificationPreferencesAsync(any(), any(), any()) } returns mockk { + coEvery { await() } returns DataResult.Success(notificationResponse) + } + + val viewModel = createViewModel() + + viewModel.state.observe(lifecycleOwner) {} + + assertEquals(ViewState.Empty(emptyTitle = R.string.no_notifications_to_show, emptyImage = R.drawable.ic_panda_noalerts), viewModel.state.value) + } + + @Test + fun `Show frequency selection dialog when notification category is clicked`() { + val notificationResponse = NotificationPreferenceResponse( + notificationPreferences = listOf( + NotificationPreference(notification = "notification1", category = "due_date", frequency = "immediately") + ) + ) + + every { notificationPreferencesManager.getNotificationPreferencesAsync(any(), any(), any()) } returns mockk { + coEvery { await() } returns DataResult.Success(notificationResponse) + } + + val viewModel = createViewModel() + + viewModel.data.observe(lifecycleOwner) {} + + val data = viewModel.data.value + + val itemViewModel = data?.items?.get(0)?.itemViewModels?.get(0) as? EmailNotificationCategoryItemViewModel + itemViewModel?.onClick() + + val event = viewModel.events.value?.getContentIfNotHandled() + val expectedEvent = NotificationPreferencesAction.ShowFrequencySelectionDialog("notification1", NotificationPreferencesFrequency.IMMEDIATELY) + assertEquals(expectedEvent, event) + } + + @Test + fun `Change notification category frequency`() { + val notificationResponse = NotificationPreferenceResponse( + notificationPreferences = listOf( + NotificationPreference(notification = "notification1", category = "due_date", frequency = "immediately") + ) + ) + + val updatedNotificationResponse = NotificationPreferenceResponse( + notificationPreferences = listOf( + NotificationPreference(notification = "notification1", category = "due_date", frequency = "daily") + ) + ) + + every { notificationPreferencesManager.getNotificationPreferencesAsync(any(), any(), any()) } returns mockk { + coEvery { await() } returns DataResult.Success(notificationResponse) + } + + every { notificationPreferencesManager.updatePreferenceCategoryAsync(any(), any(), any()) } returns mockk { + coEvery { await() } returns DataResult.Success(updatedNotificationResponse) + } + + val viewModel = createViewModel() + + viewModel.data.observe(lifecycleOwner) {} + + val data = viewModel.data.value + + val itemViewModel = data?.items?.get(0)?.itemViewModels?.get(0) as? EmailNotificationCategoryItemViewModel + + assertEquals("Immediately", itemViewModel?.frequency) + viewModel.updateFrequency("notification1", NotificationPreferencesFrequency.DAILY) + verify { notificationPreferencesManager.updatePreferenceCategoryAsync("notification1", any(), "daily") } + + assertEquals("Daily", itemViewModel?.frequency) + } + + @Test + fun `Revert previous state when changing frequency has error`() { + val notificationResponse = NotificationPreferenceResponse( + notificationPreferences = listOf( + NotificationPreference(notification = "notification1", category = "due_date", frequency = "weekly") + ) + ) + + every { notificationPreferencesManager.getNotificationPreferencesAsync(any(), any(), any()) } returns mockk { + coEvery { await() } returns DataResult.Success(notificationResponse) + } + + every { notificationPreferencesManager.updatePreferenceCategoryAsync(any(), any(), any()) } returns mockk { + coEvery { await() } returns DataResult.Fail() + } + + val viewModel = createViewModel() + + viewModel.data.observe(lifecycleOwner) {} + viewModel.events.observe(lifecycleOwner) {} + + val data = viewModel.data.value + + val itemViewModel = data?.items?.get(0)?.itemViewModels?.get(0) as? EmailNotificationCategoryItemViewModel + + assertEquals("Weekly", itemViewModel?.frequency) + viewModel.updateFrequency("notification1", NotificationPreferencesFrequency.DAILY) + + assertEquals("Weekly", itemViewModel?.frequency) + val event = viewModel.events.value?.getContentIfNotHandled() + assert(event is NotificationPreferencesAction.ShowSnackbar) + assertEquals("An unexpected error occurred.", (event as NotificationPreferencesAction.ShowSnackbar).snackbar) + } + + @Test + fun `Refresh`() { + var notificationResponse = NotificationPreferenceResponse(emptyList()) + + every { notificationPreferencesManager.getNotificationPreferencesAsync(any(), any(), any()) } returns mockk { + coEvery { await() } returns DataResult.Success(notificationResponse) + } + + val viewModel = createViewModel() + + viewModel.state.observe(lifecycleOwner) {} + + assertEquals(ViewState.Empty(emptyTitle = R.string.no_notifications_to_show, emptyImage = R.drawable.ic_panda_noalerts), viewModel.state.value) + + notificationResponse = NotificationPreferenceResponse( + notificationPreferences = listOf( + NotificationPreference(notification = "notification1", category = "due_date", frequency = "never") + ) + ) + + every { notificationPreferencesManager.getNotificationPreferencesAsync(any(), any(), any()) } returns mockk { + coEvery { await() } returns DataResult.Success(notificationResponse) + } + + viewModel.refresh() + assertEquals(ViewState.Success, viewModel.state.value) + assertEquals(1, viewModel.data.value?.items?.size) + } + + private fun createViewModel(): EmailNotificationPreferencesViewModel { + return EmailNotificationPreferencesViewModel(communicationChannelsManager, notificationPreferencesManager, apiPrefs, notificationPreferenceUtils, resources) + } + + private fun setupStrings() { + every { resources.getString(R.string.notification_pref_due_date) } returns "Due Date" + every { resources.getString(R.string.notification_pref_discussion) } returns "Discussion" + every { resources.getString(R.string.notification_pref_announcement_created_by_you) } returns "Announcement Created By You" + every { resources.getString(R.string.notification_pref_membership_update) } returns "Membership Update" + every { resources.getString(R.string.notification_desc_due_date) } returns "Get notified when an assignment due date changes." + every { resources.getString(R.string.notification_desc_announcement_created_by_you) } returns "Get notified when you create an announcement and when somebody replies to your announcement." + every { resources.getString(R.string.notification_desc_discussion) } returns "Get notified when there’s a new discussion topic in your course." + every { resources.getString(R.string.notification_desc_membership_update) } returns "Admin only, pending enrollment activated. Get notified when a group enrollment is accepted or rejected." + every { resources.getString(R.string.notification_cat_course_activities) } returns "Course Activities" + every { resources.getString(R.string.notification_cat_discussions) } returns "Discussions" + every { resources.getString(R.string.notification_cat_groups) } returns "Groups" + every { resources.getString(R.string.errorOccurred) } returns "An unexpected error occurred." + every { resources.getString(R.string.emailNotificationsImmediately) } returns "Immediately" + every { resources.getString(R.string.emailNotificationsNever) } returns "Never" + every { resources.getString(R.string.emailNotificationsDaily) } returns "Daily" + every { resources.getString(R.string.emailNotificationsWeekly) } returns "Weekly" + } +} \ No newline at end of file diff --git a/libs/pandautils/src/test/java/com/instructure/pandautils/features/notification/preferences/NotificationPreferencesViewModelTest.kt b/libs/pandautils/src/test/java/com/instructure/pandautils/features/notification/preferences/PushNotificationPreferencesViewModelTest.kt similarity index 94% rename from libs/pandautils/src/test/java/com/instructure/pandautils/features/notification/preferences/NotificationPreferencesViewModelTest.kt rename to libs/pandautils/src/test/java/com/instructure/pandautils/features/notification/preferences/PushNotificationPreferencesViewModelTest.kt index 47d3f3bcca..b0b7b7f04f 100644 --- a/libs/pandautils/src/test/java/com/instructure/pandautils/features/notification/preferences/NotificationPreferencesViewModelTest.kt +++ b/libs/pandautils/src/test/java/com/instructure/pandautils/features/notification/preferences/PushNotificationPreferencesViewModelTest.kt @@ -30,11 +30,11 @@ import com.instructure.canvasapi2.models.User import com.instructure.canvasapi2.utils.ApiPrefs import com.instructure.canvasapi2.utils.DataResult import com.instructure.pandautils.R +import com.instructure.pandautils.features.notification.preferences.itemviewmodels.PushNotificationCategoryItemViewModel import com.instructure.pandautils.mvvm.ViewState import io.mockk.coEvery import io.mockk.every import io.mockk.mockk -import io.mockk.mockkStatic import junit.framework.Assert.assertEquals import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -45,7 +45,7 @@ import org.junit.Rule import org.junit.Test @ExperimentalCoroutinesApi -class NotificationPreferencesViewModelTest { +class PushNotificationPreferencesViewModelTest { @get:Rule var instantExecutorRule = InstantTaskExecutorRule() @@ -106,7 +106,7 @@ class NotificationPreferencesViewModelTest { assertEquals(2, courseActivitiesHeader?.itemViewModels?.size) //Due Date - val courseActivitiesItems = courseActivitiesHeader?.itemViewModels + val courseActivitiesItems = courseActivitiesHeader?.itemViewModels as? List assertEquals(2, courseActivitiesItems?.size) assertEquals("Due Date", courseActivitiesItems?.get(0)?.data?.title) assertEquals("Get notified when an assignment due date changes.", courseActivitiesItems?.get(0)?.data?.description) @@ -126,7 +126,7 @@ class NotificationPreferencesViewModelTest { assertEquals(1, discussionsHeader?.itemViewModels?.size) //Discussion - val discussionItems = discussionsHeader?.itemViewModels + val discussionItems = discussionsHeader?.itemViewModels as? List assertEquals(1, discussionItems?.size) assertEquals("Discussion", discussionItems?.get(0)?.data?.title) assertEquals("Get notified when there’s a new discussion topic in your course.", discussionItems?.get(0)?.data?.description) @@ -140,7 +140,7 @@ class NotificationPreferencesViewModelTest { assertEquals(1, groupsHeader?.itemViewModels?.size) //Membership update - val groupsItems = groupsHeader?.itemViewModels + val groupsItems = groupsHeader?.itemViewModels as? List assertEquals(1, groupsItems?.size) assertEquals("Membership Update", groupsItems?.get(0)?.data?.title) assertEquals("Admin only, pending enrollment activated. Get notified when a group enrollment is accepted or rejected.", groupsItems?.get(0)?.data?.description) @@ -228,7 +228,7 @@ class NotificationPreferencesViewModelTest { val data = viewModel.data.value - val itemViewModel = data?.items?.get(0)?.itemViewModels?.get(0) + val itemViewModel = data?.items?.get(0)?.itemViewModels?.get(0) as? PushNotificationCategoryItemViewModel assertEquals(true, itemViewModel?.isChecked) itemViewModel?.onCheckedChanged(false) @@ -264,7 +264,7 @@ class NotificationPreferencesViewModelTest { val data = viewModel.data.value - val itemViewModel = data?.items?.get(0)?.itemViewModels?.get(0) + val itemViewModel = data?.items?.get(0)?.itemViewModels?.get(0) as? PushNotificationCategoryItemViewModel assertEquals(false, itemViewModel?.isChecked) itemViewModel?.onCheckedChanged(true) @@ -295,7 +295,7 @@ class NotificationPreferencesViewModelTest { val data = viewModel.data.value - val itemViewModel = data?.items?.get(0)?.itemViewModels?.get(0) + val itemViewModel = data?.items?.get(0)?.itemViewModels?.get(0) as? PushNotificationCategoryItemViewModel assertEquals(false, itemViewModel?.isChecked) itemViewModel?.onCheckedChanged(true) @@ -335,8 +335,8 @@ class NotificationPreferencesViewModelTest { assertEquals(1, viewModel.data.value?.items?.size) } - private fun createViewModel(): NotificationPreferencesViewModel { - return NotificationPreferencesViewModel(communicationChannelsManager, notificationPreferencesManager, apiPrefs, notificationPreferenceUtils, resources) + private fun createViewModel(): PushNotificationPreferencesViewModel { + return PushNotificationPreferencesViewModel(communicationChannelsManager, notificationPreferencesManager, apiPrefs, notificationPreferenceUtils, resources) } private fun setupStrings() { From 4b0c620cdca38da1d54e75c580c0325c0377172a Mon Sep 17 00:00:00 2001 From: Tamas Kozmer <72397075+tamaskozmer@users.noreply.github.com> Date: Mon, 8 Aug 2022 15:46:27 +0200 Subject: [PATCH 09/49] [MBL-16186][All] Remove Id and domain from firebase reporting refs: MBL-16186 affects: Student, Teacher, Parent release note: none --- apps/flutter_parent/lib/utils/crash_utils.dart | 4 ++-- .../student/activity/CallbackActivity.kt | 13 +++---------- .../teacher/activities/SplashActivity.kt | 10 ++-------- 3 files changed, 7 insertions(+), 20 deletions(-) diff --git a/apps/flutter_parent/lib/utils/crash_utils.dart b/apps/flutter_parent/lib/utils/crash_utils.dart index 1d75550be9..d3dfb4e50f 100644 --- a/apps/flutter_parent/lib/utils/crash_utils.dart +++ b/apps/flutter_parent/lib/utils/crash_utils.dart @@ -25,8 +25,8 @@ class CrashUtils { FirebaseCrashlytics firebase = locator(); FlutterError.onError = (error) async { - await firebase - .setUserIdentifier('domain: ${ApiPrefs.getDomain() ?? 'null'} user_id: ${ApiPrefs.getUser()?.id ?? 'null'}'); + // We don't know how the crashlytics stores the userId so we just set it to empty to make sure we don't log it. + await firebase.setUserIdentifier(''); firebase.recordFlutterError(error); }; 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 4386a7ba62..ff7e145dd8 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 @@ -60,8 +60,6 @@ abstract class CallbackActivity : ParentActivity(), InboxFragment.OnUnreadCountI private fun loadInitialData() { loadInitialDataJob = tryWeave { - val crashlytics = FirebaseCrashlytics.getInstance(); - // Determine if user can masquerade if (ApiPrefs.canBecomeUser == null) { if (ApiPrefs.domain.startsWith("siteadmin", true)) { @@ -117,14 +115,9 @@ abstract class CallbackActivity : ParentActivity(), InboxFragment.OnUnreadCountI } if (!ApiPrefs.isMasquerading) { - // Set logged user details - if (Logger.canLogUserDetails()) { - Logger.d("User detail logging allowed. Setting values.") - crashlytics.setUserId("UserID: ${ApiPrefs.user?.id.toString()} User Domain: ${ApiPrefs.domain}") - } else { - Logger.d("User detail logging disallowed. Clearing values.") - crashlytics.setUserId("") - } + // We don't know how the crashlytics stores the userId so we just set it to empty to make sure we don't log it. + val crashlytics = FirebaseCrashlytics.getInstance(); + crashlytics.setUserId("") } // get unread count of conversations diff --git a/apps/teacher/src/main/java/com/instructure/teacher/activities/SplashActivity.kt b/apps/teacher/src/main/java/com/instructure/teacher/activities/SplashActivity.kt index 0fd9d44629..16fce1bdca 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/activities/SplashActivity.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/activities/SplashActivity.kt @@ -185,15 +185,9 @@ class SplashActivity : AppCompatActivity() { Logger.e(e.message) } - // Set logged user details + // We don't know how the crashlytics stores the userId so we just set it to empty to make sure we don't log it. val crashlytics = FirebaseCrashlytics.getInstance(); - if (Logger.canLogUserDetails()) { - Logger.d("User detail logging allowed. Setting values.") - crashlytics.setUserId("UserID: ${ApiPrefs.user?.id.toString()} User Domain: ${ApiPrefs.domain}") - } else { - Logger.d("User detail logging disallowed. Clearing values.") - crashlytics.setUserId("") - } + crashlytics.setUserId("") startActivity(InitActivity.createIntent(this@SplashActivity, intent?.extras)) canvasLoadingView.announceForAccessibility(getString(R.string.loading)) From c8c48476bb3776fa1ff6ca9a33297c6b209e726b Mon Sep 17 00:00:00 2001 From: Akos Hermann <72087159+hermannakos@users.noreply.github.com> Date: Mon, 8 Aug 2022 16:14:44 +0200 Subject: [PATCH 10/49] [MBL-16138][Student][Teacher] Share Extension basic UI + File Upload (#1661) --- .../interaction/UserFilesInteractionTest.kt | 2 + apps/student/src/main/AndroidManifest.xml | 2 +- .../student/activity/CandroidPSPDFActivity.kt | 6 +- .../student/activity/NavigationActivity.kt | 11 +- .../activity/ShareFileUploadActivity.kt | 253 ------ .../dialog/ShareFileDestinationDialog.kt | 328 -------- .../StudentShareExtensionActivity.kt | 36 + .../fragment/DiscussionsReplyFragment.kt | 10 +- .../student/fragment/FileListFragment.kt | 6 +- .../fragment/InboxComposeMessageFragment.kt | 7 +- .../ui/AssignmentDetailsView.kt | 2 +- .../student/router/RouteMatcher.kt | 1 + .../com/instructure/student/util/FileUtils.kt | 2 +- .../student/util/UploadCheckboxManager.kt | 143 ---- .../res/layout/upload_file_destination.xml | 192 ----- apps/student/src/main/res/values/styles.xml | 2 +- .../activities/BaseAppCompatActivity.kt | 10 +- .../teacher/activities/SpeedGraderActivity.kt | 10 +- .../teacher/fragments/AddMessageFragment.kt | 6 +- .../fragments/CreateDiscussionFragment.kt | 21 +- .../CreateOrEditAnnouncementFragment.kt | 11 +- .../fragments/DiscussionsReplyFragment.kt | 12 +- .../teacher/fragments/FileListFragment.kt | 11 +- .../canvasapi2/managers/AssignmentManager.kt | 2 + libs/pandares/src/main/assets/confetti.json | 1 + .../src/main/res/anim/ease_in_bottom.xml | 0 .../src/main/res/anim/ease_in_shrink.xml | 0 .../src/main/res/anim/expand_from_middle.xml | 0 .../pandares}/src/main/res/anim/fab_hide.xml | 0 .../src/main/res/anim/fab_reveal.xml | 0 .../src/main/res/anim/fab_rotate_backward.xml | 0 .../src/main/res/anim/fab_rotate_forward.xml | 0 .../src/main/res/anim/fade_in_quick.xml | 0 .../pandares}/src/main/res/anim/fade_out.xml | 0 .../src/main/res/anim/fade_out_quick.xml | 0 .../src/main/res/anim/hs_slide_out_left.xml | 0 .../pandares}/src/main/res/anim/none.xml | 0 .../pandares}/src/main/res/anim/rotate.xml | 0 .../src/main/res/anim/rotate_back.xml | 0 .../main/res/anim/scale_slide_in_bottom.xml | 0 .../res/anim/scale_slide_in_bottom_slow.xml | 0 .../src/main/res/anim/shrink_to_middle.xml | 0 .../main/res/anim/slide_in_from_bottom.xml | 0 .../src/main/res/anim/slide_in_right.xml | 0 .../src/main/res/anim/slide_out_to_bottom.xml | 0 .../src/main/res/anim/slow_push_left_in.xml | 0 .../src/main/res/anim/slow_push_left_out.xml | 0 .../src/main/res/anim/slow_push_right_in.xml | 0 .../src/main/res/anim/slow_push_right_out.xml | 0 .../src/main/res/anim/up_from_bottom.xml | 0 .../src/main/res/drawable/upload_file_bg.xml | 0 libs/pandares/src/main/res/values/strings.xml | 1 + libs/pandautils/build.gradle | 2 + .../binding/BindableSpinnerAdapter.kt | 63 ++ .../pandautils/binding/BindingAdapters.kt | 29 +- .../pandautils/di/ApplicationModule.kt | 7 + .../pandautils/di/FileUploadModule.kt | 43 + .../pandautils/dialogs/UploadFilesDialog.kt | 779 ------------------ .../file/upload/FileUploadDialogFragment.kt | 336 ++++++++ .../file/upload/FileUploadDialogViewData.kt | 40 + .../file/upload/FileUploadDialogViewModel.kt | 275 +++++++ .../features/file/upload/FileUploadType.kt | 21 + .../file/upload/FileUploadUtilsHelper.kt | 37 + .../itemviewmodels/FileItemViewModel.kt | 29 + .../shareextension/ShareExtensionActivity.kt | 199 +++++ .../shareextension/ShareExtensionViewModel.kt | 119 +++ .../ShareExtensionProgressDialogFragment.kt | 47 ++ .../ShareExtensionProgressDialogViewModel.kt | 7 + .../ShareExtensionSuccessDialogFragment.kt | 97 +++ .../ShareExtensionSuccessDialogViewModel.kt | 57 ++ .../success/ShareExtensionSuccessViewData.kt | 28 + .../target/ShareExtensionTargetFragment.kt | 186 +++++ .../target/ShareExtensionTargetViewData.kt | 48 ++ .../target/ShareExtensionTargetViewModel.kt | 160 ++++ .../ShareExtensionAssignmentItemViewModel.kt | 29 + .../ShareExtensionCourseItemViewModel.kt | 26 + .../loaders/OpenMediaAsyncTaskLoader.kt | 5 +- .../pandautils/utils}/AnimationHelpers.kt | 2 +- .../pandautils/utils/FragmentExtensions.kt | 2 + .../pandautils/utils/RequestCodes.kt | 1 + .../pandautils/views/CanvasWebView.kt | 8 +- .../main/res/layout/activity_share_file.xml | 0 .../main/res/layout/adapter_file_uploads.xml | 151 ++-- .../main/res/layout/dialog_files_upload.xml | 211 ----- .../layout/fragment_file_upload_dialog.xml | 227 +++++ ...agment_share_extension_progress_dialog.xml | 29 + ...ragment_share_extension_success_dialog.xml | 130 +++ .../fragment_share_extension_target.xml | 170 ++++ .../res/layout/item_assignment_spinner.xml | 45 + .../layout/item_canvas_context_spinner.xml | 54 ++ .../file/upload/FileUploadViewModelTest.kt | 212 +++++ .../ShareExtensionTargetViewModelTest.kt | 246 ++++++ 92 files changed, 3190 insertions(+), 2055 deletions(-) delete mode 100644 apps/student/src/main/java/com/instructure/student/activity/ShareFileUploadActivity.kt delete mode 100644 apps/student/src/main/java/com/instructure/student/dialog/ShareFileDestinationDialog.kt create mode 100644 apps/student/src/main/java/com/instructure/student/features/shareextension/StudentShareExtensionActivity.kt delete mode 100644 apps/student/src/main/java/com/instructure/student/util/UploadCheckboxManager.kt delete mode 100644 apps/student/src/main/res/layout/upload_file_destination.xml create mode 100644 libs/pandares/src/main/assets/confetti.json rename {apps/student => libs/pandares}/src/main/res/anim/ease_in_bottom.xml (100%) rename {apps/student => libs/pandares}/src/main/res/anim/ease_in_shrink.xml (100%) rename {apps/student => libs/pandares}/src/main/res/anim/expand_from_middle.xml (100%) rename {apps/student => libs/pandares}/src/main/res/anim/fab_hide.xml (100%) rename {apps/student => libs/pandares}/src/main/res/anim/fab_reveal.xml (100%) rename {apps/student => libs/pandares}/src/main/res/anim/fab_rotate_backward.xml (100%) rename {apps/student => libs/pandares}/src/main/res/anim/fab_rotate_forward.xml (100%) rename {apps/student => libs/pandares}/src/main/res/anim/fade_in_quick.xml (100%) rename {apps/student => libs/pandares}/src/main/res/anim/fade_out.xml (100%) rename {apps/student => libs/pandares}/src/main/res/anim/fade_out_quick.xml (100%) rename {apps/student => libs/pandares}/src/main/res/anim/hs_slide_out_left.xml (100%) rename {apps/student => libs/pandares}/src/main/res/anim/none.xml (100%) rename {apps/student => libs/pandares}/src/main/res/anim/rotate.xml (100%) rename {apps/student => libs/pandares}/src/main/res/anim/rotate_back.xml (100%) rename {apps/student => libs/pandares}/src/main/res/anim/scale_slide_in_bottom.xml (100%) rename {apps/student => libs/pandares}/src/main/res/anim/scale_slide_in_bottom_slow.xml (100%) rename {apps/student => libs/pandares}/src/main/res/anim/shrink_to_middle.xml (100%) rename {apps/student => libs/pandares}/src/main/res/anim/slide_in_from_bottom.xml (100%) rename {apps/student => libs/pandares}/src/main/res/anim/slide_in_right.xml (100%) rename {apps/student => libs/pandares}/src/main/res/anim/slide_out_to_bottom.xml (100%) rename {apps/student => libs/pandares}/src/main/res/anim/slow_push_left_in.xml (100%) rename {apps/student => libs/pandares}/src/main/res/anim/slow_push_left_out.xml (100%) rename {apps/student => libs/pandares}/src/main/res/anim/slow_push_right_in.xml (100%) rename {apps/student => libs/pandares}/src/main/res/anim/slow_push_right_out.xml (100%) rename {apps/student => libs/pandares}/src/main/res/anim/up_from_bottom.xml (100%) rename {apps/student => libs/pandares}/src/main/res/drawable/upload_file_bg.xml (100%) create mode 100644 libs/pandautils/src/main/java/com/instructure/pandautils/binding/BindableSpinnerAdapter.kt create mode 100644 libs/pandautils/src/main/java/com/instructure/pandautils/di/FileUploadModule.kt delete mode 100644 libs/pandautils/src/main/java/com/instructure/pandautils/dialogs/UploadFilesDialog.kt create mode 100644 libs/pandautils/src/main/java/com/instructure/pandautils/features/file/upload/FileUploadDialogFragment.kt create mode 100644 libs/pandautils/src/main/java/com/instructure/pandautils/features/file/upload/FileUploadDialogViewData.kt create mode 100644 libs/pandautils/src/main/java/com/instructure/pandautils/features/file/upload/FileUploadDialogViewModel.kt create mode 100644 libs/pandautils/src/main/java/com/instructure/pandautils/features/file/upload/FileUploadType.kt create mode 100644 libs/pandautils/src/main/java/com/instructure/pandautils/features/file/upload/FileUploadUtilsHelper.kt create mode 100644 libs/pandautils/src/main/java/com/instructure/pandautils/features/file/upload/itemviewmodels/FileItemViewModel.kt create mode 100644 libs/pandautils/src/main/java/com/instructure/pandautils/features/shareextension/ShareExtensionActivity.kt create mode 100644 libs/pandautils/src/main/java/com/instructure/pandautils/features/shareextension/ShareExtensionViewModel.kt create mode 100644 libs/pandautils/src/main/java/com/instructure/pandautils/features/shareextension/progress/ShareExtensionProgressDialogFragment.kt create mode 100644 libs/pandautils/src/main/java/com/instructure/pandautils/features/shareextension/progress/ShareExtensionProgressDialogViewModel.kt create mode 100644 libs/pandautils/src/main/java/com/instructure/pandautils/features/shareextension/success/ShareExtensionSuccessDialogFragment.kt create mode 100644 libs/pandautils/src/main/java/com/instructure/pandautils/features/shareextension/success/ShareExtensionSuccessDialogViewModel.kt create mode 100644 libs/pandautils/src/main/java/com/instructure/pandautils/features/shareextension/success/ShareExtensionSuccessViewData.kt create mode 100644 libs/pandautils/src/main/java/com/instructure/pandautils/features/shareextension/target/ShareExtensionTargetFragment.kt create mode 100644 libs/pandautils/src/main/java/com/instructure/pandautils/features/shareextension/target/ShareExtensionTargetViewData.kt create mode 100644 libs/pandautils/src/main/java/com/instructure/pandautils/features/shareextension/target/ShareExtensionTargetViewModel.kt create mode 100644 libs/pandautils/src/main/java/com/instructure/pandautils/features/shareextension/target/itemviewmodels/ShareExtensionAssignmentItemViewModel.kt create mode 100644 libs/pandautils/src/main/java/com/instructure/pandautils/features/shareextension/target/itemviewmodels/ShareExtensionCourseItemViewModel.kt rename {apps/student/src/main/java/com/instructure/student/util => libs/pandautils/src/main/java/com/instructure/pandautils/utils}/AnimationHelpers.kt (98%) rename {apps/student => libs/pandautils}/src/main/res/layout/activity_share_file.xml (100%) delete mode 100644 libs/pandautils/src/main/res/layout/dialog_files_upload.xml create mode 100644 libs/pandautils/src/main/res/layout/fragment_file_upload_dialog.xml create mode 100644 libs/pandautils/src/main/res/layout/fragment_share_extension_progress_dialog.xml create mode 100644 libs/pandautils/src/main/res/layout/fragment_share_extension_success_dialog.xml create mode 100644 libs/pandautils/src/main/res/layout/fragment_share_extension_target.xml create mode 100644 libs/pandautils/src/main/res/layout/item_assignment_spinner.xml create mode 100644 libs/pandautils/src/main/res/layout/item_canvas_context_spinner.xml create mode 100644 libs/pandautils/src/test/java/com/instructure/pandautils/features/file/upload/FileUploadViewModelTest.kt create mode 100644 libs/pandautils/src/test/java/com/instructure/pandautils/features/shareextension/target/ShareExtensionTargetViewModelTest.kt diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/UserFilesInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/UserFilesInteractionTest.kt index 17cbc628d6..73630d088d 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/UserFilesInteractionTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/UserFilesInteractionTest.kt @@ -85,6 +85,7 @@ class UserFilesInteractionTest : StudentTest() { // Should be able to upload a file from the user's device // Mocks the result from the expected intent, then uploads it. + @Stub @Test @TestMetaData(Priority.IMPORTANT, FeatureCategory.FILES, TestCategory.INTERACTION, false) fun testUpload_deviceFile() { @@ -156,6 +157,7 @@ class UserFilesInteractionTest : StudentTest() { // Should be able to upload a file from the user's photo gallery // Mocks the result from the expected intent, then uploads it. + @Stub @Test @TestMetaData(Priority.IMPORTANT, FeatureCategory.FILES, TestCategory.INTERACTION, false) fun testUpload_gallery() { diff --git a/apps/student/src/main/AndroidManifest.xml b/apps/student/src/main/AndroidManifest.xml index 0a83d35912..b98710653d 100644 --- a/apps/student/src/main/AndroidManifest.xml +++ b/apps/student/src/main/AndroidManifest.xml @@ -210,7 +210,7 @@ . - * - */ - -package com.instructure.student.activity - -import android.animation.Animator -import android.animation.AnimatorListenerAdapter -import android.animation.ArgbEvaluator -import android.animation.ValueAnimator -import android.content.DialogInterface -import android.content.Intent -import android.net.Uri -import android.os.Bundle -import android.os.Parcelable -import android.text.TextUtils -import android.view.ViewTreeObserver -import android.widget.Toast -import androidx.appcompat.app.AppCompatActivity -import androidx.core.app.ActivityCompat -import androidx.core.content.ContextCompat -import androidx.fragment.app.DialogFragment -import com.instructure.canvasapi2.managers.CourseManager -import com.instructure.canvasapi2.models.Assignment -import com.instructure.canvasapi2.models.CanvasContext -import com.instructure.canvasapi2.models.Course -import com.instructure.canvasapi2.models.StorageQuotaExceededError -import com.instructure.canvasapi2.utils.ApiPrefs -import com.instructure.canvasapi2.utils.isNotDeleted -import com.instructure.canvasapi2.utils.weave.awaitApi -import com.instructure.canvasapi2.utils.weave.catch -import com.instructure.canvasapi2.utils.weave.tryWeave -import com.instructure.pandautils.dialogs.UploadFilesDialog -import com.instructure.pandautils.utils.* -import com.instructure.student.R -import com.instructure.student.dialog.ShareFileDestinationDialog -import com.instructure.student.util.Analytics -import com.instructure.student.util.AnimationHelpers -import kotlinx.android.parcel.Parcelize -import kotlinx.android.synthetic.main.activity_share_file.* -import kotlinx.coroutines.Job -import org.greenrobot.eventbus.EventBus -import org.greenrobot.eventbus.Subscribe -import org.greenrobot.eventbus.ThreadMode -import java.util.* - -@Parcelize -data class ShareFileSubmissionTarget( - val course: Course, - val assignment: Assignment -) : Parcelable - -class ShareFileUploadActivity : AppCompatActivity(), ShareFileDestinationDialog.DialogCloseListener { - - private val PERMISSION_REQUEST_WRITE_STORAGE = 0 - - private var loadCoursesJob: Job? = null - private var uploadFileSourceFragment: DialogFragment? = null - private var courses: ArrayList? = null - - private val submissionTarget: ShareFileSubmissionTarget? by lazy { - intent?.extras?.getParcelable(Const.SUBMISSION_TARGET) - } - - private var sharedURI: Uri? = null - - public override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContentView(R.layout.activity_share_file) - ViewStyler.setStatusBarDark(this, ContextCompat.getColor(this, R.color.studentDocumentSharingColor)) - if (checkLoggedIn()) { - revealBackground() - Analytics.trackAppFlow(this) - sharedURI = parseIntentType() - if (submissionTarget != null) { - // If targeted for submission, skip the picker and go immediately to the submission workflow - val bundle = UploadFilesDialog.createAssignmentBundle( - sharedURI, - submissionTarget!!.course, - submissionTarget!!.assignment - ) - onNext(bundle) - } else { - getCourses() - } - askForStoragePermissionIfNecessary() - } - } - - private fun askForStoragePermissionIfNecessary() { - if ((sharedURI?.scheme?.equals("file") == true || sharedURI?.scheme?.equals("content") == true) && !PermissionUtils.hasPermissions(this, PermissionUtils.WRITE_EXTERNAL_STORAGE)) { - ActivityCompat.requestPermissions(this, PermissionUtils.makeArray(PermissionUtils.WRITE_EXTERNAL_STORAGE), PERMISSION_REQUEST_WRITE_STORAGE) - } - } - - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - super.onActivityResult(requestCode, resultCode, data) - - if (requestCode == UploadFilesDialog.CAMERA_PIC_REQUEST || - requestCode == UploadFilesDialog.PICK_FILE_FROM_DEVICE || - requestCode == UploadFilesDialog.PICK_IMAGE_GALLERY) { - //File Dialog Fragment will not be notified of onActivityResult(), alert manually - OnActivityResults(ActivityResult(requestCode, resultCode, data), null).postSticky() - } - } - - override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) { - super.onRequestPermissionsResult(requestCode, permissions, grantResults) - when(requestCode) { - PERMISSION_REQUEST_WRITE_STORAGE -> { - if (!PermissionUtils.allPermissionsGrantedResultSummary(grantResults)) { - Toast.makeText(this, R.string.permissionDenied, Toast.LENGTH_LONG).show() - finish() - } - } - } - } - - private fun getCourses() { - loadCoursesJob = tryWeave { - val courses = awaitApi> { CourseManager.getCourses(true, it) } - if (courses.isNotEmpty()) { - this@ShareFileUploadActivity.courses = ArrayList(courses) - if (uploadFileSourceFragment == null) showDestinationDialog() - } else { - Toast.makeText(applicationContext, R.string.uploadingFromSourceFailed, Toast.LENGTH_LONG).show() - exitActivity() - } - } catch { - Toast.makeText(this@ShareFileUploadActivity, R.string.uploadingFromSourceFailed, Toast.LENGTH_LONG).show() - exitActivity() - } - } - - private fun revealBackground() { - rootView.viewTreeObserver.addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener { - override fun onGlobalLayout() { - AnimationHelpers.removeGlobalLayoutListeners(rootView, this) - AnimationHelpers.createRevealAnimator(rootView).start() - } - }) - } - - private fun checkLoggedIn(): Boolean { - return if (TextUtils.isEmpty(ApiPrefs.getValidToken())) { - exitActivity() - false - } else { - true - } - } - - private fun exitActivity() { - val intent = LoginActivity.createIntent(this) - startActivity(intent) - finish() - } - - override fun onBackPressed() { - uploadFileSourceFragment?.dismissAllowingStateLoss() - super.onBackPressed() - } - - override fun onDestroy() { - uploadFileSourceFragment?.dismissAllowingStateLoss() - loadCoursesJob?.cancel() - super.onDestroy() - } - - override fun onStart() { - super.onStart() - EventBus.getDefault().register(this) - } - - override fun onStop() { - super.onStop() - EventBus.getDefault().unregister(this) - } - - private fun showDestinationDialog() { - if (sharedURI == null) { - Toast.makeText(applicationContext, R.string.uploadingFromSourceFailed, Toast.LENGTH_LONG).show() - } else { - uploadFileSourceFragment = ShareFileDestinationDialog.newInstance(ShareFileDestinationDialog.createBundle(sharedURI!!, courses!!)) - uploadFileSourceFragment!!.show(supportFragmentManager, ShareFileDestinationDialog.TAG) - } - } - - private fun parseIntentType(): Uri? { - // Get intent, action and MIME type - val intent = intent - val action = intent.action - val type = intent.type - - return if (Intent.ACTION_SEND == action && type != null) { - intent.getParcelableExtra(Intent.EXTRA_STREAM) - } else null - - } - - override fun onCancel(dialog: DialogInterface?) { - finish() - } - - - @Suppress("unused", "UNUSED_PARAMETER") - @Subscribe(threadMode = ThreadMode.MAIN) - fun onQuotaExceeded(errorCode: StorageQuotaExceededError) { - toast(R.string.fileQuotaExceeded) - } - - private fun getColor(bundle: Bundle?): Int { - return if(bundle != null && bundle.containsKey(Const.CANVAS_CONTEXT)) { - val color = ColorKeeper.getOrGenerateColor(bundle.getParcelable(Const.CANVAS_CONTEXT) as CanvasContext) - ViewStyler.setStatusBarDark(this, color) - color - } else { - val color = ContextCompat.getColor(this, R.color.login_studentAppTheme) - ViewStyler.setStatusBarDark(this, color) - color - } - } - - override fun onNext(bundle: Bundle) { - ValueAnimator.ofObject(ArgbEvaluator(), ContextCompat.getColor(this, R.color.login_studentAppTheme), getColor(bundle)).let { - it.addUpdateListener { animation -> rootView!!.setBackgroundColor(animation.animatedValue as Int) } - it.duration = 500 - it.addListener(object : AnimatorListenerAdapter() { - override fun onAnimationStart(animation: Animator) { - UploadFilesDialog.show(supportFragmentManager, bundle) { event -> - if(event == UploadFilesDialog.EVENT_ON_UPLOAD_BEGIN || event == UploadFilesDialog.EVENT_DIALOG_CANCELED) { - finish() - } - } - } - }) - it.start() - } - } -} diff --git a/apps/student/src/main/java/com/instructure/student/dialog/ShareFileDestinationDialog.kt b/apps/student/src/main/java/com/instructure/student/dialog/ShareFileDestinationDialog.kt deleted file mode 100644 index 4d09f4da63..0000000000 --- a/apps/student/src/main/java/com/instructure/student/dialog/ShareFileDestinationDialog.kt +++ /dev/null @@ -1,328 +0,0 @@ -/* - * Copyright (C) 2016 - present Instructure, Inc. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, version 3 of the License. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - * - */ -package com.instructure.student.dialog - -import android.animation.Animator -import android.animation.AnimatorListenerAdapter -import android.annotation.SuppressLint -import android.app.Dialog -import android.content.DialogInterface -import android.net.Uri -import android.os.Bundle -import android.os.Handler -import android.view.* -import android.view.animation.AnimationUtils -import android.widget.AdapterView -import android.widget.Toast -import androidx.appcompat.app.AlertDialog -import androidx.fragment.app.DialogFragment -import com.instructure.canvasapi2.managers.AssignmentManager.getAllAssignments -import com.instructure.canvasapi2.models.Assignment -import com.instructure.canvasapi2.models.Course -import com.instructure.canvasapi2.models.User -import com.instructure.canvasapi2.utils.ApiPrefs -import com.instructure.canvasapi2.utils.Pronouns.span -import com.instructure.canvasapi2.utils.weave.awaitApi -import com.instructure.canvasapi2.utils.weave.catch -import com.instructure.canvasapi2.utils.weave.tryWeave -import com.instructure.pandautils.dialogs.UploadFilesDialog -import com.instructure.pandautils.dialogs.UploadFilesDialog.Companion.createAssignmentBundle -import com.instructure.pandautils.dialogs.UploadFilesDialog.Companion.createFilesBundle -import com.instructure.pandautils.utils.Const -import com.instructure.pandautils.utils.ParcelableArg -import com.instructure.pandautils.utils.ParcelableArrayListArg -import com.instructure.pandautils.utils.ThemePrefs.buttonColor -import com.instructure.pandautils.utils.setVisible -import com.instructure.student.R -import com.instructure.student.adapter.FileUploadAssignmentsAdapter -import com.instructure.student.adapter.FileUploadAssignmentsAdapter.Companion.getOnlineUploadAssignmentsList -import com.instructure.student.adapter.FileUploadCoursesAdapter -import com.instructure.student.adapter.FileUploadCoursesAdapter.Companion.getFilteredCourseList -import com.instructure.student.util.AnimationHelpers.createRevealAnimator -import com.instructure.student.util.AnimationHelpers.removeGlobalLayoutListeners -import com.instructure.student.util.UploadCheckboxManager -import com.instructure.student.util.UploadCheckboxManager.OnOptionCheckedListener -import kotlinx.android.synthetic.main.upload_file_destination.* -import kotlinx.coroutines.Job -import java.util.* - -@SuppressLint("InflateParams") -class ShareFileDestinationDialog : DialogFragment(), OnOptionCheckedListener { - // Dismiss interface - interface DialogCloseListener { - fun onCancel(dialog: DialogInterface?) - fun onNext(bundle: Bundle) - } - - private var uri: Uri by ParcelableArg(key = Const.URI) - private var courses: ArrayList by ParcelableArrayListArg(key = Const.COURSES) - private var user: User = ApiPrefs.user!! - - private lateinit var checkboxManager: UploadCheckboxManager - private lateinit var rootView: View - - private var assignmentJob: Job? = null - - private var selectedAssignment: Assignment? = null - private var studentEnrollmentsAdapter: FileUploadCoursesAdapter? = null - - override fun onStart() { - super.onStart() - // Don't dim the background when the dialog is created. - dialog?.window?.apply { - val params = attributes - params.dimAmount = 0f - params.flags = params.flags or WindowManager.LayoutParams.FLAG_DIM_BEHIND - attributes = params - } - } - - override fun onActivityCreated(savedInstanceState: Bundle?) { - super.onActivityCreated(savedInstanceState) - dialog?.window?.let { - it.attributes.windowAnimations = R.style.FileDestinationDialogAnimation - it.setWindowAnimations(R.style.FileDestinationDialogAnimation) - } - } - - override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { - rootView = LayoutInflater.from(activity).inflate(R.layout.upload_file_destination, null) - val alertDialog = AlertDialog.Builder(requireContext()) - .setView(rootView) - .setPositiveButton(R.string.next) { _, _ -> validateAndShowNext() } - .setNegativeButton(R.string.cancel) { _, _ -> dismissAllowingStateLoss() } - .setCancelable(true) - .create() - - alertDialog.setOnShowListener { - alertDialog.getButton(AlertDialog.BUTTON_POSITIVE).setTextColor(buttonColor) - alertDialog.getButton(AlertDialog.BUTTON_NEGATIVE).setTextColor(buttonColor) - } - - return alertDialog - } - - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - return rootView - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - userName.text = span(user.name, user.pronouns) - - // Init checkboxes - checkboxManager = UploadCheckboxManager(this, selectionIndicator) - checkboxManager.add(myFilesCheckBox) - checkboxManager.add(assignmentCheckBox) - - setRevealContentsListener() - assignmentContainer.setVisible() - } - - override fun onCancel(dialog: DialogInterface) { - (activity as? DialogCloseListener)?.onCancel(dialog) - } - - override fun onDestroyView() { - if (retainInstance) dialog?.dismiss() - super.onDestroyView() - } - - private fun validateAndShowNext() { - // Validate selections - val errorString = validateForm() - if (errorString.isNotEmpty()) { - Toast.makeText(activity, errorString, Toast.LENGTH_SHORT).show() - } else { - (activity as? DialogCloseListener)?.onNext(uploadBundle) - dismiss() - } - } - - /** - * Checks if user has filled out form completely. - * @return Returns an error string if the form is not valid. - */ - private fun validateForm(): String { - // Make sure the user has selected a course and an assignment - val uploadType = checkboxManager.selectedType - - // Make sure an assignment & course was selected if FileUploadType.Assignment - if (uploadType == UploadFilesDialog.FileUploadType.ASSIGNMENT) { - if (studentCourseSpinner.selectedItem == null) { - return getString(R.string.noCourseSelected) - } else if (assignmentSpinner.selectedItem == null || (assignmentSpinner.selectedItem as? Assignment)?.id == Long.MIN_VALUE) { - return getString(R.string.noAssignmentSelected) - } - } - return "" - } - - private val uploadBundle: Bundle - get() = when (checkboxManager.selectedCheckBox!!.id) { - R.id.myFilesCheckBox -> createFilesBundle(uri, null) - R.id.assignmentCheckBox -> createAssignmentBundle( - uri, - (studentCourseSpinner.selectedItem as Course), - (assignmentSpinner.selectedItem as Assignment) - ) - else -> createFilesBundle(uri, null) - } - - private fun setAssignmentsSpinnerToLoading() { - val loading = Assignment() - val courseAssignments = ArrayList() - loading.name = getString(R.string.loadingAssignments) - loading.id = Long.MIN_VALUE - courseAssignments.add(loading) - assignmentSpinner.adapter = FileUploadAssignmentsAdapter(requireContext(), courseAssignments) - } - - fun fetchAssignments(courseId: Long) { - assignmentJob?.cancel() - assignmentJob = tryWeave { - val assignments = awaitApi> { getAllAssignments(courseId, false, it) } - if (assignments.isNotEmpty() && courseSelectionChanged(assignments[0].courseId)) return@tryWeave - val courseAssignments = getOnlineUploadAssignmentsList(requireContext(), assignments) - - // Init assignment spinner - val adapter = FileUploadAssignmentsAdapter(requireContext(), courseAssignments) - assignmentSpinner.adapter = adapter - if (selectedAssignment != null) { - // Prevent listener from firing the when selection is placed - assignmentSpinner.onItemSelectedListener = null - val position = adapter.getPosition(selectedAssignment) - if (position >= 0) { - // Prevents the network callback from replacing what the user selected while cache was being displayed - assignmentSpinner.setSelection(position, false) - } - } - assignmentSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { - override fun onItemSelected(parent: AdapterView<*>?, view: View, position: Int, id: Long) { - if (position < 0) return - if (position < adapter.count) { - selectedAssignment = adapter.getItem(position) - } - } - - override fun onNothingSelected(parent: AdapterView<*>?) {} - } - } catch { - // Do nothing - } - } - - private fun setupCourseSpinners() { - if (activity?.isFinishing != false) return - if (studentEnrollmentsAdapter == null) { - studentEnrollmentsAdapter = FileUploadCoursesAdapter( - requireContext(), - requireActivity().layoutInflater, - getFilteredCourseList(courses, FileUploadCoursesAdapter.Type.STUDENT) - ) - studentCourseSpinner.adapter = studentEnrollmentsAdapter - } else { - studentEnrollmentsAdapter?.setCourses(getFilteredCourseList(courses, FileUploadCoursesAdapter.Type.STUDENT)) - } - studentCourseSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { - override fun onItemSelected(parent: AdapterView<*>, view: View, position: Int, id: Long) { - // Make the allowed extensions disappear - val (courseId) = parent.adapter.getItem(position) as Course - // If the user is a teacher, let them know and don't let them select an assignment - if (courseId > 0) { - setAssignmentsSpinnerToLoading() - fetchAssignments(courseId) - } - } - - override fun onNothingSelected(parent: AdapterView<*>?) {} - } - } - - private fun courseSelectionChanged(newCourseId: Long): Boolean { - return checkboxManager.selectedCheckBox!!.id == R.id.assignmentCheckBox && newCourseId != (studentCourseSpinner.selectedItem as Course).id - } - - private fun setRevealContentsListener() { - val avatarAnimation = AnimationUtils.loadAnimation(activity, R.anim.ease_in_shrink) - val titleAnimation = AnimationUtils.loadAnimation(activity, R.anim.ease_in_bottom) - avatar.viewTreeObserver.addOnGlobalLayoutListener( - object : ViewTreeObserver.OnGlobalLayoutListener { - override fun onGlobalLayout() { - removeGlobalLayoutListeners(avatar, this) - avatar.startAnimation(avatarAnimation) - userName.startAnimation(titleAnimation) - dialogTitle.startAnimation(titleAnimation) - } - } - ) - dialogContents.viewTreeObserver.addOnGlobalLayoutListener( - object : ViewTreeObserver.OnGlobalLayoutListener { - override fun onGlobalLayout() { - removeGlobalLayoutListeners(dialogContents, this) - val revealAnimator = createRevealAnimator(dialogContents) - Handler().postDelayed({ - if (!isAdded) return@postDelayed - dialogContents.visibility = View.VISIBLE - revealAnimator.addListener( - object : AnimatorListenerAdapter() { - override fun onAnimationEnd(animation: Animator) { - setupCourseSpinners() - } - } - ) - revealAnimator.start() - }, 600) - } - } - ) - } - - private fun enableStudentSpinners(isEnabled: Boolean) { - assignmentSpinner.isEnabled = isEnabled - studentCourseSpinner.isEnabled = isEnabled - } - - override fun onUserFilesSelected() { - enableStudentSpinners(false) - } - - override fun onAssignmentFilesSelected() { - enableStudentSpinners(true) - } - - override fun onDestroy() { - assignmentJob?.cancel() - super.onDestroy() - } - - companion object { - const val TAG = "uploadFileSourceFragment" - - fun newInstance(bundle: Bundle): ShareFileDestinationDialog { - val uploadFileSourceFragment = ShareFileDestinationDialog() - uploadFileSourceFragment.arguments = bundle - return uploadFileSourceFragment - } - - fun createBundle(uri: Uri, courses: ArrayList): Bundle { - val bundle = Bundle() - bundle.putParcelable(Const.URI, uri) - bundle.putParcelableArrayList(Const.COURSES, courses) - return bundle - } - } -} diff --git a/apps/student/src/main/java/com/instructure/student/features/shareextension/StudentShareExtensionActivity.kt b/apps/student/src/main/java/com/instructure/student/features/shareextension/StudentShareExtensionActivity.kt new file mode 100644 index 0000000000..6cd9ede4b9 --- /dev/null +++ b/apps/student/src/main/java/com/instructure/student/features/shareextension/StudentShareExtensionActivity.kt @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2022 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.instructure.student.features.shareextension + +import android.os.Bundle +import com.instructure.pandautils.features.shareextension.ShareExtensionActivity +import com.instructure.student.activity.LoginActivity +import com.instructure.student.util.Analytics + +class StudentShareExtensionActivity : ShareExtensionActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + Analytics.trackAppFlow(this) + } + + override fun exitActivity() { + val intent = LoginActivity.createIntent(this) + startActivity(intent) + finish() + } +} \ No newline at end of file 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 f352041ff8..809228f1aa 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 @@ -35,8 +35,8 @@ import com.instructure.interactions.router.Route import com.instructure.loginapi.login.dialog.NoInternetConnectionDialog import com.instructure.pandautils.analytics.SCREEN_VIEW_DISCUSSIONS_REPLY import com.instructure.pandautils.analytics.ScreenView -import com.instructure.pandautils.dialogs.UploadFilesDialog import com.instructure.pandautils.discussions.DiscussionCaching +import com.instructure.pandautils.features.file.upload.FileUploadDialogFragment import com.instructure.pandautils.utils.* import com.instructure.pandautils.views.AttachmentView import com.instructure.student.R @@ -76,12 +76,12 @@ class DiscussionsReplyFragment : ParentFragment() { val attachments = ArrayList() if (attachment != null) attachments.add(attachment!!) - val bundle = UploadFilesDialog.createDiscussionsBundle(attachments) - UploadFilesDialog.show(fragmentManager, bundle) { event, attachment -> - if (event == UploadFilesDialog.EVENT_ON_FILE_SELECTED) { + val bundle = FileUploadDialogFragment.createDiscussionsBundle(attachments) + FileUploadDialogFragment.newInstance(bundle, pickerCallback = { event, attachment -> + if (event == FileUploadDialogFragment.EVENT_ON_FILE_SELECTED) { handleAttachment(attachment) } - } + }).show(childFragmentManager, FileUploadDialogFragment.TAG) } else { NoInternetConnectionDialog.show(requireFragmentManager()) } diff --git a/apps/student/src/main/java/com/instructure/student/fragment/FileListFragment.kt b/apps/student/src/main/java/com/instructure/student/fragment/FileListFragment.kt index 198877c353..9b2b6165bb 100644 --- a/apps/student/src/main/java/com/instructure/student/fragment/FileListFragment.kt +++ b/apps/student/src/main/java/com/instructure/student/fragment/FileListFragment.kt @@ -44,7 +44,7 @@ import com.instructure.interactions.router.Route import com.instructure.interactions.router.RouterParams import com.instructure.pandautils.analytics.SCREEN_VIEW_FILE_LIST import com.instructure.pandautils.analytics.ScreenView -import com.instructure.pandautils.dialogs.UploadFilesDialog +import com.instructure.pandautils.features.file.upload.FileUploadDialogFragment import com.instructure.pandautils.utils.* import com.instructure.student.R import com.instructure.student.adapter.FileFolderCallback @@ -406,8 +406,8 @@ class FileListFragment : ParentFragment(), Bookmarkable { private fun uploadFile() { folder?.let { - val bundle = UploadFilesDialog.createContextBundle(null, canvasContext, it.id) - UploadFilesDialog.show(fragmentManager, bundle) { _ -> } + val bundle = FileUploadDialogFragment.createContextBundle(null, canvasContext, it.id) + FileUploadDialogFragment.newInstance(bundle).show(childFragmentManager, FileUploadDialogFragment.TAG) } } diff --git a/apps/student/src/main/java/com/instructure/student/fragment/InboxComposeMessageFragment.kt b/apps/student/src/main/java/com/instructure/student/fragment/InboxComposeMessageFragment.kt index 2af3db8363..392ae0f7a7 100644 --- a/apps/student/src/main/java/com/instructure/student/fragment/InboxComposeMessageFragment.kt +++ b/apps/student/src/main/java/com/instructure/student/fragment/InboxComposeMessageFragment.kt @@ -35,7 +35,7 @@ import com.instructure.canvasapi2.utils.weave.* import com.instructure.interactions.router.Route import com.instructure.pandautils.analytics.SCREEN_VIEW_INBOX_COMPOSE import com.instructure.pandautils.analytics.ScreenView -import com.instructure.pandautils.dialogs.UploadFilesDialog +import com.instructure.pandautils.features.file.upload.FileUploadDialogFragment import com.instructure.pandautils.services.FileUploadService import com.instructure.pandautils.utils.* import com.instructure.student.R @@ -51,7 +51,6 @@ import kotlinx.android.synthetic.main.fragment_inbox_compose_message.* import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.ThreadMode -import java.util.ArrayList @ScreenView(SCREEN_VIEW_INBOX_COMPOSE) class InboxComposeMessageFragment : ParentFragment() { @@ -279,8 +278,8 @@ class InboxComposeMessageFragment : ParentFragment() { sendMessage() } R.id.menu_attachment -> { - val bundle = UploadFilesDialog.createMessageAttachmentsBundle(arrayListOf()) - UploadFilesDialog.show(fragmentManager, bundle, { _ -> }) + val bundle = FileUploadDialogFragment.createMessageAttachmentsBundle(arrayListOf()) + FileUploadDialogFragment.newInstance(bundle).show(childFragmentManager, FileUploadDialogFragment.TAG) } else -> return@setOnMenuItemClickListener false } diff --git a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/ui/AssignmentDetailsView.kt b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/ui/AssignmentDetailsView.kt index 737bf19719..a3545873c9 100644 --- a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/ui/AssignmentDetailsView.kt +++ b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/ui/AssignmentDetailsView.kt @@ -43,7 +43,7 @@ import com.instructure.pandautils.views.RecordingMediaType import com.instructure.student.R import com.instructure.student.activity.BaseRouterActivity import com.instructure.student.activity.InternalWebViewActivity -import com.instructure.student.activity.ShareFileSubmissionTarget +import com.instructure.pandautils.features.shareextension.ShareFileSubmissionTarget import com.instructure.student.fragment.* import com.instructure.student.mobius.assignmentDetails.AssignmentDetailsEvent import com.instructure.student.mobius.assignmentDetails.submission.annnotation.AnnotationSubmissionUploadFragment diff --git a/apps/student/src/main/java/com/instructure/student/router/RouteMatcher.kt b/apps/student/src/main/java/com/instructure/student/router/RouteMatcher.kt index 96a48a6a7c..a0a6d15a50 100644 --- a/apps/student/src/main/java/com/instructure/student/router/RouteMatcher.kt +++ b/apps/student/src/main/java/com/instructure/student/router/RouteMatcher.kt @@ -38,6 +38,7 @@ import com.instructure.canvasapi2.utils.Logger import com.instructure.interactions.router.* import com.instructure.pandautils.activities.BaseViewMediaActivity import com.instructure.pandautils.features.discussion.details.DiscussionDetailsWebViewFragment +import com.instructure.pandautils.features.shareextension.ShareFileSubmissionTarget import com.instructure.pandautils.loaders.OpenMediaAsyncTaskLoader import com.instructure.pandautils.utils.Const import com.instructure.pandautils.utils.LoaderUtils diff --git a/apps/student/src/main/java/com/instructure/student/util/FileUtils.kt b/apps/student/src/main/java/com/instructure/student/util/FileUtils.kt index 026a917d4c..428ccbd03d 100644 --- a/apps/student/src/main/java/com/instructure/student/util/FileUtils.kt +++ b/apps/student/src/main/java/com/instructure/student/util/FileUtils.kt @@ -24,7 +24,7 @@ import com.instructure.canvasapi2.utils.ApiPrefs import com.instructure.pandautils.loaders.OpenMediaAsyncTaskLoader import com.instructure.student.R import com.instructure.student.activity.CandroidPSPDFActivity -import com.instructure.student.activity.ShareFileSubmissionTarget +import com.instructure.pandautils.features.shareextension.ShareFileSubmissionTarget import com.pspdfkit.PSPDFKit import com.pspdfkit.annotations.AnnotationType import com.pspdfkit.configuration.activity.PdfActivityConfiguration diff --git a/apps/student/src/main/java/com/instructure/student/util/UploadCheckboxManager.kt b/apps/student/src/main/java/com/instructure/student/util/UploadCheckboxManager.kt deleted file mode 100644 index 60f37ac536..0000000000 --- a/apps/student/src/main/java/com/instructure/student/util/UploadCheckboxManager.kt +++ /dev/null @@ -1,143 +0,0 @@ -/* - * Copyright (C) 2016 - present Instructure, Inc. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, version 3 of the License. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - * - */ -package com.instructure.student.util - -import android.view.View -import android.view.ViewTreeObserver -import android.view.animation.* -import android.widget.CheckedTextView -import com.instructure.pandautils.dialogs.UploadFilesDialog -import com.instructure.student.R -import java.util.* - -class UploadCheckboxManager(private val listener: OnOptionCheckedListener, private val selectionIndicator: View) { - interface OnOptionCheckedListener { - fun onUserFilesSelected() - fun onAssignmentFilesSelected() - } - - private var checkBoxes: MutableList = ArrayList() - - var selectedCheckBox: CheckedTextView? = null - private set - - private var isAnimating = false - - fun add(checkBox: CheckedTextView) { - if (checkBoxes.size == 0) { - selectedCheckBox = checkBox - setInitialIndicatorHeight() - } - checkBoxes.add(checkBox) - checkBox.setOnClickListener(destinationClickListener) - } - - val selectedType: UploadFilesDialog.FileUploadType - get() = when (selectedCheckBox?.id) { - R.id.myFilesCheckBox -> UploadFilesDialog.FileUploadType.USER - R.id.assignmentCheckBox -> UploadFilesDialog.FileUploadType.ASSIGNMENT - else -> UploadFilesDialog.FileUploadType.USER - } - - private fun setInitialIndicatorHeight() { - selectionIndicator.viewTreeObserver.addOnGlobalLayoutListener( - object : ViewTreeObserver.OnGlobalLayoutListener { - override fun onGlobalLayout() { - selectionIndicator.viewTreeObserver.removeOnGlobalLayoutListener(this) - if (selectedCheckBox != null) { - selectionIndicator.layoutParams.height = (selectedCheckBox!!.parent as View).height - selectionIndicator.layoutParams = selectionIndicator.layoutParams - } - listener.onUserFilesSelected() - } - } - ) - } - - private fun moveIndicator(newCurrentCheckBox: CheckedTextView) { - val moveAnimation: Animation = getAnimation(newCurrentCheckBox) - selectionIndicator.startAnimation(moveAnimation) - moveAnimation.setAnimationListener(object : Animation.AnimationListener { - override fun onAnimationStart(animation: Animation) { - isAnimating = true - } - - override fun onAnimationEnd(animation: Animation) { - selectedCheckBox = newCurrentCheckBox - isAnimating = false - } - - override fun onAnimationRepeat(animation: Animation) {} - }) - } - - private fun getAnimation(toCheckBox: CheckedTextView): AnimationSet { - val toView = toCheckBox.parent as View - val fromView = selectedCheckBox!!.parent as View - - // get ratio between current height and new height - val toRatio = - toView.height.toFloat() / selectionIndicator.height.toFloat() - val fromRatio = - fromView.height.toFloat() / selectionIndicator.height.toFloat() - val scaleAnimation = ScaleAnimation( - 1f, // fromXType - 1f, // toX - fromRatio, // fromY - toRatio, // toY - .5f, // pivotX - 0.0f - ) // pivotY - val translateAnimation = TranslateAnimation( - Animation.RELATIVE_TO_SELF, 0.0f, // fromXType, fromXValue - Animation.RELATIVE_TO_SELF, 0.0f, // toXType, toXValue - Animation.ABSOLUTE, fromView.top.toFloat(), // fromYType, fromYValue - Animation.ABSOLUTE, toView.top.toFloat() - ) // toYTyp\e, toYValue - translateAnimation.interpolator = AccelerateDecelerateInterpolator() - translateAnimation.fillAfter = true - val animSet = AnimationSet(true) - animSet.addAnimation(scaleAnimation) - animSet.addAnimation(translateAnimation) - animSet.fillAfter = true - animSet.duration = 200 - return animSet - } - - private val destinationClickListener = View.OnClickListener { v: View -> - if (isAnimating) return@OnClickListener - val checkedTextView = v as CheckedTextView - if (!checkedTextView.isChecked) { - checkedTextView.isChecked = true - notifyListener(checkedTextView) - moveIndicator(checkedTextView) - for (checkBox in checkBoxes) { - if (checkBox.id != checkedTextView.id) { - checkBox.isChecked = false - } - } - } - } - - private fun notifyListener(checkedTextView: CheckedTextView) { - when (checkedTextView.id) { - R.id.myFilesCheckBox -> listener.onUserFilesSelected() - R.id.assignmentCheckBox -> listener.onAssignmentFilesSelected() - } - } - -} diff --git a/apps/student/src/main/res/layout/upload_file_destination.xml b/apps/student/src/main/res/layout/upload_file_destination.xml deleted file mode 100644 index 2cc4bc03f3..0000000000 --- a/apps/student/src/main/res/layout/upload_file_destination.xml +++ /dev/null @@ -1,192 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/apps/student/src/main/res/values/styles.xml b/apps/student/src/main/res/values/styles.xml index 94fd5a9aa0..0575a7b29c 100644 --- a/apps/student/src/main/res/values/styles.xml +++ b/apps/student/src/main/res/values/styles.xml @@ -110,7 +110,7 @@ - - - From 60fdfa5c51dca0dfa139165e4dcba703ec6088cc Mon Sep 17 00:00:00 2001 From: Kristof Deak <92309696+kdeakinstructure@users.noreply.github.com> Date: Fri, 2 Sep 2022 14:13:15 +0200 Subject: [PATCH 28/49] [MBL-16206][Teacher] - Insert page creation flow on UI. (#1691) refs: MBL-16206 affects: Teacher release note: none --- .../teacher/ui/e2e/PagesE2ETest.kt | 20 +++++++++++++++++++ .../teacher/ui/pages/EditPageDetailsPage.kt | 16 ++++++++------- .../teacher/ui/pages/PageListPage.kt | 7 ++++++- 3 files changed, 35 insertions(+), 8 deletions(-) diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/PagesE2ETest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/PagesE2ETest.kt index 45df93cd8c..f271eb90ac 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/PagesE2ETest.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/PagesE2ETest.kt @@ -146,6 +146,26 @@ class PagesE2ETest : TeacherTest() { Log.d(STEP_TAG,"Assert that ${testPage2.title} is displayed as a front page.") pageListPage.assertFrontPageDisplayed(testPage2.title) + Log.d(STEP_TAG,"Click on '+' icon on the UI to create a new page.") + pageListPage.clickOnCreateNewPage() + + val newPageTitle = "Test Page Mobile UI" + Log.d(STEP_TAG,"Set '$newPageTitle' as the page's title and set some description as well.") + editPageDetailsPage.editPageName(newPageTitle) + editPageDetailsPage.editDescription("Mobile UI Page description") + + Log.d(STEP_TAG,"Toggle Publish checkbox and save the page.") + editPageDetailsPage.togglePublished() + editPageDetailsPage.savePage() + + Log.d(STEP_TAG,"Assert that '$newPageTitle' page is displayed and published.") + pageListPage.assertPageIsPublished(newPageTitle) + Log.d(STEP_TAG,"Click on the Search icon and type some search query string which matches only with the previously created page's title.") + pageListPage.openSearch() + pageListPage.enterSearchQuery("Test") + Log.d(STEP_TAG,"Assert that the '$newPageTitle' titled page is displayed and it is the only one.") + pageListPage.assertPageIsPublished(newPageTitle) + pageListPage.assertPageCount(1) } } \ No newline at end of file diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/EditPageDetailsPage.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/EditPageDetailsPage.kt index 83bad7811a..628553e06c 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/EditPageDetailsPage.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/EditPageDetailsPage.kt @@ -1,6 +1,7 @@ package com.instructure.teacher.ui.pages -import androidx.test.espresso.matcher.ViewMatchers.* +import androidx.test.espresso.matcher.ViewMatchers.isDisplayed +import androidx.test.espresso.matcher.ViewMatchers.withId import androidx.test.espresso.web.assertion.WebViewAssertions.webMatches import androidx.test.espresso.web.sugar.Web.onWebView import androidx.test.espresso.web.webdriver.DriverAtoms.findElement @@ -8,19 +9,16 @@ import androidx.test.espresso.web.webdriver.DriverAtoms.getText import androidx.test.espresso.web.webdriver.Locator import com.instructure.canvas.espresso.checkToastText import com.instructure.canvas.espresso.withElementRepeat -import com.instructure.espresso.ActivityHelper -import com.instructure.espresso.click +import com.instructure.espresso.* import com.instructure.espresso.page.BasePage import com.instructure.espresso.page.onView -import com.instructure.espresso.page.plus -import com.instructure.espresso.page.withParent -import com.instructure.espresso.replaceText -import com.instructure.espresso.scrollTo import com.instructure.teacher.R +import com.instructure.teacher.ui.utils.TypeInRCETextEditor import org.hamcrest.Matchers.allOf import org.hamcrest.Matchers.containsString class EditPageDetailsPage : BasePage() { + private val contentRceView by WaitForViewWithId(R.id.rce_webView) fun runTextChecks(vararg checks : WebViewTextCheck) { for(check in checks) { @@ -59,6 +57,10 @@ class EditPageDetailsPage : BasePage() { onView(withId(R.id.pageNameEditText)).replaceText(editedPageName) } + fun editDescription(newDescription: String) { + contentRceView.perform(TypeInRCETextEditor(newDescription)) + } + fun unableToSaveUnpublishedFrontPage() { savePage() checkToastText(R.string.frontPageUnpublishedError, ActivityHelper.currentActivity()) diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/PageListPage.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/PageListPage.kt index fe66751c2b..b0f5afcb25 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/PageListPage.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/PageListPage.kt @@ -18,7 +18,8 @@ package com.instructure.teacher.ui.pages import android.view.View import androidx.test.espresso.Espresso import androidx.test.espresso.action.ViewActions -import androidx.test.espresso.matcher.ViewMatchers.* +import androidx.test.espresso.matcher.ViewMatchers.hasSibling +import androidx.test.espresso.matcher.ViewMatchers.withContentDescription import com.instructure.canvas.espresso.scrollRecyclerView import com.instructure.canvasapi2.models.Page import com.instructure.espresso.* @@ -38,6 +39,10 @@ class PageListPage : BasePage() { private val toolbar by OnViewWithId(R.id.pageListToolbar) + fun clickOnCreateNewPage() { + onView(withId(R.id.createNewPage)).click() + } + fun assertHasPage(page: Page) { waitForViewWithText(page.title!!).assertDisplayed() } From 42f9e05073f496b0f1547cf064c82a3234a5fa93 Mon Sep 17 00:00:00 2001 From: Kristof Nemere <109959688+kristofnemere@users.noreply.github.com> Date: Fri, 2 Sep 2022 14:16:58 +0200 Subject: [PATCH 29/49] [MBL-16141][Student] Share extension | Dashboard notification refs: MBL-16141 affects: Student release note: none * wip: share extension upload dashboard notifications * removing observer in onCleared * Added upload notification unit tests * Fixed subtitle * On running workers change just updating uploads not calling the loadData * minor tweak * Fixed findings --- .../src/main/res/drawable/ic_upload.xml | 10 ++ libs/pandares/src/main/res/values/strings.xml | 3 + libs/pandautils/build.gradle | 2 + .../pandautils/di/FileUploadModule.kt | 7 +- .../DashboardNotificationsFragment.kt | 17 ++- .../DashboardNotificationsViewData.kt | 25 +++- .../DashboardNotificationsViewModel.kt | 56 +++++-- .../itemviewmodels/UploadItemViewModel.kt | 34 +++++ .../preferences/FileUploadPreferences.kt | 13 +- .../file/upload/worker/FileUploadWorker.kt | 32 +++- .../fragment_dashboard_notifications.xml | 4 +- .../main/res/layout/item_dashboard_upload.xml | 117 +++++++++++++++ .../DashboardNotificationsViewModelTest.kt | 141 +++++++++++++++--- .../ShareExtensionTargetViewModelTest.kt | 6 - 14 files changed, 399 insertions(+), 68 deletions(-) create mode 100644 libs/pandares/src/main/res/drawable/ic_upload.xml create mode 100644 libs/pandautils/src/main/java/com/instructure/pandautils/features/dashboard/notifications/itemviewmodels/UploadItemViewModel.kt create mode 100644 libs/pandautils/src/main/res/layout/item_dashboard_upload.xml diff --git a/libs/pandares/src/main/res/drawable/ic_upload.xml b/libs/pandares/src/main/res/drawable/ic_upload.xml new file mode 100644 index 0000000000..eea4fa9ffd --- /dev/null +++ b/libs/pandares/src/main/res/drawable/ic_upload.xml @@ -0,0 +1,10 @@ + + + diff --git a/libs/pandares/src/main/res/values/strings.xml b/libs/pandares/src/main/res/values/strings.xml index 651cb50ee9..cfa06b55f6 100644 --- a/libs/pandares/src/main/res/values/strings.xml +++ b/libs/pandares/src/main/res/values/strings.xml @@ -1283,4 +1283,7 @@ Weekly Never Select frequency + + Uploading Submission + Uploading files diff --git a/libs/pandautils/build.gradle b/libs/pandautils/build.gradle index a3c3d773d3..823116b65c 100644 --- a/libs/pandautils/build.gradle +++ b/libs/pandautils/build.gradle @@ -109,6 +109,8 @@ dependencies { testImplementation Libs.ANDROIDX_CORE_TESTING testImplementation Libs.THREETEN_BP + testImplementation project(path: ':pandautils') + /* Media handling */ api(Libs.GLIDE) { exclude group: "com.android.support" diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/di/FileUploadModule.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/di/FileUploadModule.kt index 871f8a2276..1d337f32d9 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/di/FileUploadModule.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/di/FileUploadModule.kt @@ -19,6 +19,7 @@ package com.instructure.pandautils.di import android.content.ContentResolver import android.content.Context import com.instructure.pandautils.features.file.upload.FileUploadUtilsHelper +import com.instructure.pandautils.features.file.upload.preferences.FileUploadPreferences import com.instructure.pandautils.features.file.upload.worker.FileUploadBundleCreator import com.instructure.pandautils.utils.FileUploadUtils import dagger.Module @@ -26,7 +27,6 @@ import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent -import javax.inject.Singleton @Module @InstallIn(SingletonComponent::class) @@ -46,4 +46,9 @@ class FileUploadModule { fun provideFileUploadBundleCreator(): FileUploadBundleCreator { return FileUploadBundleCreator() } + + @Provides + fun provideFileUploadPreferences(): FileUploadPreferences { + return FileUploadPreferences + } } \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/dashboard/notifications/DashboardNotificationsFragment.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/dashboard/notifications/DashboardNotificationsFragment.kt index 639ed2c456..9e8377366b 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/dashboard/notifications/DashboardNotificationsFragment.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/dashboard/notifications/DashboardNotificationsFragment.kt @@ -59,11 +59,11 @@ class DashboardNotificationsFragment : Fragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - viewModel.events.observe(viewLifecycleOwner, { event -> + viewModel.events.observe(viewLifecycleOwner) { event -> event.getContentIfNotHandled()?.let { handleAction(it) } - }) + } } fun refresh() { @@ -74,14 +74,14 @@ class DashboardNotificationsFragment : Fragment() { when (action) { is DashboardNotificationsActions.LaunchConference -> { val colorSchemeParams = CustomTabColorSchemeParams.Builder() - .setToolbarColor(ColorKeeper.getOrGenerateColor(action.canvasContext)) - .build() + .setToolbarColor(ColorKeeper.getOrGenerateColor(action.canvasContext)) + .build() var intent = CustomTabsIntent.Builder() - .setDefaultColorSchemeParams(colorSchemeParams) - .setShowTitle(true) - .build() - .intent + .setDefaultColorSchemeParams(colorSchemeParams) + .setShowTitle(true) + .build() + .intent intent.data = Uri.parse(action.url) @@ -97,6 +97,7 @@ class DashboardNotificationsFragment : Fragment() { action.subject, action.message ) + is DashboardNotificationsActions.OpenProgressDialog -> {} } } diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/dashboard/notifications/DashboardNotificationsViewData.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/dashboard/notifications/DashboardNotificationsViewData.kt index 58fe29dc3b..4b43a68358 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/dashboard/notifications/DashboardNotificationsViewData.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/dashboard/notifications/DashboardNotificationsViewData.kt @@ -17,13 +17,20 @@ package com.instructure.pandautils.features.dashboard.notifications import androidx.annotation.DrawableRes +import androidx.databinding.BaseObservable +import androidx.databinding.Bindable import com.instructure.canvasapi2.models.CanvasContext import com.instructure.canvasapi2.models.Conference import com.instructure.pandautils.mvvm.ItemViewModel +import java.util.* data class DashboardNotificationsViewData( - val items: List -) + val items: List, + var uploadItems: List +) : BaseObservable() { + @Bindable + fun getConcatenatedItems() = uploadItems + items +} data class InvitationViewData( val title: String, @@ -45,8 +52,16 @@ data class AnnouncementViewData( @DrawableRes val icon: Int ) +data class UploadViewData( + val title: String, + val subTitle: String, + val color: String, + @DrawableRes val icon: Int +) + sealed class DashboardNotificationsActions { - data class ShowToast(val toast: String): DashboardNotificationsActions() - data class LaunchConference(val canvasContext: CanvasContext, val url: String): DashboardNotificationsActions() - data class OpenAnnouncement(val subject: String, val message: String): DashboardNotificationsActions() + data class ShowToast(val toast: String) : DashboardNotificationsActions() + data class LaunchConference(val canvasContext: CanvasContext, val url: String) : DashboardNotificationsActions() + data class OpenAnnouncement(val subject: String, val message: String) : DashboardNotificationsActions() + data class OpenProgressDialog(val uuid: UUID): DashboardNotificationsActions() } diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/dashboard/notifications/DashboardNotificationsViewModel.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/dashboard/notifications/DashboardNotificationsViewModel.kt index b2c31b1ab0..90a8972092 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/dashboard/notifications/DashboardNotificationsViewModel.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/dashboard/notifications/DashboardNotificationsViewModel.kt @@ -19,21 +19,13 @@ package com.instructure.pandautils.features.dashboard.notifications import android.content.res.Resources import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.Observer import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import androidx.work.WorkManager import com.instructure.canvasapi2.apis.EnrollmentAPI -import com.instructure.canvasapi2.managers.AccountNotificationManager -import com.instructure.canvasapi2.managers.ConferenceManager -import com.instructure.canvasapi2.managers.CourseManager -import com.instructure.canvasapi2.managers.EnrollmentManager -import com.instructure.canvasapi2.managers.GroupManager -import com.instructure.canvasapi2.managers.OAuthManager -import com.instructure.canvasapi2.models.AccountNotification -import com.instructure.canvasapi2.models.CanvasContext -import com.instructure.canvasapi2.models.Conference -import com.instructure.canvasapi2.models.Course -import com.instructure.canvasapi2.models.Enrollment -import com.instructure.canvasapi2.models.Group +import com.instructure.canvasapi2.managers.* +import com.instructure.canvasapi2.models.* import com.instructure.canvasapi2.utils.ApiPrefs import com.instructure.canvasapi2.utils.isValidTerm import com.instructure.pandautils.BR @@ -41,6 +33,10 @@ import com.instructure.pandautils.R import com.instructure.pandautils.features.dashboard.notifications.itemviewmodels.AnnouncementItemViewModel import com.instructure.pandautils.features.dashboard.notifications.itemviewmodels.ConferenceItemViewModel import com.instructure.pandautils.features.dashboard.notifications.itemviewmodels.InvitationItemViewModel +import com.instructure.pandautils.features.dashboard.notifications.itemviewmodels.UploadItemViewModel +import com.instructure.pandautils.features.file.upload.preferences.FileUploadPreferences +import com.instructure.pandautils.features.file.upload.worker.FileUploadWorker.Companion.PROGRESS_DATA_SUBTITLE +import com.instructure.pandautils.features.file.upload.worker.FileUploadWorker.Companion.PROGRESS_DATA_TITLE import com.instructure.pandautils.models.ConferenceDashboardBlacklist import com.instructure.pandautils.mvvm.Event import com.instructure.pandautils.mvvm.ItemViewModel @@ -63,7 +59,9 @@ class DashboardNotificationsViewModel @Inject constructor( private val accountNotificationManager: AccountNotificationManager, private val oauthManager: OAuthManager, private val conferenceDashboardBlacklist: ConferenceDashboardBlacklist, - private val apiPrefs: ApiPrefs + private val apiPrefs: ApiPrefs, + private val workManager: WorkManager, + private val fileUploadPreferences: FileUploadPreferences ) : ViewModel() { val state: LiveData @@ -81,6 +79,20 @@ class DashboardNotificationsViewModel @Inject constructor( private var coursesMap: Map = emptyMap() private var groupMap: Map = emptyMap() + private val runningWorkersObserver = Observer> { + _data.value?.uploadItems = getUploads(it) + _data.value?.notifyPropertyChanged(BR.concatenatedItems) + } + + init { + fileUploadPreferences.getRunningWorkersLiveData().observeForever(runningWorkersObserver) + } + + override fun onCleared() { + fileUploadPreferences.getRunningWorkersLiveData().removeObserver(runningWorkersObserver) + super.onCleared() + } + fun loadData(forceNetwork: Boolean = false) { viewModelScope.launch { @@ -102,7 +114,9 @@ class DashboardNotificationsViewModel @Inject constructor( val conferenceViewModels = getConferences(forceNetwork) items.addAll(conferenceViewModels) - _data.postValue(DashboardNotificationsViewData(items)) + val uploadViewModels = getUploads(fileUploadPreferences.getRunningWorkerIds()) + + _data.postValue(DashboardNotificationsViewData(items, uploadViewModels)) } } @@ -204,6 +218,20 @@ class DashboardNotificationsViewModel @Inject constructor( } } + private fun getUploads(runningWorkerIds: List) = runningWorkerIds.map { + val workInfo = workManager.getWorkInfoById(it).get() + UploadItemViewModel( + it, UploadViewData( + workInfo.progress.getString(PROGRESS_DATA_TITLE).orEmpty(), + workInfo.progress.getString(PROGRESS_DATA_SUBTITLE).orEmpty(), + "#${resources.getColor(R.color.backgroundInfo).toHexString()}", + R.drawable.ic_upload + ) + ) { uuid -> + _events.postValue(Event(DashboardNotificationsActions.OpenProgressDialog(uuid))) + } + } + private fun hasValidCourseForEnrollment(enrollment: Enrollment): Boolean { return coursesMap[enrollment.courseId]?.let { course -> course.isValidTerm() && !course.accessRestrictedByDate && isEnrollmentBeforeEndDateOrNotRestricted(course) diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/dashboard/notifications/itemviewmodels/UploadItemViewModel.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/dashboard/notifications/itemviewmodels/UploadItemViewModel.kt new file mode 100644 index 0000000000..912b0c0472 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/dashboard/notifications/itemviewmodels/UploadItemViewModel.kt @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2022 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.instructure.pandautils.features.dashboard.notifications.itemviewmodels + +import androidx.databinding.BaseObservable +import com.instructure.pandautils.R +import com.instructure.pandautils.features.dashboard.notifications.UploadViewData +import com.instructure.pandautils.mvvm.ItemViewModel +import java.util.* + +class UploadItemViewModel( + private val workerId: UUID, + val data: UploadViewData, + val open: (UUID) -> Unit +) : ItemViewModel, BaseObservable() { + override val layoutId = R.layout.item_dashboard_upload + + fun open() = open.invoke(workerId) +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/file/upload/preferences/FileUploadPreferences.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/file/upload/preferences/FileUploadPreferences.kt index 00f395e786..54b0ecc650 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/file/upload/preferences/FileUploadPreferences.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/file/upload/preferences/FileUploadPreferences.kt @@ -25,28 +25,27 @@ import java.util.* object FileUploadPreferences : PrefManager("fileUploadPrefs") { private var runningWorkerIds by StringSetPref() - - private val runningWorkersLiveData = MutableLiveData(getRunningWorkerIds()) + private var runningWorkersLiveData: MutableLiveData>? = null fun addWorkerId(id: UUID) { runningWorkerIds = runningWorkerIds + id.toString() - runningWorkersLiveData.postValue(getRunningWorkerIds()) + runningWorkersLiveData?.postValue(getRunningWorkerIds()) } fun removeWorkerId(id: UUID) { val idString = id.toString() if (runningWorkerIds.contains(idString)) { runningWorkerIds = runningWorkerIds - id.toString() - runningWorkersLiveData.postValue(getRunningWorkerIds()) + runningWorkersLiveData?.postValue(getRunningWorkerIds()) } } - private fun getRunningWorkerIds(): List { + fun getRunningWorkerIds(): List { return runningWorkerIds.map { UUID.fromString(it) } } fun getRunningWorkersLiveData(): LiveData> { - return runningWorkersLiveData + if (runningWorkersLiveData == null) runningWorkersLiveData = MutableLiveData(getRunningWorkerIds()) + return runningWorkersLiveData!! } - } \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/file/upload/worker/FileUploadWorker.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/file/upload/worker/FileUploadWorker.kt index c2e2c1c5d0..1bcae3984e 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/file/upload/worker/FileUploadWorker.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/file/upload/worker/FileUploadWorker.kt @@ -54,7 +54,31 @@ class FileUploadWorker(private val context: Context, private val workerParameter override suspend fun doWork(): Result { try { + var assignmentName = "" + var groupId: Long? = null + if (assignmentId != INVALID_ID && courseId != INVALID_ID) { + val assignment = getAssignment(assignmentId, courseId) + groupId = getGroupId(assignment, courseId) + assignmentName = assignment.name.orEmpty() + } + + val title = context.getString( + if (action == ACTION_ASSIGNMENT_SUBMISSION) { + R.string.dashboardNotificationUploadingSubmissionTitle + } else { + R.string.dashboardNotificationUploadingFilesTitle + } + ) + + setProgress( + Data.Builder() + .putString(PROGRESS_DATA_TITLE, title) + .putString(PROGRESS_DATA_SUBTITLE, assignmentName) + .build() + ) + FileUploadPreferences.addWorkerId(id) + val filePaths = inputData.getStringArray(FILE_PATHS) val fileSubmitObjects = filePaths?.let { @@ -63,12 +87,6 @@ class FileUploadWorker(private val context: Context, private val workerParameter uploadCount = fileSubmitObjects.size - var groupId: Long? = null - if (assignmentId != INVALID_ID && courseId != INVALID_ID) { - val assignment = getAssignment(assignmentId, courseId) - groupId = getGroupId(assignment, courseId) - } - val attachments = uploadFiles(fileSubmitObjects, groupId) val attachmentsIds = attachments.map { it.id }.plus(inputData.getLongArray(Const.ATTACHMENTS)?.toList() @@ -242,5 +260,7 @@ class FileUploadWorker(private val context: Context, private val workerParameter const val RESULT_ATTACHMENTS = "attachments" + const val PROGRESS_DATA_TITLE = "PROGRESS_DATA_TITLE" + const val PROGRESS_DATA_SUBTITLE = "PROGRESS_DATA_SUBTITLE" } } \ No newline at end of file diff --git a/libs/pandautils/src/main/res/layout/fragment_dashboard_notifications.xml b/libs/pandautils/src/main/res/layout/fragment_dashboard_notifications.xml index ee60d77c95..b59b0d311d 100644 --- a/libs/pandautils/src/main/res/layout/fragment_dashboard_notifications.xml +++ b/libs/pandautils/src/main/res/layout/fragment_dashboard_notifications.xml @@ -32,6 +32,6 @@ android:layout_height="wrap_content" android:orientation="vertical" android:paddingTop="12dp" - android:visibility="@{viewModel.data.items.size() == 0 ? View.GONE : View.VISIBLE}" - app:itemViewModels="@{viewModel.data.items}" /> + android:visibility="@{viewModel.data.concatenatedItems.size() == 0 ? View.GONE : View.VISIBLE}" + app:itemViewModels="@{viewModel.data.concatenatedItems}" /> \ No newline at end of file diff --git a/libs/pandautils/src/main/res/layout/item_dashboard_upload.xml b/libs/pandautils/src/main/res/layout/item_dashboard_upload.xml new file mode 100644 index 0000000000..7f603435fb --- /dev/null +++ b/libs/pandautils/src/main/res/layout/item_dashboard_upload.xml @@ -0,0 +1,117 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/libs/pandautils/src/test/java/com/instructure/pandautils/features/dashboard/notifications/DashboardNotificationsViewModelTest.kt b/libs/pandautils/src/test/java/com/instructure/pandautils/features/dashboard/notifications/DashboardNotificationsViewModelTest.kt index 054c30bf61..e526fbced1 100644 --- a/libs/pandautils/src/test/java/com/instructure/pandautils/features/dashboard/notifications/DashboardNotificationsViewModelTest.kt +++ b/libs/pandautils/src/test/java/com/instructure/pandautils/features/dashboard/notifications/DashboardNotificationsViewModelTest.kt @@ -20,9 +20,10 @@ import android.content.Context import android.content.res.Resources import android.graphics.Color import androidx.arch.core.executor.testing.InstantTaskExecutorRule -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.LifecycleOwner -import androidx.lifecycle.LifecycleRegistry +import androidx.lifecycle.* +import androidx.work.Data +import androidx.work.WorkInfo +import androidx.work.WorkManager import com.instructure.canvasapi2.apis.EnrollmentAPI import com.instructure.canvasapi2.managers.* import com.instructure.canvasapi2.models.* @@ -33,24 +34,23 @@ import com.instructure.pandautils.R import com.instructure.pandautils.features.dashboard.notifications.itemviewmodels.AnnouncementItemViewModel import com.instructure.pandautils.features.dashboard.notifications.itemviewmodels.ConferenceItemViewModel import com.instructure.pandautils.features.dashboard.notifications.itemviewmodels.InvitationItemViewModel +import com.instructure.pandautils.features.dashboard.notifications.itemviewmodels.UploadItemViewModel +import com.instructure.pandautils.features.file.upload.preferences.FileUploadPreferences +import com.instructure.pandautils.features.file.upload.worker.FileUploadWorker import com.instructure.pandautils.models.ConferenceDashboardBlacklist -import com.instructure.pandautils.mvvm.ItemViewModel -import com.instructure.pandautils.utils.ThemePrefs -import io.mockk.coEvery -import io.mockk.every -import io.mockk.mockk -import io.mockk.mockkStatic +import io.mockk.* import junit.framework.Assert.assertEquals import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.TestCoroutineDispatcher import kotlinx.coroutines.test.setMain import okhttp3.internal.toHexString +import org.junit.After import org.junit.Before import org.junit.Rule import org.junit.Test import org.mockito.Mockito -import kotlin.math.exp +import java.util.* @ExperimentalCoroutinesApi class DashboardNotificationsViewModelTest { @@ -72,6 +72,7 @@ class DashboardNotificationsViewModelTest { private val oauthManager: OAuthManager = mockk(relaxed = true) private val conferenceDashboardBlacklist: ConferenceDashboardBlacklist = mockk(relaxed = true) private val apiPrefs: ApiPrefs = mockk(relaxed = true) + private val workManager: WorkManager = mockk(relaxed = true) private lateinit var viewModel: DashboardNotificationsViewModel @@ -108,22 +109,33 @@ class DashboardNotificationsViewModelTest { coEvery { await() } returns DataResult.Success(emptyList()) } + mockkObject(FileUploadPreferences) + every { FileUploadPreferences.getRunningWorkerIds() } returns emptyList() + every { FileUploadPreferences.getRunningWorkersLiveData() } returns MutableLiveData(emptyList()) + viewModel = DashboardNotificationsViewModel( - resources, - courseManager, - groupManager, - enrollmentManager, - conferenceManager, - accountNotificationManager, - oauthManager, - conferenceDashboardBlacklist, - apiPrefs + resources, + courseManager, + groupManager, + enrollmentManager, + conferenceManager, + accountNotificationManager, + oauthManager, + conferenceDashboardBlacklist, + apiPrefs, + workManager, + FileUploadPreferences ) viewModel.data.observe(lifecycleOwner, {}) viewModel.events.observe(lifecycleOwner, {}) } + @After + fun tearDown() { + unmockkObject(FileUploadPreferences) + } + private fun setupResources() { every { resources.getColor(R.color.backgroundDanger) } returns Color.parseColor("#EE0612") every { resources.getColor(R.color.backgroundWarning) } returns Color.parseColor("#FC5E13") @@ -409,4 +421,95 @@ class DashboardNotificationsViewModelTest { assertEquals(expectedData[index], (itemViewModel as ConferenceItemViewModel).data) } } + + @Test + fun `Upload map correctly`() { + val workerId = UUID.randomUUID() + val title = "Title" + val subTitle = "SubTitle" + + every { FileUploadPreferences.getRunningWorkerIds() } returns listOf(workerId) + every { workManager.getWorkInfoById(workerId).get() } returns WorkInfo( + workerId, + WorkInfo.State.RUNNING, + Data.EMPTY, + emptyList(), + Data.Builder() + .putString(FileUploadWorker.PROGRESS_DATA_TITLE, title) + .putString(FileUploadWorker.PROGRESS_DATA_SUBTITLE, subTitle) + .build(), + 1 + ) + + val expectedData = listOf( + UploadViewData( + title, + subTitle, + "#${resources.getColor(R.color.backgroundInfo).toHexString()}", + R.drawable.ic_upload + ) + ) + + viewModel.loadData() + + viewModel.data.value?.uploadItems?.forEachIndexed { index, itemViewModel -> + assert(itemViewModel is UploadItemViewModel) + assertEquals(expectedData[index], (itemViewModel as UploadItemViewModel).data) + } + } + + @Test + fun `Upload notification shows up and disappears when it's finished`() { + val workerId = UUID.randomUUID() + + every { FileUploadPreferences.getRunningWorkerIds() } returns listOf(workerId) + every { FileUploadPreferences.getRunningWorkersLiveData() } returns MutableLiveData(listOf(workerId)) + every { workManager.getWorkInfoById(workerId).get() } returns WorkInfo( + workerId, + WorkInfo.State.RUNNING, + Data.EMPTY, + emptyList(), + Data.EMPTY, + 1 + ) + + viewModel.loadData() + assertEquals(false, viewModel.data.value?.uploadItems?.isEmpty()) + + every { FileUploadPreferences.getRunningWorkersLiveData() } returns MutableLiveData(emptyList()) + every { FileUploadPreferences.getRunningWorkerIds() } returns emptyList() + + viewModel.loadData() + assertEquals(true, viewModel.data.value?.uploadItems?.isEmpty()) + } + + @Test + fun `Open progress dialog`() { + val workerId = UUID.randomUUID() + + every { FileUploadPreferences.getRunningWorkerIds() } returns listOf(workerId) + every { FileUploadPreferences.getRunningWorkersLiveData() } returns MutableLiveData(listOf(workerId)) + every { workManager.getWorkInfoById(workerId).get() } returns WorkInfo( + workerId, + WorkInfo.State.RUNNING, + Data.EMPTY, + emptyList(), + Data.EMPTY, + 1 + ) + + viewModel.loadData() + + val itemViewModel = viewModel.data.value?.uploadItems?.first() + assert(itemViewModel is UploadItemViewModel) + + itemViewModel as UploadItemViewModel + itemViewModel.open(workerId) + + val event = viewModel.events.value?.getContentIfNotHandled() + assert(event is DashboardNotificationsActions.OpenProgressDialog) + + event as DashboardNotificationsActions.OpenProgressDialog + assertEquals(workerId, event.uuid) + } } \ No newline at end of file diff --git a/libs/pandautils/src/test/java/com/instructure/pandautils/features/shareextension/target/ShareExtensionTargetViewModelTest.kt b/libs/pandautils/src/test/java/com/instructure/pandautils/features/shareextension/target/ShareExtensionTargetViewModelTest.kt index cebc6a6988..b2d57b8f28 100644 --- a/libs/pandautils/src/test/java/com/instructure/pandautils/features/shareextension/target/ShareExtensionTargetViewModelTest.kt +++ b/libs/pandautils/src/test/java/com/instructure/pandautils/features/shareextension/target/ShareExtensionTargetViewModelTest.kt @@ -17,7 +17,6 @@ package com.instructure.pandautils.features.shareextension.target import android.content.res.Resources -import android.graphics.Canvas import androidx.arch.core.executor.testing.InstantTaskExecutorRule import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleOwner @@ -25,18 +24,13 @@ import androidx.lifecycle.LifecycleRegistry import com.instructure.canvasapi2.managers.AssignmentManager import com.instructure.canvasapi2.managers.CourseManager import com.instructure.canvasapi2.models.Assignment -import com.instructure.canvasapi2.models.CanvasContext import com.instructure.canvasapi2.models.Course import com.instructure.canvasapi2.models.User import com.instructure.canvasapi2.utils.ApiPrefs -import com.instructure.canvasapi2.utils.ColorPref import com.instructure.canvasapi2.utils.DataResult import com.instructure.pandautils.R import com.instructure.pandautils.features.file.upload.FileUploadType -import com.instructure.pandautils.features.shareextension.target.itemviewmodels.ShareExtensionAssignmentItemViewModel import com.instructure.pandautils.utils.ColorKeeper -import com.instructure.pandautils.utils.ThemePrefs -import com.instructure.pandautils.utils.color import io.mockk.* import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi From 7165ec47b57c83d7f512dd79f97c50a6b007e065 Mon Sep 17 00:00:00 2001 From: Kristof Nemere <109959688+kristofnemere@users.noreply.github.com> Date: Mon, 5 Sep 2022 11:40:58 +0200 Subject: [PATCH 30/49] [MBL-16220][Student] Calendar needs to be refreshed day by day after filter refs: MBL-16220 affects: Student release note: none --- libs/flutter_student_embed/lib/network/utils/dio_config.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/flutter_student_embed/lib/network/utils/dio_config.dart b/libs/flutter_student_embed/lib/network/utils/dio_config.dart index c17ed9fc79..83f164f3d7 100644 --- a/libs/flutter_student_embed/lib/network/utils/dio_config.dart +++ b/libs/flutter_student_embed/lib/network/utils/dio_config.dart @@ -167,7 +167,7 @@ class DioConfig { if (path == null) { return DioCacheManager(CacheConfig(baseUrl: baseUrl)).clearAll(); } else { - return DioCacheManager(CacheConfig(baseUrl: baseUrl)).deleteByPrimaryKey(path); + return DioCacheManager(CacheConfig(baseUrl: baseUrl)).deleteByPrimaryKey(path, requestMethod: "GET"); } } } From 8dfd3df7c2d555a8d4dc0307b77d23b9a724b539 Mon Sep 17 00:00:00 2001 From: Kristof Nemere <109959688+kristofnemere@users.noreply.github.com> Date: Mon, 5 Sep 2022 14:56:31 +0200 Subject: [PATCH 31/49] [MBL-15753][Parent] Add back the userId param when the api is fixed refs: MBL-15753 affects: Parent release note: none --- apps/flutter_parent/lib/network/api/enrollments_api.dart | 2 +- .../lib/screens/courses/details/course_details_model.dart | 2 -- .../test/screens/courses/course_details_model_test.dart | 3 +-- .../test/screens/courses/course_grades_screen_test.dart | 6 ++---- 4 files changed, 4 insertions(+), 9 deletions(-) diff --git a/apps/flutter_parent/lib/network/api/enrollments_api.dart b/apps/flutter_parent/lib/network/api/enrollments_api.dart index 848d579e30..c373b780fe 100644 --- a/apps/flutter_parent/lib/network/api/enrollments_api.dart +++ b/apps/flutter_parent/lib/network/api/enrollments_api.dart @@ -41,7 +41,7 @@ class EnrollmentsApi { final dio = canvasDio(forceRefresh: forceRefresh); final params = { 'state[]': ['active', 'completed'], // current_and_concluded state not supported for observers - //'user_id': studentId, <-- add this back when the api is fixed + 'user_id': studentId, if (gradingPeriodId?.isNotEmpty == true) 'grading_period_id': gradingPeriodId, }; diff --git a/apps/flutter_parent/lib/screens/courses/details/course_details_model.dart b/apps/flutter_parent/lib/screens/courses/details/course_details_model.dart index 163457ffcb..60eea98f46 100644 --- a/apps/flutter_parent/lib/screens/courses/details/course_details_model.dart +++ b/apps/flutter_parent/lib/screens/courses/details/course_details_model.dart @@ -98,8 +98,6 @@ class CourseDetailsModel extends BaseModel { final enrollmentsFuture = _interactor() .loadEnrollmentsForGradingPeriod(courseId, student.id, _nextGradingPeriod?.id, forceRefresh: forceRefresh) ?.then((enrollments) { - enrollments = enrollments - .where((element) => element.userId == student.id).toList(); return enrollments.length > 0 ? enrollments.first : null; })?.catchError((_) => null); // Some 'legacy' parents can't read grades for students, so catch and return null diff --git a/apps/flutter_parent/test/screens/courses/course_details_model_test.dart b/apps/flutter_parent/test/screens/courses/course_details_model_test.dart index 34749806dd..3285c6bf1f 100644 --- a/apps/flutter_parent/test/screens/courses/course_details_model_test.dart +++ b/apps/flutter_parent/test/screens/courses/course_details_model_test.dart @@ -150,8 +150,7 @@ void main() { // Initial setup final termEnrollment = Enrollment((b) => b ..id = '10' - ..enrollmentState = 'active' - ..userId = _studentId); + ..enrollmentState = 'active'); final gradingPeriods = [ GradingPeriod((b) => b ..id = '123' diff --git a/apps/flutter_parent/test/screens/courses/course_grades_screen_test.dart b/apps/flutter_parent/test/screens/courses/course_grades_screen_test.dart index db0ae0ef27..7e03ef1cc9 100644 --- a/apps/flutter_parent/test/screens/courses/course_grades_screen_test.dart +++ b/apps/flutter_parent/test/screens/courses/course_grades_screen_test.dart @@ -326,8 +326,7 @@ void main() { ]; final enrollment = Enrollment((b) => b ..enrollmentState = 'active' - ..grades = _mockGrade(currentScore: 1.2345) - ..userId = _studentId); + ..grades = _mockGrade(currentScore: 1.2345)); final model = CourseDetailsModel(_student, _courseId); model.course = _mockCourse(); @@ -351,8 +350,7 @@ void main() { ]; final enrollment = Enrollment((b) => b ..enrollmentState = 'active' - ..grades = _mockGrade(currentGrade: grade) - ..userId = _studentId); + ..grades = _mockGrade(currentGrade: grade)); final model = CourseDetailsModel(_student, _courseId); model.course = _mockCourse(); when(interactor.loadAssignmentGroups(_courseId, _studentId, null)).thenAnswer((_) async => groups); From 7f491611c0644cd46d7fa390254b460df6a79ddb Mon Sep 17 00:00:00 2001 From: balintbartok <72031065+balintbartok@users.noreply.github.com> Date: Tue, 6 Sep 2022 11:20:21 +0200 Subject: [PATCH 32/49] Create PULL_REQUEST_TEMPLATE --- PULL_REQUEST_TEMPLATE | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 PULL_REQUEST_TEMPLATE diff --git a/PULL_REQUEST_TEMPLATE b/PULL_REQUEST_TEMPLATE new file mode 100644 index 0000000000..b56e60264e --- /dev/null +++ b/PULL_REQUEST_TEMPLATE @@ -0,0 +1,16 @@ +--- edit or delete this section --- +## Screenshots + + + + + + + +
BeforeAfter
+ +## Checklist + +- [ ] Follow-up e2e test ticket created or not needed +- [ ] A11y checked +- [ ] Approve from product or not needed From bd3d8cacb90e53011f637463ee5a4da65fa85833 Mon Sep 17 00:00:00 2001 From: Kristof Deak <92309696+kdeakinstructure@users.noreply.github.com> Date: Wed, 7 Sep 2022 10:55:09 +0200 Subject: [PATCH 33/49] [MBL-16227][Parent] - Rename build apk names to contains the appname (#1698) add variant section define an apk build name which contains the word 'parent' instead of using a default build name (app-debug.apk or app-release.apk) add timestamp to debug builds to be more accurate refs: MBL-16227 affects: Parent release note: test plan: --- apps/flutter_parent/android/app/build.gradle | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/apps/flutter_parent/android/app/build.gradle b/apps/flutter_parent/android/app/build.gradle index ea3bc8685a..532b872ab9 100644 --- a/apps/flutter_parent/android/app/build.gradle +++ b/apps/flutter_parent/android/app/build.gradle @@ -78,6 +78,19 @@ android { shrinkResources false // Must be false, otherwise resources we need are erroneously stripped out proguardFiles 'proguard-rules.pro' } + applicationVariants.all{ + variant -> + variant.outputs.each{ + output-> + project.ext { appName = 'parent' } + def dateTimeStamp = new Date().format('yyyy-MM-dd-HH-mm-ss') + def newName = output.outputFile.name + newName = newName.replace("app-", "$project.ext.appName-") + newName = newName.replace("-debug", "-dev-debug-" + dateTimeStamp) + newName = newName.replace("-release", "-prod-release") + output.outputFileName = newName + } + } } } From ac66e9e13d47f6adf7667ee0f3c653ea500c87ce Mon Sep 17 00:00:00 2001 From: Kristof Deak <92309696+kdeakinstructure@users.noreply.github.com> Date: Wed, 7 Sep 2022 10:59:37 +0200 Subject: [PATCH 34/49] [MBL-16225][Student][Teacher] - Introduce StubMultiAPILevel annotation (#1695) Create StubMultiAPILevel annotation. Put annotation on some Multi API Level flaky tests. Modify flank files to exclude the tests with the annotation. refs: MBL-16225 affects: Student, Teacher release note: none --- apps/student/flank_multi_api_level.yml | 2 +- apps/teacher/flank_multi_api_level.yml | 2 +- .../teacher/ui/SpeedGraderGradePageTest.kt | 3 +++ .../canvas/espresso/StubMultiAPILevel.kt | 22 +++++++++++++++++++ 4 files changed, 27 insertions(+), 2 deletions(-) create mode 100644 automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/StubMultiAPILevel.kt diff --git a/apps/student/flank_multi_api_level.yml b/apps/student/flank_multi_api_level.yml index d56fee62c7..dd25260b89 100644 --- a/apps/student/flank_multi_api_level.yml +++ b/apps/student/flank_multi_api_level.yml @@ -12,7 +12,7 @@ gcloud: record-video: true timeout: 60m test-targets: - - notAnnotation com.instructure.canvas.espresso.E2E, com.instructure.canvas.espresso.Stub, com.instructure.canvas.espresso.FlakyE2E, com.instructure.canvas.espresso.KnownBug + - notAnnotation com.instructure.canvas.espresso.E2E, com.instructure.canvas.espresso.Stub, com.instructure.canvas.espresso.StubMultiAPILevel, com.instructure.canvas.espresso.FlakyE2E, com.instructure.canvas.espresso.KnownBug device: - model: NexusLowRes version: 27 diff --git a/apps/teacher/flank_multi_api_level.yml b/apps/teacher/flank_multi_api_level.yml index c90ecd1a9a..2274d848d1 100644 --- a/apps/teacher/flank_multi_api_level.yml +++ b/apps/teacher/flank_multi_api_level.yml @@ -12,7 +12,7 @@ gcloud: record-video: true timeout: 60m test-targets: - - notAnnotation com.instructure.canvas.espresso.E2E, com.instructure.canvas.espresso.Stub, com.instructure.canvas.espresso.FlakyE2E, com.instructure.canvas.espresso.KnownBug + - notAnnotation com.instructure.canvas.espresso.E2E, com.instructure.canvas.espresso.Stub, com.instructure.canvas.espresso.StubMultiAPILevel, com.instructure.canvas.espresso.FlakyE2E, com.instructure.canvas.espresso.KnownBug device: - model: NexusLowRes version: 27 diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/SpeedGraderGradePageTest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/SpeedGraderGradePageTest.kt index 3b9cd7322b..c96af11ac7 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/SpeedGraderGradePageTest.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/SpeedGraderGradePageTest.kt @@ -16,6 +16,7 @@ package com.instructure.teacher.ui import android.util.Log +import com.instructure.canvas.espresso.StubMultiAPILevel import com.instructure.canvas.espresso.mockCanvas.* import com.instructure.canvasapi2.models.Assignment import com.instructure.canvasapi2.models.CanvasContextPermission @@ -137,6 +138,7 @@ class SpeedGraderGradePageTest : TeacherTest() { } @Test + @StubMultiAPILevel("Failed API levels = { 27, 28, 29 }") fun overgradePointAssignment() { val pointsPossible = 20 goToSpeedGraderGradePage(pointsPossible = pointsPossible) @@ -159,6 +161,7 @@ class SpeedGraderGradePageTest : TeacherTest() { } @Test + @StubMultiAPILevel("Failed API levels = { 27, 28, 29 }") fun clearGrade() { goToSpeedGraderGradePage() speedGraderPage.swipeUpGradesTab() diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/StubMultiAPILevel.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/StubMultiAPILevel.kt new file mode 100644 index 0000000000..90080e9df1 --- /dev/null +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/StubMultiAPILevel.kt @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2022 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.instructure.canvas.espresso + +// Apply on a test method which is failing on Firebase Test Lab (FTL) on some API levels. Write the failing API Levels into the parameter. +@Target(AnnotationTarget.FUNCTION) +@Retention(AnnotationRetention.RUNTIME) +annotation class StubMultiAPILevel(val failedApiLevels: String = "") \ No newline at end of file From 010636a8c0acd790b107f0e8adebce84fa6db7cd Mon Sep 17 00:00:00 2001 From: Kristof Deak <92309696+kdeakinstructure@users.noreply.github.com> Date: Wed, 7 Sep 2022 14:21:47 +0200 Subject: [PATCH 35/49] [MBL-13517][Student] - Investigate and put in order lti related tests (#1696) Implement testModules_launchesIntoExternalTool interaction test. Extend MockCanvas class addItemToModule method to handle LTITool type as well. Add URL assertion to testModules_launchesIntoExternalURL method. refs: MBL-13517 affects: Student release note: none --- .../ui/interaction/ModuleInteractionTest.kt | 45 +++++++------------ .../NavigationDrawerInteractionTest.kt | 4 +- .../student/ui/pages/CanvasWebViewPage.kt | 11 ++++- .../canvas/espresso/mockCanvas/MockCanvas.kt | 5 +++ 4 files changed, 33 insertions(+), 32 deletions(-) diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ModuleInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ModuleInteractionTest.kt index b1a6092ddb..6e84a2e81e 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ModuleInteractionTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ModuleInteractionTest.kt @@ -18,34 +18,12 @@ package com.instructure.student.ui.interaction import android.text.Html import androidx.test.espresso.Espresso import androidx.test.espresso.web.webdriver.Locator -import com.instructure.canvas.espresso.Stub -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.addAssignment -import com.instructure.canvas.espresso.mockCanvas.addDiscussionTopicToCourse -import com.instructure.canvas.espresso.mockCanvas.addFileToCourse -import com.instructure.canvas.espresso.mockCanvas.addItemToModule -import com.instructure.canvas.espresso.mockCanvas.addModuleToCourse -import com.instructure.canvas.espresso.mockCanvas.addPageToCourse -import com.instructure.canvas.espresso.mockCanvas.addQuestionToQuiz -import com.instructure.canvas.espresso.mockCanvas.addQuizToCourse -import com.instructure.canvas.espresso.mockCanvas.init -import com.instructure.canvasapi2.models.Assignment -import com.instructure.canvasapi2.models.DiscussionTopicHeader -import com.instructure.canvasapi2.models.LockInfo -import com.instructure.canvasapi2.models.LockedModule -import com.instructure.canvasapi2.models.ModuleObject -import com.instructure.canvasapi2.models.Page -import com.instructure.canvasapi2.models.Quiz -import com.instructure.canvasapi2.models.QuizAnswer -import com.instructure.canvasapi2.models.Tab +import com.instructure.canvas.espresso.mockCanvas.* +import com.instructure.canvasapi2.models.* import com.instructure.dataseeding.util.days import com.instructure.dataseeding.util.fromNow import com.instructure.dataseeding.util.iso8601 -import com.instructure.panda_annotations.FeatureCategory -import com.instructure.panda_annotations.Priority -import com.instructure.panda_annotations.SecondaryFeatureCategory -import com.instructure.panda_annotations.TestCategory -import com.instructure.panda_annotations.TestMetaData +import com.instructure.panda_annotations.* import com.instructure.student.R import com.instructure.student.ui.pages.WebViewTextCheck import com.instructure.student.ui.utils.StudentTest @@ -101,12 +79,16 @@ class ModuleInteractionTest : StudentTest() { discussionDetailsPage.assertTopicInfoShowing(topicHeader!!) } - // I'm punting on LTI testing for now. But MBL-13517 captures this work. - @Stub @Test - @TestMetaData(Priority.MANDATORY, FeatureCategory.MODULES, TestCategory.INTERACTION, true) + @TestMetaData(Priority.MANDATORY, FeatureCategory.MODULES, TestCategory.INTERACTION) fun testModules_launchesIntoExternalTool() { // Tapping an ExternalTool module item should navigate to that item's detail page + val data = getToCourseModules(studentCount = 1, courseCount = 1) + val course1 = data.courses.values.first() + val module = data.courseModules[course1.id]!!.first() + + modulesPage.clickModuleItem(module, "Google Drive") + canvasWebViewPage.assertTitle("Google Drive") } // Tapping an ExternalURL module item should navigate to that item's detail page @@ -122,6 +104,7 @@ class ModuleInteractionTest : StudentTest() { modulesPage.clickModuleItem(module,externalUrl) // Not much we can test here, as it is an external URL, but testModules_navigateToNextAndPreviousModuleItems // will test that the module name and module item name are displayed correctly. + canvasWebViewPage.checkWebViewURL("https://www.google.com") } // Tapping a File module item should navigate to that item's detail page @@ -487,6 +470,12 @@ class ModuleInteractionTest : StudentTest() { item = quiz!! ) + val ltiTool = data.addLTITool("Google Drive", "http://google.com", course1, 1234L) + data.addItemToModule( + course = course1, + moduleId = module.id, + item = ltiTool!! + ) // Sign in val student = data.students[0] diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/NavigationDrawerInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/NavigationDrawerInteractionTest.kt index 020c69234d..0ce11e4de2 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/NavigationDrawerInteractionTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/NavigationDrawerInteractionTest.kt @@ -34,9 +34,9 @@ import com.instructure.panda_annotations.FeatureCategory import com.instructure.panda_annotations.Priority import com.instructure.panda_annotations.TestCategory import com.instructure.panda_annotations.TestMetaData +import com.instructure.student.R import com.instructure.student.ui.utils.StudentTest import com.instructure.student.ui.utils.tokenLogin -import com.instructure.student.R import dagger.hilt.android.testing.HiltAndroidTest import org.hamcrest.CoreMatchers import org.junit.Before @@ -164,7 +164,7 @@ class NavigationDrawerInteractionTest : StudentTest() { dashboardPage.goToHelp() helpPage.launchGuides() - canvasWebViewPage.verifyTitle(R.string.searchGuides) + canvasWebViewPage.assertTitle(R.string.searchGuides) } // Should send an error report diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/CanvasWebViewPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/CanvasWebViewPage.kt index d30fa91f02..64aa9bc941 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/CanvasWebViewPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/CanvasWebViewPage.kt @@ -21,7 +21,10 @@ import androidx.test.espresso.matcher.ViewMatchers.isDisplayed import androidx.test.espresso.web.assertion.WebViewAssertions.webMatches import androidx.test.espresso.web.model.Atoms.getCurrentUrl import androidx.test.espresso.web.sugar.Web.onWebView -import androidx.test.espresso.web.webdriver.DriverAtoms.* +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.DriverAtoms.webScrollIntoView import androidx.test.espresso.web.webdriver.Locator import com.instructure.canvas.espresso.withElementRepeat import com.instructure.espresso.assertVisible @@ -35,7 +38,11 @@ import org.hamcrest.Matchers.containsString */ open class CanvasWebViewPage : BasePage(R.id.canvasWebView) { - fun verifyTitle(@StringRes title: Int) { + fun assertTitle(@StringRes title: Int) { + onView(withAncestor(R.id.toolbar) + withText(title)).assertVisible() + } + + fun assertTitle(title: String) { onView(withAncestor(R.id.toolbar) + withText(title)).assertVisible() } 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 0a6671f9fc..ba6744f500 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 @@ -1510,6 +1510,11 @@ fun MockCanvas.addItemToModule( itemTitle = item itemUrl = item } + is LTITool -> { + itemType = ModuleItem.Type.ExternalTool + itemTitle = item.name + itemUrl = item.url + } else -> { throw Exception("Unknown item type: ${item::class.java.simpleName}") } From f810d0f0827bc537fd7116e68c42c8ac646a4952 Mon Sep 17 00:00:00 2001 From: Kristof Deak <92309696+kdeakinstructure@users.noreply.github.com> Date: Fri, 9 Sep 2022 10:49:53 +0200 Subject: [PATCH 36/49] [MBL-16210][Teacher] - Insert discussion creation on UI #1700 implement discussion creation flow on UI. add searching to the discussionE2E test add expand/collapse to the test as well. refs: MBL-16210 affects: Teacher release note: none --- .../teacher/ui/e2e/DiscussionsE2ETest.kt | 59 +++++++++++++++---- .../teacher/ui/pages/DiscussionsListPage.kt | 26 ++++++-- .../ui/pages/EditDiscussionsDetailsPage.kt | 25 +++++--- 3 files changed, 88 insertions(+), 22 deletions(-) diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/DiscussionsE2ETest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/DiscussionsE2ETest.kt index 49a500d636..788a7a6a80 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/DiscussionsE2ETest.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/DiscussionsE2ETest.kt @@ -17,6 +17,7 @@ package com.instructure.teacher.ui.e2e import android.util.Log +import androidx.test.espresso.Espresso import com.instructure.canvas.espresso.E2E import com.instructure.panda_annotations.FeatureCategory import com.instructure.panda_annotations.Priority @@ -32,9 +33,7 @@ import org.junit.Test class DiscussionsE2ETest : TeacherTest() { override fun displaysPageObjects() = Unit - override fun enableAndConfigureAccessibilityChecks() { - //We dont want to see accessibility errors on E2E tests - } + override fun enableAndConfigureAccessibilityChecks() = Unit @E2E @Test @@ -59,35 +58,73 @@ class DiscussionsE2ETest : TeacherTest() { courseBrowserPage.openDiscussionsTab() discussionsListPage.assertHasDiscussion(discussion) - Log.d(STEP_TAG,"Click on ${discussion.title} discussion and navigate to Discussions Details Page by clicking on 'Edit'.") + Log.d(STEP_TAG,"Click on '${discussion.title}' discussion and navigate to Discussions Details Page by clicking on 'Edit'.") discussionsListPage.clickDiscussion(discussion) discussionsDetailsPage.openEdit() val newTitle = "New Discussion" - Log.d(STEP_TAG,"Edit the discussions's title to: $newTitle. Click on 'Save'.") + Log.d(STEP_TAG,"Edit the discussion's title to: '$newTitle'. Click on 'Save'.") editDiscussionsDetailsPage.editTitle(newTitle) editDiscussionsDetailsPage.clickSave() - Log.d(STEP_TAG,"Refresh the page. Assert that the discussion's name has been changed to $newTitle and it is published.") + Log.d(STEP_TAG,"Refresh the page. Assert that the discussion's name has been changed to '$newTitle' and it is published.") discussionsDetailsPage.refresh() discussionsDetailsPage.assertDiscussionTitle(newTitle) discussionsDetailsPage.assertDiscussionPublished() - Log.d(STEP_TAG,"Navigate to Discussions Details Page by clicking on 'Edit'. Unpublish the $newTitle discussion and click on 'Save'.") + Log.d(STEP_TAG,"Navigate to Discussions Details Page by clicking on 'Edit'. Unpublish the '$newTitle' discussion and click on 'Save'.") discussionsDetailsPage.openEdit() - editDiscussionsDetailsPage.switchPublished() + editDiscussionsDetailsPage.togglePublished() editDiscussionsDetailsPage.clickSave() - Log.d(STEP_TAG,"Refresh the page. Assert that the $newTitle discussion has been unpublished.") + Log.d(STEP_TAG,"Refresh the page. Assert that the '$newTitle' discussion has been unpublished.") discussionsDetailsPage.refresh() discussionsDetailsPage.assertDiscussionUnpublished() - Log.d(STEP_TAG,"Navigate to Discussions Details Page by clicking on 'Edit'. Delete the $newTitle discussion.") + Log.d(STEP_TAG,"Navigate to Discussions Details Page by clicking on 'Edit'. Delete the '$newTitle' discussion.") discussionsDetailsPage.openEdit() editDiscussionsDetailsPage.deleteDiscussion() - Log.d(STEP_TAG,"Refresh the page. Assert that there is no discussion, so the $newTitle discussion has been deleted successfully.") + Log.d(STEP_TAG,"Refresh the page. Assert that there is no discussion, so the '$newTitle' discussion has been deleted successfully.") discussionsListPage.refresh() discussionsListPage.assertNoDiscussion() + + Log.d(STEP_TAG,"Click on '+' icon on the UI to create a new discussion.") + discussionsListPage.createNewDiscussion() + + val newDiscussionTitle = "Test Discussion Mobile UI" + Log.d(STEP_TAG,"Set '$newDiscussionTitle' as the discussion's title and set some description as well.") + editDiscussionsDetailsPage.editTitle(newDiscussionTitle) + editDiscussionsDetailsPage.editDescription("Mobile UI Discussion description") + + Log.d(STEP_TAG,"Toggle Publish checkbox and save the page.") + editDiscussionsDetailsPage.togglePublished() + editDiscussionsDetailsPage.clickSendNewDiscussion() + + Log.d(STEP_TAG,"Assert that '$newDiscussionTitle' discussion is displayed and published.") + discussionsListPage.assertHasDiscussion(newDiscussionTitle) + discussionsListPage.clickDiscussion(newDiscussionTitle) + discussionsDetailsPage.assertDiscussionPublished() + Espresso.pressBack() + + Log.d(STEP_TAG,"Click on the Search icon and type some search query string which matches only with the previously created discussion's title.") + discussionsListPage.openSearch() + discussionsListPage.enterSearchQuery("Test Discussion") + + Log.d(STEP_TAG,"Assert that the '$newDiscussionTitle' discussion is displayed and it is the only one.") + discussionsListPage.assertDiscussionCount(2) // header + single search result + discussionsListPage.assertHasDiscussion(newDiscussionTitle) + Espresso.pressBack() // need to press back to exit from the search input field + + Log.d(STEP_TAG,"Collapse the discussion list and assert that the '$newDiscussionTitle' discussion can NOT be seen.") + discussionsListPage.toggleCollapseExpandIcon() + discussionsListPage.assertDiscussionCount(1) // header only + discussionsListPage.assertDiscussionDoesNotExist(newDiscussionTitle) + + Log.d(STEP_TAG,"Expand the discussion list and assert that the '$newDiscussionTitle' discussion can be seen.") + discussionsListPage.toggleCollapseExpandIcon() + discussionsListPage.assertDiscussionCount(2) // header only + single search result + discussionsListPage.assertHasDiscussion(newDiscussionTitle) + } } \ No newline at end of file diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/DiscussionsListPage.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/DiscussionsListPage.kt index 2270b02a17..d3c6ba4499 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/DiscussionsListPage.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/DiscussionsListPage.kt @@ -17,13 +17,11 @@ package com.instructure.teacher.ui.pages import androidx.test.espresso.action.ViewActions +import androidx.test.espresso.assertion.ViewAssertions.doesNotExist import com.instructure.canvasapi2.models.DiscussionTopicHeader import com.instructure.dataseeding.model.DiscussionApiModel import com.instructure.espresso.* -import com.instructure.espresso.page.BasePage -import com.instructure.espresso.page.onView -import com.instructure.espresso.page.waitForViewWithText -import com.instructure.espresso.page.withId +import com.instructure.espresso.page.* import com.instructure.teacher.R class DiscussionsListPage : BasePage() { @@ -38,10 +36,22 @@ class DiscussionsListPage : BasePage() { waitForViewWithText(discussion.title).click() } + fun clickDiscussion(discussionTitle: String) { + waitForViewWithText(discussionTitle).click() + } + fun assertHasDiscussion(discussion: DiscussionApiModel) { waitForViewWithText(discussion.title).assertDisplayed() } + fun assertHasDiscussion(discussionTitle: String) { + waitForViewWithText(discussionTitle).assertDisplayed() + } + + fun assertDiscussionDoesNotExist(discussionTitle: String) { + onView(withText(discussionTitle)).check(doesNotExist()) + } + fun assertNoDiscussion() { onView(withId(R.id.emptyPandaView)).assertDisplayed() } @@ -65,4 +75,12 @@ class DiscussionsListPage : BasePage() { fun refresh() { onView(withId(R.id.swipeRefreshLayout)).swipeDown() } + + fun createNewDiscussion() { + onView(withId(R.id.createNewDiscussion)).click() + } + + fun toggleCollapseExpandIcon() { + onView(withId(R.id.collapseIcon)).click() + } } diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/EditDiscussionsDetailsPage.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/EditDiscussionsDetailsPage.kt index cdbe9c1b73..d4d6000b08 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/EditDiscussionsDetailsPage.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/EditDiscussionsDetailsPage.kt @@ -17,6 +17,7 @@ package com.instructure.teacher.ui.pages import androidx.test.espresso.Espresso +import com.instructure.espresso.WaitForViewWithId import com.instructure.espresso.click import com.instructure.espresso.page.BasePage import com.instructure.espresso.page.onView @@ -24,26 +25,36 @@ import com.instructure.espresso.page.withId import com.instructure.espresso.replaceText import com.instructure.espresso.scrollTo import com.instructure.teacher.R +import com.instructure.teacher.ui.utils.TypeInRCETextEditor class EditDiscussionsDetailsPage : BasePage() { + private val contentRceView by WaitForViewWithId(R.id.rce_webView) + fun editTitle(newTitle: String) { onView(withId(R.id.editDiscussionName)).replaceText(newTitle) Espresso.closeSoftKeyboard() } - fun switchPublished() { - onView(withId(R.id.publishSwitch)).scrollTo() - onView(withId(R.id.publishSwitch)).click() + fun togglePublished() { + onView(withId(R.id.publishSwitch)).scrollTo().click() } fun deleteDiscussion() { - onView(withId(R.id.deleteText)).scrollTo() - onView(withId(R.id.deleteText)).click() - onView(withId(android.R.id.button1)).click() + onView(withId(R.id.deleteText)).scrollTo().click() + onView(withId(android.R.id.button1)).click() //button1 is actually the 'DELETE' button on the UI pop-up dialog. } - fun clickSave() { + fun clickSave() { //This method is used when editing an existing discussion. onView(withId(R.id.menuSave)).click() } + + fun clickSendNewDiscussion() { //This method is used when creating a new discussion via mobile UI. + onView(withId(R.id.menuSaveDiscussion)).click() + } + + fun editDescription(newDescription: String) { + contentRceView.perform(TypeInRCETextEditor(newDescription)) + } + } \ No newline at end of file From 0a17cd0489f719a8e21f7ce715d0b96740b94f8f Mon Sep 17 00:00:00 2001 From: Akos Hermann <72087159+hermannakos@users.noreply.github.com> Date: Tue, 13 Sep 2022 16:16:37 +0200 Subject: [PATCH 37/49] [MBL-16236][Student] Multiple file sharing support (#1701) refs: MBL-16236 affects: Student release note: Added support for sharing multiple files to the app. test plan: Select multiple files in the device file explorer. Canvas should be visible in the share sheet. All the files should be visible in the file upload dialog. --- apps/student/src/main/AndroidManifest.xml | 1 + .../file/upload/FileUploadDialogFragment.kt | 46 +++++++++++-------- .../file/upload/FileUploadDialogViewModel.kt | 8 ++-- .../shareextension/ShareExtensionActivity.kt | 4 +- .../shareextension/ShareExtensionViewModel.kt | 25 ++++++---- .../com/instructure/pandautils/utils/Const.kt | 1 + .../file/upload/FileUploadViewModelTest.kt | 12 ++--- 7 files changed, 58 insertions(+), 39 deletions(-) diff --git a/apps/student/src/main/AndroidManifest.xml b/apps/student/src/main/AndroidManifest.xml index 84a04a94c4..eaf3861f3a 100644 --- a/apps/student/src/main/AndroidManifest.xml +++ b/apps/student/src/main/AndroidManifest.xml @@ -214,6 +214,7 @@ android:exported="true"> + diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/file/upload/FileUploadDialogFragment.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/file/upload/FileUploadDialogFragment.kt index 2ba7ab1c1e..c554d0fe08 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/file/upload/FileUploadDialogFragment.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/file/upload/FileUploadDialogFragment.kt @@ -34,7 +34,7 @@ import androidx.fragment.app.DialogFragment import androidx.fragment.app.viewModels import androidx.lifecycle.LiveData import androidx.lifecycle.Observer -import androidx.work.* +import androidx.work.WorkInfo import com.instructure.canvasapi2.models.* import com.instructure.canvasapi2.models.postmodels.FileSubmitObject import com.instructure.canvasapi2.utils.ApiPrefs @@ -43,7 +43,6 @@ import com.instructure.pandautils.databinding.FragmentFileUploadDialogBinding import com.instructure.pandautils.utils.* import dagger.hilt.android.AndroidEntryPoint import java.io.File -import kotlin.collections.ArrayList @AndroidEntryPoint class FileUploadDialogFragment : DialogFragment() { @@ -56,7 +55,7 @@ class FileUploadDialogFragment : DialogFragment() { private var canvasContext: CanvasContext by ParcelableArg(ApiPrefs.user) private var position: Int by IntArg() - private var fileSubmitUri: Uri? = null + private var fileSubmitUris: ArrayList? = arrayListOf() private var cameraImageUri: Uri? = null private var assignment: Assignment? by NullableParcelableArg() @@ -173,7 +172,7 @@ class FileUploadDialogFragment : DialogFragment() { } }) - viewModel.setData(assignment, fileSubmitUri, uploadType, canvasContext, parentFolderId, quizQuestionId, + viewModel.setData(assignment, fileSubmitUris, uploadType, canvasContext, parentFolderId, quizQuestionId, position, quizId, dialogCallback, attachmentCallback, workerCallback) } @@ -236,7 +235,7 @@ class FileUploadDialogFragment : DialogFragment() { return FileUploadDialogFragment().apply { arguments = args - fileSubmitUri = args.getParcelable(Const.URI) + fileSubmitUris = args.getParcelableArrayList(Const.URIS) uploadType = args.getSerializable(Const.UPLOAD_TYPE) as FileUploadType parentFolderId = args.getLong(Const.PARENT_FOLDER_ID, INVALID_ID) quizQuestionId = args.getLong(Const.QUIZ_ANSWER_ID, INVALID_ID) @@ -249,28 +248,28 @@ class FileUploadDialogFragment : DialogFragment() { } } - fun createBundle(submitURI: Uri?, type: FileUploadType, parentFolderId: Long?): Bundle { + fun createBundle(submitURIs: ArrayList, type: FileUploadType, parentFolderId: Long?): Bundle { val bundle = Bundle() - if (submitURI != null) bundle.putParcelable(Const.URI, submitURI) + if (submitURIs.isNotEmpty()) bundle.putParcelableArrayList(Const.URIS, submitURIs) if (parentFolderId != null) bundle.putLong(Const.PARENT_FOLDER_ID, parentFolderId) bundle.putSerializable(Const.UPLOAD_TYPE, type) return bundle } fun createMessageAttachmentsBundle(defaultFileList: ArrayList): Bundle { - val bundle = createBundle(null, FileUploadType.MESSAGE, null) + val bundle = createBundle(arrayListOf(), FileUploadType.MESSAGE, null) bundle.putParcelableArrayList(Const.FILES, defaultFileList) return bundle } fun createDiscussionsBundle(defaultFileList: ArrayList): Bundle { - val bundle = createBundle(null, FileUploadType.DISCUSSION, null) + val bundle = createBundle(arrayListOf(), FileUploadType.DISCUSSION, null) bundle.putParcelableArrayList(Const.FILES, defaultFileList) return bundle } - fun createFilesBundle(submitURI: Uri?, parentFolderId: Long?): Bundle { - return createBundle(submitURI, FileUploadType.USER, parentFolderId) + fun createFilesBundle(submitUris: ArrayList, parentFolderId: Long?): Bundle { + return createBundle(submitUris, FileUploadType.USER, parentFolderId) } fun createContextBundle(submitURI: Uri?, context: CanvasContext, parentFolderId: Long?): Bundle { @@ -282,32 +281,41 @@ class FileUploadDialogFragment : DialogFragment() { } private fun createCourseBundle(submitURI: Uri?, course: Course, parentFolderId: Long?): Bundle { - val bundle = createBundle(submitURI, FileUploadType.COURSE, parentFolderId) + val submitUris = submitURI?.let { + arrayListOf(it) + } ?: arrayListOf() + val bundle = createBundle(submitUris, FileUploadType.COURSE, parentFolderId) bundle.putParcelable(Const.CANVAS_CONTEXT, course) return bundle } private fun createGroupBundle(submitURI: Uri?, group: Group, parentFolderId: Long?): Bundle { - val bundle = createBundle(submitURI, FileUploadType.GROUP, parentFolderId) + val submitUris = submitURI?.let { + arrayListOf(it) + } ?: arrayListOf() + val bundle = createBundle(submitUris, FileUploadType.GROUP, parentFolderId) bundle.putParcelable(Const.CANVAS_CONTEXT, group) return bundle } private fun createUserBundle(submitURI: Uri?, user: User, parentFolderId: Long?): Bundle { - val bundle = createBundle(submitURI, FileUploadType.USER, parentFolderId) + val submitUris = submitURI?.let { + arrayListOf(it) + } ?: arrayListOf() + val bundle = createBundle(submitUris, FileUploadType.USER, parentFolderId) bundle.putParcelable(Const.CANVAS_CONTEXT, user) return bundle } - fun createAssignmentBundle(submitURI: Uri?, course: Course, assignment: Assignment): Bundle { - val bundle = createBundle(submitURI, FileUploadType.ASSIGNMENT, null) + fun createAssignmentBundle(submitURIs: ArrayList, course: Course, assignment: Assignment): Bundle { + val bundle = createBundle(submitURIs, FileUploadType.ASSIGNMENT, null) bundle.putParcelable(Const.CANVAS_CONTEXT, course) bundle.putParcelable(Const.ASSIGNMENT, assignment) return bundle } fun createQuizFileBundle(quizQuestionId: Long, courseId: Long, quizId: Long, position: Int): Bundle { - val bundle = createBundle(null, FileUploadType.QUIZ, null) + val bundle = createBundle(arrayListOf(), FileUploadType.QUIZ, null) bundle.putLong(Const.QUIZ_ANSWER_ID, quizQuestionId) bundle.putLong(Const.QUIZ, quizId) bundle.putLong(Const.COURSE_ID, courseId) @@ -316,7 +324,7 @@ class FileUploadDialogFragment : DialogFragment() { } fun createSubmissionCommentBundle(course: Course, assignment: Assignment, defaultFileList: java.util.ArrayList): Bundle { - val bundle = createBundle(null, FileUploadType.SUBMISSION_COMMENT, null) + val bundle = createBundle(arrayListOf(), FileUploadType.SUBMISSION_COMMENT, null) bundle.putParcelable(Const.CANVAS_CONTEXT, course) bundle.putParcelable(Const.ASSIGNMENT, assignment) bundle.putParcelableArrayList(Const.FILES, defaultFileList) @@ -324,7 +332,7 @@ class FileUploadDialogFragment : DialogFragment() { } fun createAttachmentsBundle(defaultFileList: ArrayList = ArrayList()): Bundle { - val bundle = createBundle(null, FileUploadType.MESSAGE, null) + val bundle = createBundle(arrayListOf(), FileUploadType.MESSAGE, null) bundle.putParcelableArrayList(Const.FILES, defaultFileList) return bundle } diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/file/upload/FileUploadDialogViewModel.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/file/upload/FileUploadDialogViewModel.kt index 5e5834aa81..4eb64448e7 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/file/upload/FileUploadDialogViewModel.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/file/upload/FileUploadDialogViewModel.kt @@ -72,7 +72,7 @@ class FileUploadDialogViewModel @Inject constructor( fun setData( assignment: Assignment?, - file: Uri?, + files: ArrayList?, uploadType: FileUploadType, canvasContext: CanvasContext, parentFolderId: Long, @@ -84,10 +84,10 @@ class FileUploadDialogViewModel @Inject constructor( workerCallback: ((LiveData) -> Unit)? = null ) { this.assignment = assignment - file?.let { uri -> + files?.forEach { uri -> val submitObject = getUriContents(uri) - submitObject?.let { - this.filesToUpload = mutableListOf(FileUploadData(uri, it)) + submitObject?.let { fso -> + this.filesToUpload.add(FileUploadData(uri, fso)) } } this.uploadType = uploadType diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/shareextension/ShareExtensionActivity.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/shareextension/ShareExtensionActivity.kt index d80f27017d..ad0572c49c 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/shareextension/ShareExtensionActivity.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/shareextension/ShareExtensionActivity.kt @@ -92,11 +92,11 @@ abstract class ShareExtensionActivity : AppCompatActivity() { private fun handleAction(action: ShareExtensionAction) { when (action) { is ShareExtensionAction.ShowAssignmentUploadDialog -> { - val bundle = FileUploadDialogFragment.createAssignmentBundle(action.fileUri, action.course as Course, action.assignment) + val bundle = FileUploadDialogFragment.createAssignmentBundle(action.fileUris, action.course as Course, action.assignment) showUploadDialog(bundle, action.dialogCallback) } is ShareExtensionAction.ShowMyFilesUploadDialog -> { - val bundle = FileUploadDialogFragment.createFilesBundle(action.fileUri, null) + val bundle = FileUploadDialogFragment.createFilesBundle(action.fileUris, null) showUploadDialog(bundle, action.dialogCallback) } is ShareExtensionAction.ShowToast -> { diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/shareextension/ShareExtensionViewModel.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/shareextension/ShareExtensionViewModel.kt index fd4bd7c65f..016a526f4e 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/shareextension/ShareExtensionViewModel.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/shareextension/ShareExtensionViewModel.kt @@ -38,7 +38,7 @@ class ShareExtensionViewModel @Inject constructor( private val resources: Resources ) : ViewModel() { - var uri: Uri? = null + var uris: ArrayList? = null var uploadType = FileUploadType.USER val events: LiveData> @@ -53,17 +53,26 @@ class ShareExtensionViewModel @Inject constructor( val action = intent.action val type = intent.type - uri = if (Intent.ACTION_SEND == action && type != null) { - intent.getParcelableExtra(Intent.EXTRA_STREAM) - } else { + if (type == null) { _events.postValue(Event(ShareExtensionAction.ShowToast(resources.getString(R.string.uploadingFromSourceFailed)))) - null + return + } + when (action) { + Intent.ACTION_SEND -> { + val uri = intent.getParcelableExtra(Intent.EXTRA_STREAM) + uri?.let { + uris = arrayListOf(it) + } ?: _events.postValue(Event(ShareExtensionAction.ShowToast(resources.getString(R.string.errorOccurred)))) + } + Intent.ACTION_SEND_MULTIPLE -> { + uris = intent.getParcelableArrayListExtra(Intent.EXTRA_STREAM) + } } } fun showUploadDialog(course: CanvasContext?, assignment: Assignment?, uploadType: FileUploadType) { this.uploadType = uploadType - uri?.let { + uris?.let { when (uploadType) { FileUploadType.USER -> _events.postValue(Event(ShareExtensionAction.ShowMyFilesUploadDialog(it, this::uploadDialogCallback))) FileUploadType.ASSIGNMENT -> { @@ -109,8 +118,8 @@ class ShareExtensionViewModel @Inject constructor( } sealed class ShareExtensionAction { - data class ShowAssignmentUploadDialog(val course: CanvasContext, val assignment: Assignment, val fileUri: Uri, val uploadType: FileUploadType, val dialogCallback: (Int) -> Unit) : ShareExtensionAction() - data class ShowMyFilesUploadDialog(val fileUri: Uri, val dialogCallback: (Int) -> Unit) : ShareExtensionAction() + data class ShowAssignmentUploadDialog(val course: CanvasContext, val assignment: Assignment, val fileUris: ArrayList, val uploadType: FileUploadType, val dialogCallback: (Int) -> Unit) : ShareExtensionAction() + data class ShowMyFilesUploadDialog(val fileUris: ArrayList, val dialogCallback: (Int) -> Unit) : ShareExtensionAction() object ShowProgressDialog : ShareExtensionAction() object ShowSuccessDialog : ShareExtensionAction() object Finish : ShareExtensionAction() diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/utils/Const.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/utils/Const.kt index 8d70bd66cd..2898509004 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/utils/Const.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/utils/Const.kt @@ -87,6 +87,7 @@ object Const { const val UNREAD = "unread" const val UPLOAD_TYPE = "uploadType" const val URI = "uri" + const val URIS = "uris" const val URL = "url" const val USER = "user" const val USER_ID = "userId" diff --git a/libs/pandautils/src/test/java/com/instructure/pandautils/features/file/upload/FileUploadViewModelTest.kt b/libs/pandautils/src/test/java/com/instructure/pandautils/features/file/upload/FileUploadViewModelTest.kt index 92e89e9c96..a118f81219 100644 --- a/libs/pandautils/src/test/java/com/instructure/pandautils/features/file/upload/FileUploadViewModelTest.kt +++ b/libs/pandautils/src/test/java/com/instructure/pandautils/features/file/upload/FileUploadViewModelTest.kt @@ -101,7 +101,7 @@ class FileUploadViewModelTest { val viewModel = createViewModel() val course = createCourse(1L, "Course 1") val assignment = createAssignment(1L, "Assignment 1", 1L, listOf("pdf", "mp4", "docx")) - viewModel.setData(assignment, null, FileUploadType.ASSIGNMENT, course, -1L, -1L, -1, -1L) + viewModel.setData(assignment, arrayListOf(), FileUploadType.ASSIGNMENT, course, -1L, -1L, -1, -1L) viewModel.data.observe(lifecycleOwner) {} @@ -114,7 +114,7 @@ class FileUploadViewModelTest { val viewModel = createViewModel() val course = createCourse(1L, "Course 1") val assignment = createAssignment(1L, "Assignment 1", 1L, listOf("pdf")) - viewModel.setData(assignment, null, FileUploadType.ASSIGNMENT, course, -1L, -1L, -1, -1L) + viewModel.setData(assignment, arrayListOf(), FileUploadType.ASSIGNMENT, course, -1L, -1L, -1, -1L) every { fileUploadUtilsHelper.getFileSubmitObjectFromInputStream(any(), any(), any()) } returns createSubmitObject("test.pdf") @@ -132,7 +132,7 @@ class FileUploadViewModelTest { val viewModel = createViewModel() val course = createCourse(1L, "Course 1") val assignment = createAssignment(1L, "Assignment 1", 1L, listOf("pdf")) - viewModel.setData(assignment, null, FileUploadType.ASSIGNMENT, course, -1L, -1L, -1, -1L) + viewModel.setData(assignment, arrayListOf(), FileUploadType.ASSIGNMENT, course, -1L, -1L, -1, -1L) every { fileUploadUtilsHelper.getFileSubmitObjectFromInputStream(any(), any(), any()) } returns createSubmitObject("test.doc") @@ -150,7 +150,7 @@ class FileUploadViewModelTest { val viewModel = createViewModel() val course = createCourse(1L, "Course 1") val assignment = createAssignment(1L, "Assignment 1", 1L) - viewModel.setData(assignment, null, FileUploadType.ASSIGNMENT, course, -1L, -1L, -1, -1L) + viewModel.setData(assignment, arrayListOf(), FileUploadType.ASSIGNMENT, course, -1L, -1L, -1, -1L) viewModel.uploadFiles() @@ -163,7 +163,7 @@ class FileUploadViewModelTest { val viewModel = createViewModel() val course = createCourse(1L, "Course 1") val assignment = createAssignment(1L, "Assignment 1", 1L, submissionTypes = listOf("online_text_entry")) - viewModel.setData(assignment, null, FileUploadType.ASSIGNMENT, course, -1L, -1L, -1, -1L) + viewModel.setData(assignment, arrayListOf(), FileUploadType.ASSIGNMENT, course, -1L, -1L, -1, -1L) viewModel.addFile(uri) viewModel.uploadFiles() @@ -181,7 +181,7 @@ class FileUploadViewModelTest { every { fileUploadUtilsHelper.getFileSubmitObjectFromInputStream(any(), any(), any()) } returns submitObject - viewModel.setData(assignment, null, FileUploadType.ASSIGNMENT, course, -1L, -1L, -1, -1L) + viewModel.setData(assignment, arrayListOf(), FileUploadType.ASSIGNMENT, course, -1L, -1L, -1, -1L) viewModel.data.observe(lifecycleOwner) {} viewModel.events.observe(lifecycleOwner) {} From 734162308a8206429e4142992d0144461fb8e4a7 Mon Sep 17 00:00:00 2001 From: Tamas Kozmer <72397075+tamaskozmer@users.noreply.github.com> Date: Wed, 14 Sep 2022 10:41:14 +0200 Subject: [PATCH 38/49] [MBL-16252][Student][Teacher] Revert dark mode popup refs: MBL-16252 affects: Student, Teacher release note: none --- .../ui/utils/StudentActivityTestRule.kt | 4 + .../student/activity/NavigationActivity.kt | 8 +- .../main/res/values/themes_canvastheme.xml | 1 + .../ui/utils/TeacherActivityTestRule.kt | 4 + .../teacher/activities/InitActivity.kt | 7 + apps/teacher/src/main/res/values/styles.xml | 1 + .../src/main/res/drawable/bg_bottom_sheet.xml | 25 ++++ .../main/res/drawable/ic_dark_light_theme.xml | 10 ++ .../src/main/res/values-ar/strings.xml | 7 + .../main/res/values-b+da+instk12/strings.xml | 7 + .../res/values-b+en+AU+unimelb/strings.xml | 7 + .../res/values-b+en+GB+instukhe/strings.xml | 7 + .../main/res/values-b+nb+instk12/strings.xml | 7 + .../main/res/values-b+sv+instk12/strings.xml | 7 + .../src/main/res/values-b+zh+Hans/strings.xml | 7 + .../src/main/res/values-b+zh+Hant/strings.xml | 7 + .../src/main/res/values-ca/strings.xml | 7 + .../src/main/res/values-cy/strings.xml | 7 + .../src/main/res/values-da/strings.xml | 7 + .../src/main/res/values-de/strings.xml | 7 + .../src/main/res/values-en-rAU/strings.xml | 7 + .../src/main/res/values-en-rCA/strings.xml | 7 + .../src/main/res/values-en-rCY/strings.xml | 7 + .../src/main/res/values-en-rGB/strings.xml | 7 + .../src/main/res/values-es-rES/strings.xml | 7 + .../src/main/res/values-es/strings.xml | 7 + .../src/main/res/values-fi/strings.xml | 7 + .../src/main/res/values-fr-rCA/strings.xml | 7 + .../src/main/res/values-fr/strings.xml | 7 + .../src/main/res/values-ht/strings.xml | 7 + .../src/main/res/values-is/strings.xml | 7 + .../src/main/res/values-it/strings.xml | 7 + .../src/main/res/values-ja/strings.xml | 7 + .../src/main/res/values-mi/strings.xml | 7 + .../src/main/res/values-nb/strings.xml | 7 + .../src/main/res/values-nl/strings.xml | 7 + .../src/main/res/values-pl/strings.xml | 7 + .../src/main/res/values-pt-rBR/strings.xml | 7 + .../src/main/res/values-pt-rPT/strings.xml | 7 + .../src/main/res/values-ru/strings.xml | 7 + .../src/main/res/values-sl/strings.xml | 7 + .../src/main/res/values-sv/strings.xml | 7 + .../src/main/res/values-th/strings.xml | 7 + .../src/main/res/values-vi/strings.xml | 7 + .../src/main/res/values-zh-rHK/strings.xml | 7 + .../src/main/res/values-zh/strings.xml | 7 + libs/pandares/src/main/res/values/strings.xml | 8 + .../themeselector/ThemeSelectorBottomSheet.kt | 67 +++++++++ .../pandautils/utils/ThemePrefs.kt | 4 +- .../layout/bottom_sheet_theme_selector.xml | 141 ++++++++++++++++++ .../main/res/values/themes_canvasthemes.xml | 7 + 51 files changed, 551 insertions(+), 2 deletions(-) create mode 100644 libs/pandares/src/main/res/drawable/bg_bottom_sheet.xml create mode 100644 libs/pandares/src/main/res/drawable/ic_dark_light_theme.xml create mode 100644 libs/pandautils/src/main/java/com/instructure/pandautils/features/themeselector/ThemeSelectorBottomSheet.kt create mode 100644 libs/pandautils/src/main/res/layout/bottom_sheet_theme_selector.xml diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/utils/StudentActivityTestRule.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/utils/StudentActivityTestRule.kt index afba726dc8..bace30a97b 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/utils/StudentActivityTestRule.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/utils/StudentActivityTestRule.kt @@ -23,6 +23,7 @@ import com.instructure.student.util.StudentPrefs import com.instructure.espresso.InstructureActivityTestRule import com.instructure.loginapi.login.util.PreviousUsersUtils import com.instructure.pandautils.utils.PandaAppResetter +import com.instructure.pandautils.utils.ThemePrefs class StudentActivityTestRule(activityClass: Class) : InstructureActivityTestRule(activityClass) { @@ -31,6 +32,9 @@ class StudentActivityTestRule(activityClass: Class) : Instructu StudentPrefs.clearPrefs() CacheControlFlags.clearPrefs() PreviousUsersUtils.clear(context) + + // We need to set this true so the theme selector won't stop our tests. + ThemePrefs.themeSelectionShown = true } } 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 daba0f24cb..ed14703185 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 @@ -67,7 +67,7 @@ import com.instructure.loginapi.login.dialog.MasqueradingDialog import com.instructure.loginapi.login.tasks.LogoutTask import com.instructure.pandautils.features.help.HelpDialogFragment import com.instructure.pandautils.features.notification.preferences.PushNotificationPreferencesFragment - +import com.instructure.pandautils.features.themeselector.ThemeSelectorBottomSheet import com.instructure.pandautils.models.PushNotification import com.instructure.pandautils.receivers.PushExternalReceiver import com.instructure.pandautils.typeface.TypefaceBehavior @@ -261,6 +261,12 @@ class NavigationActivity : BaseRouterActivity(), Navigation, MasqueradingDialog. val savedBottomScreens = savedInstanceState?.getStringArrayList(BOTTOM_SCREENS_BUNDLE_KEY) restoreBottomNavState(savedBottomScreens) + + if (!ThemePrefs.themeSelectionShown) { + val themeSelector = ThemeSelectorBottomSheet() + themeSelector.show(supportFragmentManager, ThemeSelectorBottomSheet::javaClass.name) + ThemePrefs.themeSelectionShown = true + } } private fun restoreBottomNavState(savedBottomScreens: List?) { diff --git a/apps/student/src/main/res/values/themes_canvastheme.xml b/apps/student/src/main/res/values/themes_canvastheme.xml index b94010c29e..5a0ec587d4 100755 --- a/apps/student/src/main/res/values/themes_canvastheme.xml +++ b/apps/student/src/main/res/values/themes_canvastheme.xml @@ -32,6 +32,7 @@ true @style/CanvasDialogTheme_Default @style/CanvasDialogTheme_Default + @style/AppBottomSheetDialogTheme @style/PropertyInspector @style/ContextualToolbarStyle @style/AnnotationCreationToolbarIconsStyle diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/utils/TeacherActivityTestRule.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/utils/TeacherActivityTestRule.kt index 1cdebb5045..1b41aeca71 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/utils/TeacherActivityTestRule.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/utils/TeacherActivityTestRule.kt @@ -21,6 +21,7 @@ import android.content.Context import com.instructure.espresso.InstructureActivityTestRule import com.instructure.loginapi.login.util.PreviousUsersUtils import com.instructure.pandautils.utils.PandaAppResetter +import com.instructure.pandautils.utils.ThemePrefs import com.instructure.teacher.utils.TeacherPrefs class TeacherActivityTestRule(activityClass: Class) : InstructureActivityTestRule(activityClass) { @@ -29,6 +30,9 @@ class TeacherActivityTestRule(activityClass: Class) : Instructur PandaAppResetter.reset(context) TeacherPrefs.safeClearPrefs() PreviousUsersUtils.clear(context) + + // We need to set this true so the theme selector won't stop our tests. + ThemePrefs.themeSelectionShown = true } } 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 8346569e4d..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 @@ -55,6 +55,7 @@ import com.instructure.loginapi.login.tasks.LogoutTask import com.instructure.pandautils.activities.BasePresenterActivity import com.instructure.pandautils.dialogs.RatingDialog import com.instructure.pandautils.features.help.HelpDialogFragment +import com.instructure.pandautils.features.themeselector.ThemeSelectorBottomSheet import com.instructure.pandautils.models.PushNotification import com.instructure.pandautils.receivers.PushExternalReceiver import com.instructure.pandautils.typeface.TypefaceBehavior @@ -162,6 +163,12 @@ class InitActivity : BasePresenterActivity@color/textDark @style/CanvasDialogTheme_Default @style/CanvasDialogTheme_Default + @style/AppBottomSheetDialogTheme @color/backgroundLightest @style/Widget.ActionButton.Overflow diff --git a/libs/pandares/src/main/res/drawable/bg_bottom_sheet.xml b/libs/pandares/src/main/res/drawable/bg_bottom_sheet.xml new file mode 100644 index 0000000000..1c8a12bf12 --- /dev/null +++ b/libs/pandares/src/main/res/drawable/bg_bottom_sheet.xml @@ -0,0 +1,25 @@ + + + + + + + \ No newline at end of file diff --git a/libs/pandares/src/main/res/drawable/ic_dark_light_theme.xml b/libs/pandares/src/main/res/drawable/ic_dark_light_theme.xml new file mode 100644 index 0000000000..3dd9bc14e7 --- /dev/null +++ b/libs/pandares/src/main/res/drawable/ic_dark_light_theme.xml @@ -0,0 +1,10 @@ + + + diff --git a/libs/pandares/src/main/res/values-ar/strings.xml b/libs/pandares/src/main/res/values-ar/strings.xml index 9db8ce883b..15b0ad4877 100644 --- a/libs/pandares/src/main/res/values-ar/strings.xml +++ b/libs/pandares/src/main/res/values-ar/strings.xml @@ -1317,5 +1317,12 @@ فاتح داكن مثل الجهاز + 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 e3d16381ac..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 @@ -1262,5 +1262,12 @@ Lys Mørk 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 d033761fcf..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 @@ -1262,5 +1262,12 @@ Light Dark 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 f58d7a871a..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 @@ -1262,5 +1262,12 @@ Light Dark 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 e2dbf331b9..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 @@ -1263,5 +1263,12 @@ Lys Mørk 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 9adf0e613e..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 @@ -1262,5 +1262,12 @@ Ljus Mörk 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 ec26a77179..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 @@ -1248,5 +1248,12 @@ 浅色的、轻的 深色的、黑的 与设备相同 + 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 0dad18a366..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 @@ -1248,5 +1248,12 @@ 亮色 暗色 和裝置相同 + Canvas 現在可在暗色主題中使用 + 選擇應用程式主題 + 淡色主題 + 暗色主題 + 和裝置主題相同 + 儲存 + 您可以稍後在應用桯式設定中變更 擷取 diff --git a/libs/pandares/src/main/res/values-ca/strings.xml b/libs/pandares/src/main/res/values-ca/strings.xml index 32e3c1c4ba..84f6eaac65 100644 --- a/libs/pandares/src/main/res/values-ca/strings.xml +++ b/libs/pandares/src/main/res/values-ca/strings.xml @@ -1263,5 +1263,12 @@ Clar Fosc 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 85324c198c..95b5760db0 100644 --- a/libs/pandares/src/main/res/values-cy/strings.xml +++ b/libs/pandares/src/main/res/values-cy/strings.xml @@ -1262,5 +1262,12 @@ Golau Tywyll 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 c5bc04f866..c6f82e0923 100644 --- a/libs/pandares/src/main/res/values-da/strings.xml +++ b/libs/pandares/src/main/res/values-da/strings.xml @@ -1262,5 +1262,12 @@ Lys Mørk 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 605bfcff28..16b28eec4b 100644 --- a/libs/pandares/src/main/res/values-de/strings.xml +++ b/libs/pandares/src/main/res/values-de/strings.xml @@ -1262,5 +1262,12 @@ Hell Dunkel 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 6e9ef7a113..e6e3c9557a 100644 --- a/libs/pandares/src/main/res/values-en-rAU/strings.xml +++ b/libs/pandares/src/main/res/values-en-rAU/strings.xml @@ -1262,5 +1262,12 @@ Light Dark 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 07291c8e2b..33b713916c 100644 --- a/libs/pandares/src/main/res/values-en-rCA/strings.xml +++ b/libs/pandares/src/main/res/values-en-rCA/strings.xml @@ -1268,6 +1268,13 @@ Dark 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-rCY/strings.xml b/libs/pandares/src/main/res/values-en-rCY/strings.xml index f58d7a871a..9b7f1fc049 100644 --- a/libs/pandares/src/main/res/values-en-rCY/strings.xml +++ b/libs/pandares/src/main/res/values-en-rCY/strings.xml @@ -1262,5 +1262,12 @@ Light Dark 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-rGB/strings.xml b/libs/pandares/src/main/res/values-en-rGB/strings.xml index 6f9d6ca4ee..a669e567ba 100644 --- a/libs/pandares/src/main/res/values-en-rGB/strings.xml +++ b/libs/pandares/src/main/res/values-en-rGB/strings.xml @@ -1262,5 +1262,12 @@ Light Dark 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-es-rES/strings.xml b/libs/pandares/src/main/res/values-es-rES/strings.xml index ed083dd091..6d241f41d2 100644 --- a/libs/pandares/src/main/res/values-es-rES/strings.xml +++ b/libs/pandares/src/main/res/values-es-rES/strings.xml @@ -1264,5 +1264,12 @@ Iluminado Oscuro Coincidir con el dispositivo + Canvas está ahora disponible en tema oscuro + Elegir el tema de la aplicación + Tema claro + Tema oscuro + Coincidir con el tema del dispositivo + Guardar + Se puede cambiar después en los ajustes de la aplicación Captar diff --git a/libs/pandares/src/main/res/values-es/strings.xml b/libs/pandares/src/main/res/values-es/strings.xml index 2e96201470..d3060859f7 100644 --- a/libs/pandares/src/main/res/values-es/strings.xml +++ b/libs/pandares/src/main/res/values-es/strings.xml @@ -1262,5 +1262,12 @@ Claro Oscuro Igual que el dispositivo + Canvas está ahora disponible en tema oscuro + Elegir el tema de la aplicación + Tema claro + Tema oscuro + Igual que el tema del dispositivo + Guardar + Puede cambiarlo más tarde en las configuraciones de la aplicación Capturar diff --git a/libs/pandares/src/main/res/values-fi/strings.xml b/libs/pandares/src/main/res/values-fi/strings.xml index 4f851a7531..42163d106c 100644 --- a/libs/pandares/src/main/res/values-fi/strings.xml +++ b/libs/pandares/src/main/res/values-fi/strings.xml @@ -1262,5 +1262,12 @@ Valo Tumma Sama kuin laite + Canvas on nyt saatavilla tummana teema + Valitse sovelluksen teema + Vaalea teema + Tumma teema + Sama kuin laitteen teema + Tallenna + Voit vaihtaa sen myöhemmin sovellusasetuksissa Nappaa diff --git a/libs/pandares/src/main/res/values-fr-rCA/strings.xml b/libs/pandares/src/main/res/values-fr-rCA/strings.xml index 4a790e24c9..4aa2cfbd87 100644 --- a/libs/pandares/src/main/res/values-fr-rCA/strings.xml +++ b/libs/pandares/src/main/res/values-fr-rCA/strings.xml @@ -1262,5 +1262,12 @@ Clair Foncé Identique au dispositif + Canvas est désormais disponible en thème sombre + Choisissez le thème de l’application + Thème clair + Thème sombre + Identique au thème du dispositif + Enregistrer + Vous pouvez le changer plus tard dans les paramètres de l’application Attraper diff --git a/libs/pandares/src/main/res/values-fr/strings.xml b/libs/pandares/src/main/res/values-fr/strings.xml index 66a82833e0..485ec478e4 100644 --- a/libs/pandares/src/main/res/values-fr/strings.xml +++ b/libs/pandares/src/main/res/values-fr/strings.xml @@ -1262,5 +1262,12 @@ Clair Foncé Identique à l’appareil + Canvas est désormais disponible en thème sombre + Sélectionnez le thème de l’application + Thème clair + Thème sombre + Identique au thème de l’appareil + Enregistrer + Vous pourrez modifier le thème ultérieurement dans les paramètres de l’application Attraper diff --git a/libs/pandares/src/main/res/values-ht/strings.xml b/libs/pandares/src/main/res/values-ht/strings.xml index e47be074bb..9e39f808a0 100644 --- a/libs/pandares/src/main/res/values-ht/strings.xml +++ b/libs/pandares/src/main/res/values-ht/strings.xml @@ -1262,5 +1262,12 @@ Limyè Nwa Menm ak aparèy la + Canvas disponib kounye a an mòd sonm + Chwazi app pou chanje mòd la + Mòd klere + Mòd sonm + Menm ak mòd aparèy la + Anrejistre + Ou ka chanje l apre nan paramèt app la. Sezi diff --git a/libs/pandares/src/main/res/values-is/strings.xml b/libs/pandares/src/main/res/values-is/strings.xml index 0156a983f0..91bb520a1e 100644 --- a/libs/pandares/src/main/res/values-is/strings.xml +++ b/libs/pandares/src/main/res/values-is/strings.xml @@ -1262,5 +1262,12 @@ Ljóst Dökkt Sama og tæki + Canvas er nú í boði með dökku þema + Veldu þema smáforrits + Ljóst þema + Dökkt þema + Sama og þema tækis + Vista + Þú getur breytt því síðar í stillingum smáforrits Grípa diff --git a/libs/pandares/src/main/res/values-it/strings.xml b/libs/pandares/src/main/res/values-it/strings.xml index cc15565b1e..67b958657e 100644 --- a/libs/pandares/src/main/res/values-it/strings.xml +++ b/libs/pandares/src/main/res/values-it/strings.xml @@ -1262,5 +1262,12 @@ Chiaro Scuro Come il dispositivo + Canvas è ora disponibile in tema scuso + Scegli tema app + Tema chiaro + Tema scuro + Come il tema dispositivo + Salva + Puoi modificarlo successivamente nelle impostazioni dell’app Afferra diff --git a/libs/pandares/src/main/res/values-ja/strings.xml b/libs/pandares/src/main/res/values-ja/strings.xml index 20f0ebfd14..99d2b39b07 100644 --- a/libs/pandares/src/main/res/values-ja/strings.xml +++ b/libs/pandares/src/main/res/values-ja/strings.xml @@ -1248,5 +1248,12 @@ 明るい 暗い デバイスと同じ + Canvasがダークテーマで利用可能に + アプリのテーマを選ぶ + ライトテーマ + ダークテーマ + デバイスのテーマと同じ + 保存 + 後でアプリの設定から変更可能 捕捉 diff --git a/libs/pandares/src/main/res/values-mi/strings.xml b/libs/pandares/src/main/res/values-mi/strings.xml index 4c259f072b..55767377ad 100644 --- a/libs/pandares/src/main/res/values-mi/strings.xml +++ b/libs/pandares/src/main/res/values-mi/strings.xml @@ -1262,5 +1262,12 @@ Marama Pōuri Rite tonu ka rite ki pūrere + Canvas ko inaianei e wātea ana i te pouri kaupapa + Whiriwhiria taupānga kaupapa + Maama kaupapa + Pouriuri kaupapa + Rite tonu ka rite ki pūrere kaupapa + Tiaki + Ka taea e koe te whakarereke i muri mai i te taupānga tautuhinga Kōrapurapu diff --git a/libs/pandares/src/main/res/values-nb/strings.xml b/libs/pandares/src/main/res/values-nb/strings.xml index 84a8208a14..2d3199d921 100644 --- a/libs/pandares/src/main/res/values-nb/strings.xml +++ b/libs/pandares/src/main/res/values-nb/strings.xml @@ -1263,5 +1263,12 @@ Lys Mørk 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-nl/strings.xml b/libs/pandares/src/main/res/values-nl/strings.xml index 373d3015c1..482387492b 100644 --- a/libs/pandares/src/main/res/values-nl/strings.xml +++ b/libs/pandares/src/main/res/values-nl/strings.xml @@ -1262,5 +1262,12 @@ Licht Donker Hetzelfde als apparaat + Canvas is niet beschikbaar in donker thema + Kies app-thema + Licht thema + Donker thema + Hetzelfde als apparaatthema + Opslaan + Je kunt dit later wijzigen in de app-instellingen. Pakken diff --git a/libs/pandares/src/main/res/values-pl/strings.xml b/libs/pandares/src/main/res/values-pl/strings.xml index 0bab7939fd..36da65abbf 100644 --- a/libs/pandares/src/main/res/values-pl/strings.xml +++ b/libs/pandares/src/main/res/values-pl/strings.xml @@ -1290,5 +1290,12 @@ Jasny Ciemny Taki sam jak urządzenia + Dla Canvas jest teraz dostępny ciemny motyw + Wybierz motyw aplikacji + Jasny motyw + Ciemny motyw + Taki sam jak motyw urządzenia + Zapisz + Można zmienić go później w Ustawieniach aplikacji Chwyć diff --git a/libs/pandares/src/main/res/values-pt-rBR/strings.xml b/libs/pandares/src/main/res/values-pt-rBR/strings.xml index 83370fba79..c0f7d685d4 100644 --- a/libs/pandares/src/main/res/values-pt-rBR/strings.xml +++ b/libs/pandares/src/main/res/values-pt-rBR/strings.xml @@ -1262,5 +1262,12 @@ Claro Escuro Igual ao dispositivo + O Canvas agora está disponível no tema escuro + Escolha o tema do aplicativo + Tema claro + Tema escuro + Igual ao tema do dispositivo + Salvar + Você pode alterá-lo mais tarde nas configurações do aplicativo Pegar diff --git a/libs/pandares/src/main/res/values-pt-rPT/strings.xml b/libs/pandares/src/main/res/values-pt-rPT/strings.xml index 84f99375af..42e243f7a1 100644 --- a/libs/pandares/src/main/res/values-pt-rPT/strings.xml +++ b/libs/pandares/src/main/res/values-pt-rPT/strings.xml @@ -1262,5 +1262,12 @@ Claro Escuro O mesmo que dispositivo + O ecrã está agora disponível em tema escuro + Escolha o tema da aplicação + Tema claro + Tema escuro + O mesmo tema do dispositivo + Guardar + Pode alterá-lo mais tarde nas definições da aplicação Agarrar diff --git a/libs/pandares/src/main/res/values-ru/strings.xml b/libs/pandares/src/main/res/values-ru/strings.xml index e8d3cd3c2d..44e54a176e 100644 --- a/libs/pandares/src/main/res/values-ru/strings.xml +++ b/libs/pandares/src/main/res/values-ru/strings.xml @@ -1290,5 +1290,12 @@ Светлая Темная Аналогично устройству + Canvas теперь доступно в темной теме + Выбрать тему приложения + Светлая тема + Темная тема + Аналогично теме устройства + Сохранить + Вы можете изменить этот параметр позднее в настройках приложения Захват diff --git a/libs/pandares/src/main/res/values-sl/strings.xml b/libs/pandares/src/main/res/values-sl/strings.xml index 3c6ccf18ba..3d8364be0e 100644 --- a/libs/pandares/src/main/res/values-sl/strings.xml +++ b/libs/pandares/src/main/res/values-sl/strings.xml @@ -1262,5 +1262,12 @@ Svetlo Temno Enako kot naprava + Sistem Canvas je zdaj na voljo v temni preobleki + Izberite preobleko aplikacije + Svetla preobleka + Temna preobleka + Enako kot preobleka na napravi + Shrani + To lahko pozneje spremenite v nastavitvah aplikacije Zgrabi diff --git a/libs/pandares/src/main/res/values-sv/strings.xml b/libs/pandares/src/main/res/values-sv/strings.xml index 888ceb3dc0..c165c9eaa5 100644 --- a/libs/pandares/src/main/res/values-sv/strings.xml +++ b/libs/pandares/src/main/res/values-sv/strings.xml @@ -1262,5 +1262,12 @@ Ljus Mörk 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-th/strings.xml b/libs/pandares/src/main/res/values-th/strings.xml index c44253501b..a75508fd00 100644 --- a/libs/pandares/src/main/res/values-th/strings.xml +++ b/libs/pandares/src/main/res/values-th/strings.xml @@ -1262,5 +1262,12 @@ สว่าง มืด เหมือนกันกับอุปกรณ์ + Canvas พร้อมใช้งานในธีมมืดแล้ว + เลือกธีมของแอพ + ธีมสว่าง + ธีมมืด + ธีมเหมือนกับอุปกรณ์ + บันทึก + คุณสามารถแก้ไขได้ในภายหลังจากค่าปรับตั้งแอพ คว้าไว้ diff --git a/libs/pandares/src/main/res/values-vi/strings.xml b/libs/pandares/src/main/res/values-vi/strings.xml index f0ab481b35..7fbcc90ae8 100644 --- a/libs/pandares/src/main/res/values-vi/strings.xml +++ b/libs/pandares/src/main/res/values-vi/strings.xml @@ -1263,5 +1263,12 @@ Sáng Tối Tương tự như thiết bị + Canvas hiện nay đã có sẵn ở chủ đề tối + Lựa chọn chủ đề ứng dụng + Chủ đề sáng + Chủ đề tối + Tương tự như chủ đề thiết bị + Lưu + Bạn có thể thay đổi sau trong cài đặt ứng dụng Nắm lấy diff --git a/libs/pandares/src/main/res/values-zh-rHK/strings.xml b/libs/pandares/src/main/res/values-zh-rHK/strings.xml index 0dad18a366..6808be99dc 100644 --- a/libs/pandares/src/main/res/values-zh-rHK/strings.xml +++ b/libs/pandares/src/main/res/values-zh-rHK/strings.xml @@ -1248,5 +1248,12 @@ 亮色 暗色 和裝置相同 + Canvas 現在可在暗色主題中使用 + 選擇應用程式主題 + 淡色主題 + 暗色主題 + 和裝置主題相同 + 儲存 + 您可以稍後在應用桯式設定中變更 擷取 diff --git a/libs/pandares/src/main/res/values-zh/strings.xml b/libs/pandares/src/main/res/values-zh/strings.xml index ec26a77179..b5ea856bd0 100644 --- a/libs/pandares/src/main/res/values-zh/strings.xml +++ b/libs/pandares/src/main/res/values-zh/strings.xml @@ -1248,5 +1248,12 @@ 浅色的、轻的 深色的、黑的 与设备相同 + Canvas 已推出深色主题 + 选择应用程序主题 + 浅色主题 + 深色主题 + 与设备主题相同 + 保存 + 您可以后在应用设置中更改 抓取图像 diff --git a/libs/pandares/src/main/res/values/strings.xml b/libs/pandares/src/main/res/values/strings.xml index cfa06b55f6..6f8d1fb058 100644 --- a/libs/pandares/src/main/res/values/strings.xml +++ b/libs/pandares/src/main/res/values/strings.xml @@ -1267,6 +1267,14 @@ Dark 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 File Upload diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/themeselector/ThemeSelectorBottomSheet.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/themeselector/ThemeSelectorBottomSheet.kt new file mode 100644 index 0000000000..b4f46cfcbb --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/themeselector/ThemeSelectorBottomSheet.kt @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2022 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.pandautils.features.themeselector + +import android.content.DialogInterface +import android.os.Bundle +import android.util.Log +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.appcompat.app.AppCompatDelegate +import androidx.core.widget.CompoundButtonCompat +import com.google.android.material.bottomsheet.BottomSheetDialogFragment +import com.instructure.pandautils.R +import com.instructure.pandautils.utils.AppTheme +import com.instructure.pandautils.utils.ThemePrefs +import com.instructure.pandautils.utils.ViewStyler +import com.instructure.pandautils.utils.onClick +import kotlinx.android.synthetic.main.bottom_sheet_theme_selector.* + +class ThemeSelectorBottomSheet : BottomSheetDialogFragment() { + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? = inflater.inflate(R.layout.bottom_sheet_theme_selector, container, false) + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + val radioButtonColor = ViewStyler.makeColorStateListForRadioGroup(requireContext().getColor(R.color.textDarkest), requireContext().getColor(R.color.textInfo)) + CompoundButtonCompat.setButtonTintList(buttonLightTheme, radioButtonColor) + CompoundButtonCompat.setButtonTintList(buttonDarkTheme, radioButtonColor) + CompoundButtonCompat.setButtonTintList(buttonDeviceTheme, radioButtonColor) + + saveButton.onClick { + val appTheme = when { + buttonLightTheme.isChecked -> AppTheme.LIGHT + buttonDarkTheme.isChecked -> AppTheme.DARK + else -> AppTheme.SYSTEM + } + setAppTheme(appTheme) + } + } + + private fun setAppTheme(appTheme: AppTheme) { + AppCompatDelegate.setDefaultNightMode(appTheme.nightModeType) + ThemePrefs.appTheme = appTheme.ordinal + dismiss() + } + +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/utils/ThemePrefs.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/utils/ThemePrefs.kt index 29549cdf8b..bea672ec29 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/utils/ThemePrefs.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/utils/ThemePrefs.kt @@ -54,7 +54,9 @@ object ThemePrefs : PrefManager("CanvasTheme") { var appTheme by IntPref(defaultValue = 0) - override fun keepBaseProps() = listOf(::appTheme) + var themeSelectionShown by BooleanPref() + + override fun keepBaseProps() = listOf(::appTheme, ::themeSelectionShown) override fun onClearPrefs() { } diff --git a/libs/pandautils/src/main/res/layout/bottom_sheet_theme_selector.xml b/libs/pandautils/src/main/res/layout/bottom_sheet_theme_selector.xml new file mode 100644 index 0000000000..4204d580a9 --- /dev/null +++ b/libs/pandautils/src/main/res/layout/bottom_sheet_theme_selector.xml @@ -0,0 +1,141 @@ + + + + + + + + + + + + + + + + + + + + + +