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 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 + } + } } } diff --git a/apps/flutter_parent/android/app/src/main/AndroidManifest.xml b/apps/flutter_parent/android/app/src/main/AndroidManifest.xml index eb3ec7352a..88c5a4cae2 100644 --- a/apps/flutter_parent/android/app/src/main/AndroidManifest.xml +++ b/apps/flutter_parent/android/app/src/main/AndroidManifest.xml @@ -31,7 +31,8 @@ android:theme="@style/LaunchTheme" android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode" android:hardwareAccelerated="true" - android:windowSoftInputMode="adjustResize"> + android:windowSoftInputMode="adjustResize" + android:exported="true"> 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/network/utils/analytics.dart b/apps/flutter_parent/lib/network/utils/analytics.dart index 4e92501eba..5564eb8147 100644 --- a/apps/flutter_parent/lib/network/utils/analytics.dart +++ b/apps/flutter_parent/lib/network/utils/analytics.dart @@ -12,7 +12,6 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . import 'package:device_info/device_info.dart'; -import 'package:firebase_analytics/firebase_analytics.dart'; import 'package:firebase_crashlytics/firebase_crashlytics.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_parent/utils/debug_flags.dart'; @@ -79,12 +78,11 @@ class AnalyticsParamConstants { } class Analytics { - FirebaseAnalytics get _analytics => FirebaseAnalytics(); - /// Set the current screen in Firebase Analytics + /// Set the current screen in analytics void setCurrentScreen(String screenName) async { if (kReleaseMode) { - await _analytics.setCurrentScreen(screenName: screenName); + } if (DebugFlags.isDebug) { @@ -92,7 +90,7 @@ class Analytics { } } - /// Log an event to Firebase analytics (only in release mode). + /// Log an event to analytics (only in release mode). /// If isDebug, it will also print to the console /// /// Params @@ -100,7 +98,7 @@ class Analytics { /// * [extras] a map of keys [AnalyticsParamConstants] to values. Use sparingly, we only get 25 unique parameters void logEvent(String event, {Map extras = const {}}) async { if (kReleaseMode) { - await _analytics.logEvent(name: event, parameters: extras); + } if (DebugFlags.isDebug) { @@ -122,14 +120,6 @@ class Analytics { /// Sets environment properties such as the build type and SDK int. This only needs to be called once per session. void setEnvironmentProperties() async { - var androidInfo = await DeviceInfoPlugin().androidInfo; - await _analytics.setUserProperty( - name: AnalyticsEventConstants.USER_PROPERTY_BUILD_TYPE, - value: kReleaseMode ? 'release' : 'debug', - ); - await _analytics.setUserProperty( - name: AnalyticsEventConstants.USER_PROPERTY_OS_VERSION, - value: androidInfo.version.sdkInt.toString(), - ); + } } 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/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/flutter_parent/pubspec.lock b/apps/flutter_parent/pubspec.lock index 3def51903f..fc0fa4e97d 100644 --- a/apps/flutter_parent/pubspec.lock +++ b/apps/flutter_parent/pubspec.lock @@ -337,34 +337,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "4.2.0" - firebase: - dependency: transitive - description: - name: firebase - url: "https://pub.dartlang.org" - source: hosted - version: "9.0.2" - firebase_analytics: - dependency: "direct main" - description: - name: firebase_analytics - url: "https://pub.dartlang.org" - source: hosted - version: "8.3.4" - firebase_analytics_platform_interface: - dependency: transitive - description: - name: firebase_analytics_platform_interface - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.1" - firebase_analytics_web: - dependency: transitive - description: - name: firebase_analytics_web - url: "https://pub.dartlang.org" - source: hosted - version: "0.3.0+1" firebase_core: dependency: "direct main" description: diff --git a/apps/flutter_parent/pubspec.yaml b/apps/flutter_parent/pubspec.yaml index 98699a76f3..0b2485a37e 100644 --- a/apps/flutter_parent/pubspec.yaml +++ b/apps/flutter_parent/pubspec.yaml @@ -39,7 +39,6 @@ dependencies: sdk: flutter flutter_localizations: sdk: flutter - firebase_analytics: ^8.3.4 firebase_remote_config: ^0.11.0+2 firebase_core: ^1.8.0 firebase_crashlytics: ^2.2.4 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); diff --git a/apps/student/build.gradle b/apps/student/build.gradle index 525fcedb68..10be6c70f8 100644 --- a/apps/student/build.gradle +++ b/apps/student/build.gradle @@ -54,8 +54,8 @@ android { applicationId "com.instructure.candroid" minSdkVersion Versions.MIN_SDK targetSdkVersion Versions.TARGET_SDK - versionCode = 242 - versionName = '6.19.0' + versionCode = 243 + versionName = '6.20.0' vectorDrawables.useSupportLibrary = true multiDexEnabled = true @@ -271,12 +271,11 @@ dependencies { testImplementation Libs.ANDROIDX_CORE_TESTING /* Firebase */ - implementation platform(Libs.FIREBASE_BOM) + implementation platform(Libs.FIREBASE_BOM) { + exclude group: 'com.google.firebase', module: 'firebase-analytics' + } implementation Libs.FIREBASE_MESSAGING implementation Libs.FIREBASE_CRASHLYTICS_NDK - implementation (Libs.FIREBASE_ANALYTICS) { - transitive = true - } implementation (Libs.FIREBASE_CRASHLYTICS) { transitive = true } @@ -308,7 +307,6 @@ dependencies { implementation Libs.ANDROIDX_CONSTRAINT_LAYOUT implementation Libs.ANDROIDX_DESIGN implementation Libs.ANDROIDX_RECYCLERVIEW - implementation Libs.PLAY_SERVICES_ANALYTICS implementation Libs.ANDROIDX_PALETTE implementation Libs.PLAY_CORE @@ -332,6 +330,10 @@ dependencies { kaptAndroidTestQa Libs.HILT_TESTING_COMPILER androidTestImplementation Libs.UI_AUTOMATOR + + /* WorkManager */ + implementation Libs.ANDROIDX_WORK_MANAGER + implementation Libs.ANDROIDX_WORK_MANAGER_KTX } // Comment out this line if the reporting logic starts going wonky. 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/student/flank_multi_api_level.yml b/apps/student/flank_multi_api_level.yml index cf63ab1f30..dd25260b89 100644 --- a/apps/student/flank_multi_api_level.yml +++ b/apps/student/flank_multi_api_level.yml @@ -12,17 +12,17 @@ gcloud: record-video: true timeout: 60m test-targets: - - notAnnotation com.instructure.canvas.espresso.E2E, com.instructure.canvas.espresso.Stub + - 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: Nexus6P + - model: NexusLowRes version: 27 locale: en_US orientation: portrait - - model: Nexus6P + - model: NexusLowRes version: 28 locale: en_US orientation: portrait - - model: Nexus6P + - model: NexusLowRes version: 29 locale: en_US orientation: portrait diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/AssignmentsE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/AssignmentsE2ETest.kt index 77561fa26e..871eca2058 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/AssignmentsE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/AssignmentsE2ETest.kt @@ -404,13 +404,9 @@ class AssignmentsE2ETest: StudentTest() { assignmentDetailsPage.goToSubmissionDetails() submissionDetailsPage.openComments() - // MBL-13604: This does not work on FTL, so we're commenting it out for now. - // You could also break this out to a separate E2E test and annotate it with - // @Stub, so that we can run it locally but it doesn't run as part of our CI suite. - // send video comment - //submissionDetailsPage.addAndSendVideoComment() - //sleep(3000) // wait for video comment submission to propagate - //submissionDetailsPage.assertVideoCommentDisplayed() + submissionDetailsPage.addAndSendVideoComment() + sleep(3000) // wait for video comment submission to propagate + submissionDetailsPage.assertVideoCommentDisplayed() Log.d(STEP_TAG,"Send an audio comment.") submissionDetailsPage.addAndSendAudioComment() 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/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/e2e/k5/ScheduleE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/k5/ScheduleE2ETest.kt index 586620f868..f32ae3784d 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/k5/ScheduleE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/k5/ScheduleE2ETest.kt @@ -190,7 +190,7 @@ class ScheduleE2ETest : StudentTest() { schedulePage.scrollToItem(R.id.title, assignmentName, schedulePage.withAncestor(R.id.plannerItems)) schedulePage.assertMarkedAsDoneNotShown() schedulePage.clickDoneCheckbox() - schedulePage.swipeDown() + Thread.sleep(2000) schedulePage.assertMarkedAsDoneShown() } diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/DiscussionsInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/DiscussionsInteractionTest.kt index 4b28e5fc11..d40f45ac7c 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/DiscussionsInteractionTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/DiscussionsInteractionTest.kt @@ -19,17 +19,8 @@ package com.instructure.student.ui.interaction import android.os.SystemClock.sleep import androidx.test.espresso.Espresso import androidx.test.espresso.web.webdriver.Locator -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.addAssignment -import com.instructure.canvas.espresso.mockCanvas.addDiscussionTopicToCourse -import com.instructure.canvas.espresso.mockCanvas.addFileToCourse -import com.instructure.canvas.espresso.mockCanvas.addReplyToDiscussion -import com.instructure.canvas.espresso.mockCanvas.init -import com.instructure.canvasapi2.models.Assignment -import com.instructure.canvasapi2.models.CanvasContextPermission -import com.instructure.canvasapi2.models.DiscussionEntry -import com.instructure.canvasapi2.models.RemoteFile -import com.instructure.canvasapi2.models.Tab +import com.instructure.canvas.espresso.mockCanvas.* +import com.instructure.canvasapi2.models.* import com.instructure.panda_annotations.FeatureCategory import com.instructure.panda_annotations.Priority import com.instructure.panda_annotations.TestCategory @@ -475,7 +466,8 @@ class DiscussionsInteractionTest : StudentTest() { val attachment = createHtmlAttachment(data, attachmentHtml) discussionEntry.attachments = mutableListOf(attachment) - discussionDetailsPage.refresh() // To pick up updated reply + discussionDetailsPage.refresh() + Thread.sleep(3000) //allow some time to the reply to propagate discussionDetailsPage.assertReplyDisplayed(discussionEntry) discussionDetailsPage.assertReplyAttachment(discussionEntry) discussionDetailsPage.previewAndCheckReplyAttachment(discussionEntry, 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/interaction/ShareExtensionInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ShareExtensionInteractionTest.kt new file mode 100644 index 0000000000..7bae222dd1 --- /dev/null +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ShareExtensionInteractionTest.kt @@ -0,0 +1,311 @@ +/* + * 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.ui.interaction + +import android.app.Activity +import android.app.Instrumentation +import android.content.Intent +import android.net.Uri +import androidx.core.content.FileProvider +import androidx.test.espresso.intent.Intents +import androidx.test.espresso.intent.matcher.IntentMatchers +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.uiautomator.UiDevice +import androidx.test.uiautomator.UiSelector +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.init +import com.instructure.canvasapi2.models.Assignment +import com.instructure.canvasapi2.models.User +import com.instructure.pandautils.utils.Const +import com.instructure.student.ui.utils.StudentTest +import com.instructure.student.ui.utils.tokenLogin +import dagger.hilt.android.testing.HiltAndroidTest +import org.hamcrest.core.AllOf +import org.junit.Test +import java.io.File + +@HiltAndroidTest +class ShareExtensionInteractionTest : StudentTest() { + + override fun displaysPageObjects() = Unit + + @Test + fun shareExtensionShowsUpCorrectlyWhenSharingFileFromExternalSource() { + val data = createMockData() + val student = data.students[0] + val uri = setupFileOnDevice("sample.jpg") + val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) + + login(student) + device.pressHome() + + shareExternalFile(uri) + + device.findObject(UiSelector().text("Canvas")).click() + device.waitForIdle() + + shareExtensionTargetPage.assertPageObjects() + shareExtensionTargetPage.assertFilesCheckboxIsSelected() + shareExtensionTargetPage.assertUserName(student.name) + } + + @Test + fun fileUploadDialogShowsCorrectlyForMyFilesUpload() { + val data = createMockData() + val student = data.students[0] + val uri = setupFileOnDevice("sample.jpg") + val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) + + login(student) + device.pressHome() + + shareExternalFile(uri) + + device.findObject(UiSelector().text("Canvas")).click() + device.waitForIdle() + + shareExtensionTargetPage.pressNext() + + fileUploadPage.assertPageObjects() + fileUploadPage.assertDialogTitle("Upload To My Files") + fileUploadPage.assertFileDisplayed("sample.jpg") + } + + @Test + fun addAndRemoveFileFromFileUploadDialog() { + val data = createMockData() + val student = data.students[0] + val uri = setupFileOnDevice("sample.jpg") + setupFileOnDevice("samplepdf.pdf") + val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) + + login(student) + device.pressHome() + + shareExternalFile(uri) + + device.findObject(UiSelector().text("Canvas")).click() + device.waitForIdle() + + shareExtensionTargetPage.pressNext() + + fileUploadPage.assertPageObjects() + fileUploadPage.assertFileDisplayed("sample.jpg") + + fileUploadPage.removeFile("sample.jpg") + + // Add new file + Intents.init() + try { + stubFilePickerIntent("samplepdf.pdf") + fileUploadPage.chooseDevice() + } + finally { + Intents.release() + } + + fileUploadPage.assertFileNotDisplayed("sample.jpg") + fileUploadPage.assertFileDisplayed("samplepdf.pdf") + } + + @Test + fun fileUploadDialogShowsCorrectlyForAssignmentSubmission() { + val data = createMockData() + val student = data.students[0] + val uri = setupFileOnDevice("sample.jpg") + val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) + + val assignment = data.addAssignment(data.courses.values.first().id, submissionType = Assignment.SubmissionType.ONLINE_UPLOAD) + + login(student) + device.pressHome() + + shareExternalFile(uri) + + device.findObject(UiSelector().text("Canvas")).click() + device.waitForIdle() + + shareExtensionTargetPage.selectSubmission() + shareExtensionTargetPage.assertCourseSelectorDisplayedWithCourse(data.courses.values.first().name) + shareExtensionTargetPage.assertAssignmentSelectorDisplayedWithAssignment(assignment.name!!) + shareExtensionTargetPage.pressNext() + + fileUploadPage.assertPageObjects() + fileUploadPage.assertDialogTitle("Submission") + fileUploadPage.assertFileDisplayed("sample.jpg") + } + + // Clicking spinner item not working. + @Test + @Stub + fun changeTargetAssignment() { + val data = createMockData() + val student = data.students[0] + val uri = setupFileOnDevice("sample.jpg") + val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) + + data.addAssignment(data.courses.values.first().id, submissionType = Assignment.SubmissionType.ONLINE_UPLOAD) + val assignment2 = data.addAssignment(data.courses.values.first().id, submissionType = Assignment.SubmissionType.ONLINE_UPLOAD) + + login(student) + device.pressHome() + + shareExternalFile(uri) + + device.findObject(UiSelector().text("Canvas")).click() + device.waitForIdle() + + shareExtensionTargetPage.selectSubmission() + shareExtensionTargetPage.selectAssignment(assignment2.name!!) + + shareExtensionTargetPage.pressNext() + + fileUploadPage.assertPageObjects() + fileUploadPage.assertDialogTitle("Submission") + fileUploadPage.assertFileDisplayed("sample.jpg") + } + + @Test + fun shareExtensionShowsUpCorrectlyWhenSharingMultipleFiles() { + val data = createMockData() + val student = data.students[0] + val uri = setupFileOnDevice("sample.jpg") + val uri2 = setupFileOnDevice("samplepdf.pdf") + val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) + + login(student) + device.pressHome() + + shareMultipleFiles(arrayListOf(uri, uri2)) + + device.findObject(UiSelector().text("Canvas")).click() + device.waitForIdle() + + shareExtensionTargetPage.assertPageObjects() + shareExtensionTargetPage.assertFilesCheckboxIsSelected() + shareExtensionTargetPage.assertUserName(student.name) + + shareExtensionTargetPage.pressNext() + + fileUploadPage.assertPageObjects() + fileUploadPage.assertFileDisplayed("sample.jpg") + fileUploadPage.assertFileDisplayed("samplepdf.pdf") + } + + @Test + fun testFileAssignmentSubmission() { + val data = createMockData() + val student = data.students[0] + val uri = setupFileOnDevice("sample.jpg") + val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) + + data.addAssignment(data.courses.values.first().id, submissionType = Assignment.SubmissionType.ONLINE_UPLOAD) + + login(student) + device.pressHome() + + shareExternalFile(uri) + + device.findObject(UiSelector().text("Canvas")).click() + device.waitForIdle() + + shareExtensionTargetPage.selectSubmission() + shareExtensionTargetPage.pressNext() + fileUploadPage.clickTurnIn() + + shareExtensionStatusPage.assertPageObjects() + shareExtensionStatusPage.assertAssignemntSubmissionSuccess() + } + + private fun createMockData(): MockCanvas { + + val data = MockCanvas.init( + studentCount = 1, + teacherCount = 1, + courseCount = 1, + favoriteCourseCount = 1 + ) + + return data + } + + private fun login(student: User) { + val token = MockCanvas.data.tokenFor(student) + tokenLogin(MockCanvas.data.domain, token!!, student) + } + + private fun setupFileOnDevice(fileName: String): Uri { + copyAssetFileToExternalCache(activityRule.activity, fileName) + + val dir = activityRule.activity.externalCacheDir + val file = File(dir?.path, fileName) + + val instrumentationContext = InstrumentationRegistry.getInstrumentation().context + return FileProvider.getUriForFile( + instrumentationContext, + "com.instructure.candroid" + Const.FILE_PROVIDER_AUTHORITY, + file + ) + } + + private fun shareExternalFile(uri: Uri) { + val intent = Intent().apply { + action = Intent.ACTION_SEND + putExtra(Intent.EXTRA_STREAM, uri) + type = "image/jpg" + } + + val chooser = Intent.createChooser(intent, null) + chooser.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + + InstrumentationRegistry.getInstrumentation().context.startActivity(chooser) + } + + private fun shareMultipleFiles(uris: ArrayList) { + val intent = Intent().apply { + action = Intent.ACTION_SEND_MULTIPLE + putParcelableArrayListExtra(Intent.EXTRA_STREAM, uris) + type = "*/*" + } + + val chooser = Intent.createChooser(intent, null) + chooser.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + + InstrumentationRegistry.getInstrumentation().context.startActivity(chooser) + } + + private fun stubFilePickerIntent(fileName: String) { + val resultData = Intent() + val dir = activityRule.activity.externalCacheDir + val file = File(dir?.path, fileName) + val newFileUri = FileProvider.getUriForFile( + activityRule.activity, + "com.instructure.candroid" + Const.FILE_PROVIDER_AUTHORITY, + file + ) + resultData.data = newFileUri + + Intents.intending( + AllOf.allOf( + IntentMatchers.hasAction(Intent.ACTION_GET_CONTENT), + IntentMatchers.hasType("*/*"), + ) + ).respondWith(Instrumentation.ActivityResult(Activity.RESULT_OK, resultData)) + } +} \ No newline at end of file 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..4aacb3f12e 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 @@ -20,6 +20,7 @@ import android.app.Instrumentation import android.content.Intent import android.net.Uri import android.provider.MediaStore +import androidx.core.content.FileProvider import androidx.test.espresso.intent.ActivityResultFunction import androidx.test.espresso.intent.Intents import androidx.test.espresso.intent.Intents.intending @@ -34,6 +35,7 @@ import com.instructure.panda_annotations.FeatureCategory import com.instructure.panda_annotations.Priority import com.instructure.panda_annotations.TestCategory import com.instructure.panda_annotations.TestMetaData +import com.instructure.pandautils.utils.Const import com.instructure.student.ui.utils.StudentTest import com.instructure.student.ui.utils.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest @@ -70,7 +72,11 @@ class UserFilesInteractionTest : StudentTest() { val resultData = Intent() val dir = activity.externalCacheDir val file = File(dir?.path, "sample.jpg") - val uri = Uri.fromFile(file) + val uri = FileProvider.getUriForFile( + activityRule.activity, + "com.instructure.candroid" + Const.FILE_PROVIDER_AUTHORITY, + file + ) resultData.data = uri activityResult = Instrumentation.ActivityResult(Activity.RESULT_OK, resultData) } @@ -96,8 +102,7 @@ class UserFilesInteractionTest : StudentTest() { intending( allOf( hasAction(Intent.ACTION_GET_CONTENT), - hasType("*/*"), - hasFlag(Intent.FLAG_GRANT_READ_URI_PERMISSION) + hasType("*/*") ) ).respondWith(activityResult) fileUploadPage.chooseDevice() @@ -167,8 +172,8 @@ class UserFilesInteractionTest : StudentTest() { // Set up the "from gallery" mock result, then press "from gallery" intending( allOf( - hasAction(Intent.ACTION_PICK), - hasFlag(Intent.FLAG_GRANT_READ_URI_PERMISSION) + hasAction(Intent.ACTION_GET_CONTENT), + hasType("image/*") ) ).respondWith(activityResult) fileUploadPage.chooseGallery() 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/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/pages/FileUploadPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/FileUploadPage.kt index 39b4c4fbdf..71a5238bee 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/FileUploadPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/FileUploadPage.kt @@ -18,15 +18,21 @@ package com.instructure.student.ui.pages import android.widget.Button import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.assertion.ViewAssertions.doesNotExist +import androidx.test.espresso.matcher.ViewMatchers import androidx.test.espresso.matcher.ViewMatchers.isAssignableFrom -import androidx.test.espresso.matcher.ViewMatchers.isDisplayed import androidx.test.espresso.matcher.ViewMatchers.withId import com.instructure.canvas.espresso.containsTextCaseInsensitive -import com.instructure.canvas.espresso.scrollRecyclerView 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.scrollTo +import com.instructure.espresso.page.onViewWithText +import com.instructure.espresso.page.plus +import com.instructure.espresso.page.waitForViewWithId +import com.instructure.espresso.page.withAncestor +import com.instructure.espresso.page.withDescendant +import com.instructure.espresso.page.withText import com.instructure.espresso.scrollTo import com.instructure.student.R import org.hamcrest.core.AllOf.allOf @@ -35,6 +41,8 @@ class FileUploadPage : BasePage() { private val cameraButton by OnViewWithId(R.id.fromCamera) private val galleryButton by OnViewWithId(R.id.fromGallery) private val deviceButton by OnViewWithId(R.id.fromDevice) + private val chooseFileTitle by OnViewWithId(R.id.chooseFileTitle) + private val chooseFileSubtitle by OnViewWithId(R.id.chooseFileSubtitle) fun chooseCamera() { cameraButton.scrollTo().click() @@ -51,4 +59,27 @@ class FileUploadPage : BasePage() { fun clickUpload() { onView(allOf(isAssignableFrom(Button::class.java),containsTextCaseInsensitive("upload"))).click() } + + fun clickTurnIn() { + onView(containsTextCaseInsensitive("turn in")).click() + } + + fun removeFile(filename: String) { + val fileItemMatcher = withId(R.id.fileItem) + withDescendant(withId(R.id.fileName) + withText(filename)) + + onView(withId(R.id.removeFile) + ViewMatchers.isDescendantOfA(fileItemMatcher)) + .click() + } + + fun assertDialogTitle(title: String) { + onViewWithText(title).assertDisplayed() + } + + fun assertFileDisplayed(filename: String) { + onView(withId(R.id.fileName) + withText(filename)) + } + + fun assertFileNotDisplayed(filename: String) { + onView(withId(R.id.fileName) + withText(filename)).check(doesNotExist()) + } } \ No newline at end of file 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() } diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/ShareExtensionStatusPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/ShareExtensionStatusPage.kt new file mode 100644 index 0000000000..0e94d5a7cb --- /dev/null +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/ShareExtensionStatusPage.kt @@ -0,0 +1,35 @@ +/* + * 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.ui.pages + +import com.instructure.espresso.WaitForViewWithId +import com.instructure.espresso.assertHasText +import com.instructure.espresso.page.BasePage +import com.instructure.student.R + +class ShareExtensionStatusPage : BasePage() { + + private val dialogTitle by WaitForViewWithId(R.id.dialogTitle) + private val subtitle by WaitForViewWithId(R.id.subtitle) + private val description by WaitForViewWithId(R.id.description) + + fun assertAssignemntSubmissionSuccess() { + dialogTitle.assertHasText(R.string.submission) + subtitle.assertHasText(R.string.submissionSuccessTitle) + description.assertHasText(R.string.submissionSuccessMessage) + } +} \ No newline at end of file diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/ShareExtensionTargetPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/ShareExtensionTargetPage.kt new file mode 100644 index 0000000000..042a4e2262 --- /dev/null +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/ShareExtensionTargetPage.kt @@ -0,0 +1,85 @@ +/* + * 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.ui.pages + +import androidx.test.espresso.Espresso.onData +import androidx.test.espresso.ViewAssertion +import androidx.test.espresso.assertion.ViewAssertions +import androidx.test.espresso.matcher.RootMatchers.* +import androidx.test.espresso.matcher.ViewMatchers +import com.instructure.espresso.OnViewWithId +import com.instructure.espresso.WaitForViewWithId +import com.instructure.espresso.WaitForViewWithStringTextIgnoreCase +import com.instructure.espresso.WaitForViewWithText +import com.instructure.espresso.assertDisplayed +import com.instructure.espresso.assertHasText +import com.instructure.espresso.assertSelected +import com.instructure.espresso.click +import com.instructure.espresso.page.BasePage +import com.instructure.espresso.page.onView +import com.instructure.espresso.page.onViewWithId +import com.instructure.espresso.page.onViewWithSpinnerText +import com.instructure.espresso.page.onViewWithText +import com.instructure.espresso.page.plus +import com.instructure.espresso.page.withAncestor +import com.instructure.espresso.page.withId +import com.instructure.espresso.page.withText +import com.instructure.student.R +import org.hamcrest.Matchers.anything + +class ShareExtensionTargetPage : BasePage() { + + private val avatar by WaitForViewWithId(R.id.avatar) + private val dialogTitle by WaitForViewWithId(R.id.dialogTitle) + private val userName by WaitForViewWithId(R.id.userName) + private val selectionWrapper by WaitForViewWithId(R.id.selectionWrapper) + private val filesCheckbox by WaitForViewWithId(R.id.filesCheckBox) + private val assignmentCheckbox by WaitForViewWithId(R.id.assignmentCheckBox) + private val nextButton by WaitForViewWithStringTextIgnoreCase("next") + private val cancelButton by WaitForViewWithStringTextIgnoreCase("cancel") + + fun assertFilesCheckboxIsSelected() { + filesCheckbox.check(ViewAssertions.matches(ViewMatchers.isChecked())) + } + + fun assertUserName(username: String) { + userName.assertHasText(username) + } + + fun assertCourseSelectorDisplayedWithCourse(courseName: String) { + onViewWithId(R.id.studentCourseSpinner).assertDisplayed() + onView(withText(courseName) + withAncestor(R.id.studentCourseSpinner)).assertDisplayed() + } + + fun assertAssignmentSelectorDisplayedWithAssignment(assignmentName: String) { + onViewWithId(R.id.assignmentSpinner).assertDisplayed() + onView(withText(assignmentName) + withAncestor(R.id.assignmentSpinner)).assertDisplayed() + } + + fun selectAssignment(assignmentName: String) { + onViewWithId(R.id.assignmentSpinner).click() + onData(anything()).inRoot(isDialog()).atPosition(1) + } + + fun selectSubmission() { + assignmentCheckbox.click() + } + + fun pressNext() { + nextButton.click() + } +} \ No newline at end of file diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/AssignmentDetailsRenderTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/AssignmentDetailsRenderTest.kt index 4b468fe200..9d66d7546f 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/AssignmentDetailsRenderTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/AssignmentDetailsRenderTest.kt @@ -76,7 +76,8 @@ class AssignmentDetailsRenderTest : StudentRenderTest() { fun displaysTitleDataNotSubmitted() { val assignment = Assignment( name = "Test Assignment", - pointsPossible = 35.0 + pointsPossible = 35.0, + submissionTypesRaw = listOf("online_text_entry") ) val model = baseModel.copy(assignmentResult = DataResult.Success(assignment)) loadPageWithModel(model) 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/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..1583572955 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() @@ -110,6 +112,8 @@ abstract class StudentTest : CanvasTest() { val gradesPage = GradesPage() val resourcesPage = ResourcesPage() val importantDatesPage = ImportantDatesPage() + val shareExtensionTargetPage = ShareExtensionTargetPage() + val shareExtensionStatusPage = ShareExtensionStatusPage() // A no-op interaction to afford us an easy, harmless way to get a11y checking to trigger. fun meaninglessSwipe() { diff --git a/apps/student/src/main/AndroidManifest.xml b/apps/student/src/main/AndroidManifest.xml index 0a83d35912..888e536236 100644 --- a/apps/student/src/main/AndroidManifest.xml +++ b/apps/student/src/main/AndroidManifest.xml @@ -81,7 +81,8 @@ android:clearTaskOnLaunch="true" android:launchMode="singleTop" android:configChanges="keyboardHidden|orientation|screenSize" - android:theme="@style/LoginFlowTheme.Splash_Student"> + android:theme="@style/LoginFlowTheme.Splash_Student" + android:exported="true"> @@ -106,12 +107,6 @@ android:windowSoftInputMode="adjustResize" android:label="@string/canvas" android:theme="@style/CanvasMaterialTheme_Default"> - - - - - - + android:configChanges="keyboardHidden|orientation" + android:exported="true"> + android:theme="@style/CanvasMaterialTheme_Default.Translucent" + android:exported="true"> + @@ -238,7 +235,8 @@ + android:name=".activity.WidgetSetupActivity" + android:exported="false"> @@ -248,7 +246,8 @@ android:name=".activity.BookmarkShortcutActivity" android:icon="@drawable/ic_bookmark_shortcut" android:label="@string/student_app_name" - android:theme="@style/CanvasMaterialTheme_DefaultNoTransparency"> + android:theme="@style/CanvasMaterialTheme_DefaultNoTransparency" + android:exported="true"> @@ -259,7 +258,8 @@ + android:launchMode="singleTask" + android:exported="true"> @@ -280,7 +280,8 @@ android:name=".util.FileDownloadJobIntentService" android:permission="android.permission.BIND_JOB_SERVICE" /> - + @@ -301,7 +302,8 @@ + android:label="@string/todoWidgetTitleLong" + android:exported="false"> @@ -321,7 +323,8 @@ + android:label="@string/gradesWidgetTitleLong" + android:exported="false"> @@ -341,7 +344,8 @@ + android:label="@string/notificationWidgetTitleLong" + android:exported="false"> diff --git a/apps/student/src/main/java/com/instructure/student/activity/BaseRouterActivity.kt b/apps/student/src/main/java/com/instructure/student/activity/BaseRouterActivity.kt index 37454de549..0bd5ea46d0 100644 --- a/apps/student/src/main/java/com/instructure/student/activity/BaseRouterActivity.kt +++ b/apps/student/src/main/java/com/instructure/student/activity/BaseRouterActivity.kt @@ -205,7 +205,7 @@ abstract class BaseRouterActivity : CallbackActivity(), FullScreenInteractions { } fun openMedia(canvasContext: CanvasContext?, url: String) { - openMediaBundle = OpenMediaAsyncTaskLoader.createBundle(canvasContext, url, null) + openMediaBundle = OpenMediaAsyncTaskLoader.createBundle(url, null, canvasContext) LoaderUtils.restartLoaderWithBundle>( LoaderManager.getInstance(this), openMediaBundle, loaderCallbacks, R.id.openMediaLoaderID) } 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/student/src/main/java/com/instructure/student/activity/CandroidPSPDFActivity.kt b/apps/student/src/main/java/com/instructure/student/activity/CandroidPSPDFActivity.kt index cc5a2b290f..8d5f1f8e66 100644 --- a/apps/student/src/main/java/com/instructure/student/activity/CandroidPSPDFActivity.kt +++ b/apps/student/src/main/java/com/instructure/student/activity/CandroidPSPDFActivity.kt @@ -25,16 +25,16 @@ import android.util.LayoutDirection import android.util.TypedValue import android.view.Menu import android.view.MenuItem -import android.view.View import androidx.annotation.ColorInt import androidx.core.text.TextUtilsCompat import com.instructure.annotations.CanvasPdfMenuGrouping import com.instructure.pandautils.analytics.SCREEN_VIEW_PSPDFKIT import com.instructure.pandautils.analytics.ScreenView +import com.instructure.pandautils.features.shareextension.ShareFileSubmissionTarget import com.instructure.pandautils.utils.Const -import com.instructure.pandautils.utils.ThemePrefs import com.instructure.pandautils.utils.ViewStyler import com.instructure.student.R +import com.instructure.student.features.shareextension.StudentShareExtensionActivity import com.pspdfkit.document.processor.PdfProcessorTask import com.pspdfkit.document.sharing.DefaultDocumentSharingController import com.pspdfkit.document.sharing.DocumentSharingIntentHelper @@ -132,7 +132,7 @@ class CandroidPSPDFActivity : PdfActivity(), ToolbarCoordinatorLayout.OnContextu ) : DefaultDocumentSharingController(mContext) { override fun onDocumentPrepared(shareUri: Uri) { - val intent = Intent(mContext, ShareFileUploadActivity::class.java) + val intent = Intent(mContext, StudentShareExtensionActivity::class.java) intent.type = DocumentSharingIntentHelper.MIME_TYPE_PDF intent.putExtra(Intent.EXTRA_STREAM, shareUri) intent.putExtra(Const.SUBMISSION_TARGET, submissionTarget) 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..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 @@ -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 @@ -68,15 +65,17 @@ import com.instructure.interactions.router.RouterParams import com.instructure.loginapi.login.dialog.ErrorReportDialog 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 import com.instructure.pandautils.typeface.TypefaceBehavior import com.instructure.pandautils.update.UpdateManager import com.instructure.pandautils.utils.* +import com.instructure.pandautils.utils.RequestCodes.CAMERA_PIC_REQUEST +import com.instructure.pandautils.utils.RequestCodes.PICK_FILE_FROM_DEVICE +import com.instructure.pandautils.utils.RequestCodes.PICK_IMAGE_GALLERY import com.instructure.student.R import com.instructure.student.dialog.BookmarkCreationDialog import com.instructure.student.events.* @@ -105,7 +104,6 @@ import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.ThreadMode import java.util.* import javax.inject.Inject -import kotlin.collections.ArrayList private const val BOTTOM_NAV_SCREEN = "bottomNavScreen" private const val BOTTOM_SCREENS_BUNDLE_KEY = "bottomScreens" @@ -342,9 +340,9 @@ class NavigationActivity : BaseRouterActivity(), Navigation, MasqueradingDialog. 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 || + if (requestCode == CAMERA_PIC_REQUEST || + requestCode == PICK_FILE_FROM_DEVICE || + requestCode == PICK_IMAGE_GALLERY || PickerSubmissionUploadEffectHandler.isPickerRequest(requestCode) || AssignmentDetailsFragment.isFileRequest(requestCode) || SubmissionDetailsEmptyContentFragment.isFileRequest(requestCode) @@ -751,8 +749,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/activity/ShareFileUploadActivity.kt b/apps/student/src/main/java/com/instructure/student/activity/ShareFileUploadActivity.kt deleted file mode 100644 index 5872617ad1..0000000000 --- a/apps/student/src/main/java/com/instructure/student/activity/ShareFileUploadActivity.kt +++ /dev/null @@ -1,253 +0,0 @@ -/* - * Copyright (C) 2016 - present Instructure, Inc. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, version 3 of the License. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - * - */ - -package com.instructure.student.activity - -import android.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/di/ShareExtensionModule.kt b/apps/student/src/main/java/com/instructure/student/di/ShareExtensionModule.kt new file mode 100644 index 0000000000..237905ecbb --- /dev/null +++ b/apps/student/src/main/java/com/instructure/student/di/ShareExtensionModule.kt @@ -0,0 +1,20 @@ +package com.instructure.student.di + +import com.instructure.pandautils.features.shareextension.ShareExtensionRouter +import com.instructure.student.features.shareextension.StudentShareExtensionRouter +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +class ShareExtensionModule { + + @Provides + @Singleton + fun provideShareExtensionRouter(): ShareExtensionRouter { + return StudentShareExtensionRouter() + } +} \ No newline at end of file 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/features/shareextension/StudentShareExtensionRouter.kt b/apps/student/src/main/java/com/instructure/student/features/shareextension/StudentShareExtensionRouter.kt new file mode 100644 index 0000000000..2b3c1e3c9e --- /dev/null +++ b/apps/student/src/main/java/com/instructure/student/features/shareextension/StudentShareExtensionRouter.kt @@ -0,0 +1,15 @@ +package com.instructure.student.features.shareextension + +import android.content.Context +import android.content.Intent +import com.instructure.pandautils.features.shareextension.ShareExtensionRouter +import com.instructure.pandautils.features.shareextension.WORKER_ID +import java.util.* + +class StudentShareExtensionRouter : ShareExtensionRouter { + override fun routeToProgressScreen(context: Context, workerId: UUID): Intent { + val intent = Intent(context, StudentShareExtensionActivity::class.java) + intent.putExtra(WORKER_ID, workerId) + return intent + } +} \ No newline at end of file diff --git a/apps/student/src/main/java/com/instructure/student/fragment/ApplicationSettingsFragment.kt b/apps/student/src/main/java/com/instructure/student/fragment/ApplicationSettingsFragment.kt 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/fragment/CalendarEventFragment.kt b/apps/student/src/main/java/com/instructure/student/fragment/CalendarEventFragment.kt index 78548c9873..fe139b7280 100644 --- a/apps/student/src/main/java/com/instructure/student/fragment/CalendarEventFragment.kt +++ b/apps/student/src/main/java/com/instructure/student/fragment/CalendarEventFragment.kt @@ -237,7 +237,7 @@ class CalendarEventFragment : ParentFragment() { private fun loadCalendarHtml(html: String, contentDescription: String?) { calendarEventWebView.setVisible() calendarEventWebView.setBackgroundColor(ContextCompat.getColor(requireContext(), R.color.backgroundLightest)) - calendarEventWebView.loadHtml(html, contentDescription) + calendarEventWebView.loadHtml(html, contentDescription, baseUrl = scheduleItem?.htmlUrl) } private fun setUpCallback() { diff --git a/apps/student/src/main/java/com/instructure/student/fragment/DiscussionDetailsFragment.kt b/apps/student/src/main/java/com/instructure/student/fragment/DiscussionDetailsFragment.kt index 43529465ca..fdc6ba1ea4 100644 --- a/apps/student/src/main/java/com/instructure/student/fragment/DiscussionDetailsFragment.kt +++ b/apps/student/src/main/java/com/instructure/student/fragment/DiscussionDetailsFragment.kt @@ -544,7 +544,7 @@ class DiscussionDetailsFragment : ParentFragment(), Bookmarkable { //region Loading private fun loadHTMLTopic(html: String, contentDescription: String?) { setupHeaderWebView() - discussionTopicHeaderWebView.loadHtml(html, contentDescription) + discussionTopicHeaderWebView.loadHtml(html, contentDescription, baseUrl = discussionTopicHeader.htmlUrl) } private fun loadHTMLReplies(html: String, contentDescription: String? = null) { 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..e8589d77f1 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 @@ -29,6 +29,8 @@ import android.widget.Toast import androidx.appcompat.app.AlertDialog import androidx.appcompat.widget.PopupMenu import androidx.fragment.app.DialogFragment +import androidx.lifecycle.LiveData +import androidx.work.WorkInfo import com.instructure.canvasapi2.managers.FileFolderManager import com.instructure.canvasapi2.models.* import com.instructure.canvasapi2.utils.ApiPrefs @@ -44,7 +46,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 @@ -58,6 +60,7 @@ import kotlinx.android.synthetic.main.fragment_file_list.* import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.ThreadMode +import java.util.* @ScreenView(SCREEN_VIEW_FILE_LIST) @PageView @@ -406,8 +409,19 @@ 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, workerLiveDataCallback = this::workInfoLiveDataCallback).show(childFragmentManager, FileUploadDialogFragment.TAG) + } + } + + private fun workInfoLiveDataCallback(uuid: UUID, workInfoLiveData: LiveData) { + workInfoLiveData.observe(viewLifecycleOwner) { + if (it.state == WorkInfo.State.SUCCEEDED) { + recyclerAdapter?.refresh() + folder?.let { + StudentPrefs.staleFolderIds = StudentPrefs.staleFolderIds + it.id + } + } } } 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..efa7092d0e 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 @@ -24,6 +24,8 @@ import android.view.ViewTreeObserver import android.widget.AdapterView import android.widget.Toast import androidx.annotation.StringRes +import androidx.lifecycle.LiveData +import androidx.work.WorkInfo import com.instructure.canvasapi2.managers.CourseManager import com.instructure.canvasapi2.managers.GroupManager import com.instructure.canvasapi2.managers.InboxManager @@ -35,8 +37,9 @@ 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.services.FileUploadService +import com.instructure.pandautils.features.file.upload.FileUploadDialogFragment +import com.instructure.pandautils.features.file.upload.worker.FileUploadWorker +import com.instructure.pandautils.utils.fromJson import com.instructure.pandautils.utils.* import com.instructure.student.R import com.instructure.student.adapter.CanvasContextSpinnerAdapter @@ -51,7 +54,8 @@ 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 +import java.util.* +import kotlin.collections.ArrayList @ScreenView(SCREEN_VIEW_INBOX_COMPOSE) class InboxComposeMessageFragment : ParentFragment() { @@ -279,8 +283,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, workerLiveDataCallback = this::fileUploadLiveDataCallback).show(childFragmentManager, FileUploadDialogFragment.TAG) } else -> return@setOnMenuItemClickListener false } @@ -438,16 +442,15 @@ class InboxComposeMessageFragment : ParentFragment() { } } - @Suppress("unused") - @Subscribe(threadMode = ThreadMode.MAIN) - fun onFileUploadedEvent(event: FileUploadEvent) { - event.get { - event.remove() - if(it.intent?.action == FileUploadService.ALL_UPLOADS_COMPLETED) { - it.attachments.forEach { - attachments += it - } - refreshAttachments() + private fun fileUploadLiveDataCallback(uuid: UUID, workInfoLiveData: LiveData) { + workInfoLiveData.observe(viewLifecycleOwner) { + if (it.state == WorkInfo.State.SUCCEEDED) { + it.outputData.getStringArray(FileUploadWorker.RESULT_ATTACHMENTS) + ?.map { it.fromJson() } + ?.let { + this.attachments.addAll(it) + refreshAttachments() + } ?: toast(R.string.errorUploadingFile) } } } diff --git a/apps/student/src/main/java/com/instructure/student/fragment/PageDetailsFragment.kt b/apps/student/src/main/java/com/instructure/student/fragment/PageDetailsFragment.kt index a5633d7aaa..f590d6eb01 100644 --- a/apps/student/src/main/java/com/instructure/student/fragment/PageDetailsFragment.kt +++ b/apps/student/src/main/java/com/instructure/student/fragment/PageDetailsFragment.kt @@ -218,8 +218,8 @@ class PageDetailsFragment : InternalWebviewFragment(), Bookmarkable { checkCanEdit() } - private fun loadPageHtml(html: String, contentDescrption: String?) { - canvasWebView.loadHtml(html, contentDescrption) + private fun loadPageHtml(html: String, contentDescription: String?) { + canvasWebView.loadHtml(html, contentDescription, baseUrl = page.htmlUrl) } /** diff --git a/apps/student/src/main/java/com/instructure/student/fragment/ParentFragment.kt b/apps/student/src/main/java/com/instructure/student/fragment/ParentFragment.kt index 321c79d4d3..ab6cd62326 100644 --- a/apps/student/src/main/java/com/instructure/student/fragment/ParentFragment.kt +++ b/apps/student/src/main/java/com/instructure/student/fragment/ParentFragment.kt @@ -508,7 +508,7 @@ abstract class ParentFragment : DialogFragment(), FragmentInteractions { fun openMedia(canvasContext: CanvasContext, url: String, filename: String?) { val owner = activity ?: return onMainThread { - openMediaBundle = OpenMediaAsyncTaskLoader.createBundle(canvasContext, url, filename) + openMediaBundle = OpenMediaAsyncTaskLoader.createBundle(url, filename, canvasContext) LoaderUtils.restartLoaderWithBundle>(LoaderManager.getInstance(owner), openMediaBundle, loaderCallbacks, R.id.openMediaLoaderID) } } diff --git a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/AssignmentDetailsPresenter.kt b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/AssignmentDetailsPresenter.kt index fef59aa445..4635932219 100644 --- a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/AssignmentDetailsPresenter.kt +++ b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/AssignmentDetailsPresenter.kt @@ -248,7 +248,8 @@ object AssignmentDetailsPresenter : Presenter loadDescriptionHtml(html, contentDescription, state.htmlUrl) }, { + val args = LtiLaunchFragment.makeLTIBundle( + URLDecoder.decode(it, "utf-8"), context.getString(R.string.utils_externalToolTitle), true + ) + RouteMatcher.route(context, Route(LtiLaunchFragment::class.java, canvasContext, args)) + }, state.assignmentName + ) } if(state.visibilities.quizDetails) renderQuizDetails(state.quizDescriptionViewState!!) if(state.visibilities.discussionTopicHeader) renderDiscussionTopicHeader(state.discussionHeaderViewState!!) @@ -230,8 +233,8 @@ class AssignmentDetailsView( submissionAndRubricLabel.text = context.getText(submissionAndRubricText) } - private fun loadDescriptionHtml(html: String, contentDescrption: String?) { - descriptionWebView.loadHtml(html, contentDescrption) + private fun loadDescriptionHtml(html: String, contentDescription: String?, baseUrl: String?) { + descriptionWebView.loadHtml(html, contentDescription, baseUrl = baseUrl) } private fun renderQuizDetails(quizDescriptionViewState: QuizDescriptionViewState) { diff --git a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/ui/AssignmentDetailsViewState.kt b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/ui/AssignmentDetailsViewState.kt index 4b3a2aed76..14dd89aad5 100644 --- a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/ui/AssignmentDetailsViewState.kt +++ b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/ui/AssignmentDetailsViewState.kt @@ -46,7 +46,8 @@ sealed class AssignmentDetailsViewState(val visibilities: AssignmentDetailsVisib val isExternalToolSubmission: Boolean = false, val quizDescriptionViewState: QuizDescriptionViewState? = null, val discussionHeaderViewState: DiscussionHeaderViewState? = null, - val showSubmissionsAndRubric: Boolean = true + val showSubmissionsAndRubric: Boolean = true, + val htmlUrl: String? = null ) : AssignmentDetailsViewState(assignmentDetailsVisibilities) } diff --git a/apps/student/src/main/java/com/instructure/student/mobius/common/ui/SubmissionService.kt b/apps/student/src/main/java/com/instructure/student/mobius/common/ui/SubmissionService.kt index 3ef22d2e26..959dd19af1 100644 --- a/apps/student/src/main/java/com/instructure/student/mobius/common/ui/SubmissionService.kt +++ b/apps/student/src/main/java/com/instructure/student/mobius/common/ui/SubmissionService.kt @@ -550,7 +550,7 @@ class SubmissionService : IntentService(SubmissionService::class.java.simpleName putExtra(PushNotification.HTML_URL, path) } - return PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT) + return PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE) } private fun showErrorNotification( 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/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/java/com/instructure/student/util/BaseAppManager.kt b/apps/student/src/main/java/com/instructure/student/util/BaseAppManager.kt index 17cb0fbe34..07d301fec1 100644 --- a/apps/student/src/main/java/com/instructure/student/util/BaseAppManager.kt +++ b/apps/student/src/main/java/com/instructure/student/util/BaseAppManager.kt @@ -20,9 +20,6 @@ import android.os.Build import android.webkit.WebView import androidx.appcompat.app.AppCompatDelegate import androidx.core.content.ContextCompat -import com.google.android.gms.analytics.GoogleAnalytics -import com.google.android.gms.analytics.HitBuilders -import com.google.android.gms.analytics.Tracker import com.google.android.play.core.missingsplits.MissingSplitsManagerFactory import com.google.firebase.crashlytics.FirebaseCrashlytics import com.instructure.canvasapi2.utils.* @@ -53,12 +50,6 @@ import javax.inject.Inject open class BaseAppManager : com.instructure.canvasapi2.AppManager(), AnalyticsEventHandling { - // To enable debug logging use: adb shell setprop log.tag.GAv4 DEBUG - private val defaultTracker: Tracker by lazy { - val analytics = GoogleAnalytics.getInstance(this) - analytics.newTracker(R.xml.analytics) - } - override fun onCreate() { if (MissingSplitsManagerFactory.create(this).disableAppIfMissingRequiredSplits()) { // Skip app initialization. @@ -118,94 +109,31 @@ open class BaseAppManager : com.instructure.canvasapi2.AppManager(), AnalyticsEv override fun onCanvasTokenRefreshed() = FlutterComm.sendUpdatedLogin() override fun trackButtonPressed(buttonName: String?, buttonValue: Long?) { - if (buttonName == null) return - - if (buttonValue == null) { - defaultTracker.send( - HitBuilders.EventBuilder() - .setCategory("UI Actions") - .setAction("Button Pressed") - .setLabel(buttonName) - .build() - ) - } else { - defaultTracker.send( - HitBuilders.EventBuilder() - .setCategory("UI Actions") - .setAction("Button Pressed") - .setLabel(buttonName) - .setValue(buttonValue) - .build() - ) - } + } override fun trackScreen(screenName: String?) { - if (screenName == null) return - val tracker = defaultTracker - tracker.setScreenName(screenName) - tracker.send(HitBuilders.ScreenViewBuilder().build()) } override fun trackEnrollment(enrollmentType: String?) { - if (enrollmentType == null) return - defaultTracker.send( - HitBuilders.AppViewBuilder() - .setCustomDimension(1, enrollmentType) - .build() - ) } override fun trackDomain(domain: String?) { - if (domain == null) return - defaultTracker.send( - HitBuilders.AppViewBuilder() - .setCustomDimension(2, domain) - .build() - ) } override fun trackEvent(category: String?, action: String?, label: String?, value: Long) { - if (category == null || action == null || label == null) return - - val tracker = defaultTracker - tracker.send( - HitBuilders.EventBuilder() - .setCategory(category) - .setAction(action) - .setLabel(label) - .setValue(value) - .build() - ) + } override fun trackUIEvent(action: String?, label: String?, value: Long) { - if (action == null || label == null) return - - defaultTracker.send( - HitBuilders.EventBuilder() - .setAction(action) - .setLabel(label) - .setValue(value) - .build() - ) + } override fun trackTiming(category: String?, name: String?, label: String?, duration: Long) { - if (category == null || name == null || label == null) return - - val tracker = defaultTracker - tracker.send( - HitBuilders.TimingBuilder() - .setCategory(category) - .setLabel(label) - .setVariable(name) - .setValue(duration) - .build() - ) + } private fun initPSPDFKit() { diff --git a/apps/student/src/main/java/com/instructure/student/util/FileDownloadJobIntentService.kt b/apps/student/src/main/java/com/instructure/student/util/FileDownloadJobIntentService.kt index 66cdf77db8..6570caec0d 100644 --- a/apps/student/src/main/java/com/instructure/student/util/FileDownloadJobIntentService.kt +++ b/apps/student/src/main/java/com/instructure/student/util/FileDownloadJobIntentService.kt @@ -23,7 +23,6 @@ import android.app.NotificationManager import android.app.PendingIntent import android.content.Context import android.content.Intent -import android.os.Build import android.os.Bundle import android.os.Environment import android.util.Log @@ -32,7 +31,6 @@ import androidx.core.app.NotificationCompat import com.google.firebase.crashlytics.FirebaseCrashlytics import com.instructure.canvasapi2.models.Attachment import com.instructure.canvasapi2.models.FileFolder -import com.instructure.pandautils.services.FileUploadService.Companion.CHANNEL_ID import com.instructure.student.R import okhttp3.OkHttpClient import okhttp3.Request @@ -55,7 +53,7 @@ class FileDownloadJobIntentService : JobIntentService() { // Tell Android where to send the user if they click on the notification val viewDownloadIntent = Intent(DownloadManager.ACTION_VIEW_DOWNLOADS) - val pendingIntent = PendingIntent.getActivity(this, 0, viewDownloadIntent, 0) + val pendingIntent = PendingIntent.getActivity(this, 0, viewDownloadIntent, PendingIntent.FLAG_IMMUTABLE) // Setup a notification val notification = NotificationCompat.Builder(this, CHANNEL_ID) @@ -191,6 +189,8 @@ class FileDownloadJobIntentService : JobIntentService() { val NOTIFICATION_ID = "notificationid" val USE_HTTPURLCONNECTION = "usehttpurlconnection" + const val CHANNEL_ID = "uploadChannel" + // Notification ID is passed into the extras of the job, make sure to use that for any notification updates inside the job var notificationId = 1 get() = ++field 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/ShortcutUtils.kt b/apps/student/src/main/java/com/instructure/student/util/ShortcutUtils.kt index ed7cd43934..b7fafb2893 100644 --- a/apps/student/src/main/java/com/instructure/student/util/ShortcutUtils.kt +++ b/apps/student/src/main/java/com/instructure/student/util/ShortcutUtils.kt @@ -62,7 +62,7 @@ object ShortcutUtils { .build() val successIntent = shortcutManager.createShortcutResultIntent(pinShortcutInfo) - val pendingIntent = PendingIntent.getBroadcast(context, 0, successIntent, 0) + val pendingIntent = PendingIntent.getBroadcast(context, 0, successIntent, PendingIntent.FLAG_IMMUTABLE) shortcutManager.requestPinShortcut(pinShortcutInfo, pendingIntent.intentSender) return true } 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/java/com/instructure/student/widget/CanvasWidgetProvider.kt b/apps/student/src/main/java/com/instructure/student/widget/CanvasWidgetProvider.kt index c76f038d01..a03c429ca7 100644 --- a/apps/student/src/main/java/com/instructure/student/widget/CanvasWidgetProvider.kt +++ b/apps/student/src/main/java/com/instructure/student/widget/CanvasWidgetProvider.kt @@ -92,7 +92,7 @@ abstract class CanvasWidgetProvider : AppWidgetProvider() { // Tapping on the logo or title should open the app val launchMain = Intent(context, LoginActivity::class.java) - val pendingMainIntent = PendingIntent.getActivity(context, 0, launchMain, 0) + val pendingMainIntent = PendingIntent.getActivity(context, 0, launchMain, PendingIntent.FLAG_IMMUTABLE) remoteViews.setOnClickPendingIntent(R.id.widget_title, pendingMainIntent) remoteViews.setOnClickPendingIntent(R.id.widget_logo, pendingMainIntent) diff --git a/apps/student/src/main/java/com/instructure/student/widget/CanvasWidgetRowFactory.kt b/apps/student/src/main/java/com/instructure/student/widget/CanvasWidgetRowFactory.kt index a4c1f00d7b..0fbdaea5b9 100644 --- a/apps/student/src/main/java/com/instructure/student/widget/CanvasWidgetRowFactory.kt +++ b/apps/student/src/main/java/com/instructure/student/widget/CanvasWidgetRowFactory.kt @@ -100,7 +100,7 @@ abstract class CanvasWidgetRowFactory : RemoteViewsService.RemoteViewsFactory //create log in intent val intent = LoginActivity.createIntent(ContextKeeper.appContext) val pendingIntent = PendingIntent.getActivity( - ContextKeeper.appContext, CanvasWidgetProvider.cycleBit++, intent, PendingIntent.FLAG_UPDATE_CURRENT) + ContextKeeper.appContext, CanvasWidgetProvider.cycleBit++, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE) row.setOnClickPendingIntent(R.id.widget_root, pendingIntent) true } else { diff --git a/apps/student/src/main/java/com/instructure/student/widget/GradesWidgetProvider.kt b/apps/student/src/main/java/com/instructure/student/widget/GradesWidgetProvider.kt index 1456b160e9..031e2bf43c 100644 --- a/apps/student/src/main/java/com/instructure/student/widget/GradesWidgetProvider.kt +++ b/apps/student/src/main/java/com/instructure/student/widget/GradesWidgetProvider.kt @@ -44,9 +44,22 @@ class GradesWidgetProvider : CanvasWidgetProvider() { remoteViews.setTextColor(R.id.widget_title, textColor) val listViewItemIntent = Intent(context, InterwebsToApplication::class.java) - remoteViews.setPendingIntentTemplate(R.id.contentList, PendingIntent.getActivity(context, CanvasWidgetProvider.cycleBit++, listViewItemIntent, PendingIntent.FLAG_UPDATE_CURRENT)) + remoteViews.setPendingIntentTemplate( + R.id.contentList, + PendingIntent.getActivity( + context, + CanvasWidgetProvider.cycleBit++, + listViewItemIntent, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE + ) + ) - val pendingRefreshIntent = PendingIntent.getBroadcast(context, refreshIntentID, getRefreshIntent(appWidgetManager), PendingIntent.FLAG_UPDATE_CURRENT) + val pendingRefreshIntent = PendingIntent.getBroadcast( + context, + refreshIntentID, + getRefreshIntent(appWidgetManager), + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE + ) remoteViews.setOnClickPendingIntent(R.id.widget_refresh, pendingRefreshIntent) } diff --git a/apps/student/src/main/java/com/instructure/student/widget/NotificationViewWidgetService.kt b/apps/student/src/main/java/com/instructure/student/widget/NotificationViewWidgetService.kt index a529985323..a0c8107b21 100644 --- a/apps/student/src/main/java/com/instructure/student/widget/NotificationViewWidgetService.kt +++ b/apps/student/src/main/java/com/instructure/student/widget/NotificationViewWidgetService.kt @@ -141,7 +141,7 @@ class NotificationViewWidgetService : BaseRemoteViewsService(), Serializable { val courses = CourseManager.getCoursesSynchronous(true) .filter { it.isFavorite && !it.accessRestrictedByDate && !it.isInvited() } val groups = GroupManager.getFavoriteGroupsSynchronous(false) - val userStream = StreamManager.getUserStreamSynchronous(25, false).toMutableList() + val userStream = StreamManager.getUserStreamSynchronous(25, true).toMutableList() userStream.sort() userStream.reverse() diff --git a/apps/student/src/main/java/com/instructure/student/widget/NotificationWidgetProvider.kt b/apps/student/src/main/java/com/instructure/student/widget/NotificationWidgetProvider.kt index c1e6a45e17..bcb9e67a45 100644 --- a/apps/student/src/main/java/com/instructure/student/widget/NotificationWidgetProvider.kt +++ b/apps/student/src/main/java/com/instructure/student/widget/NotificationWidgetProvider.kt @@ -45,9 +45,22 @@ class NotificationWidgetProvider : CanvasWidgetProvider() { remoteViews.setTextColor(R.id.widget_title, textColor) val listViewItemIntent = Intent(context, NotificationWidgetRouter::class.java) - remoteViews.setPendingIntentTemplate(R.id.contentList, PendingIntent.getActivity(context, CanvasWidgetProvider.cycleBit++, listViewItemIntent, PendingIntent.FLAG_UPDATE_CURRENT)) + remoteViews.setPendingIntentTemplate( + R.id.contentList, + PendingIntent.getActivity( + context, + CanvasWidgetProvider.cycleBit++, + listViewItemIntent, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE + ) + ) - val pendingRefreshIntent = PendingIntent.getBroadcast(context, refreshIntentID, getRefreshIntent(appWidgetManager), PendingIntent.FLAG_UPDATE_CURRENT) + val pendingRefreshIntent = PendingIntent.getBroadcast( + context, + refreshIntentID, + getRefreshIntent(appWidgetManager), + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE + ) remoteViews.setOnClickPendingIntent(R.id.widget_refresh, pendingRefreshIntent) } diff --git a/apps/student/src/main/java/com/instructure/student/widget/TodoWidgetProvider.kt b/apps/student/src/main/java/com/instructure/student/widget/TodoWidgetProvider.kt index 841cb89370..2163f0883f 100644 --- a/apps/student/src/main/java/com/instructure/student/widget/TodoWidgetProvider.kt +++ b/apps/student/src/main/java/com/instructure/student/widget/TodoWidgetProvider.kt @@ -45,9 +45,22 @@ class TodoWidgetProvider : CanvasWidgetProvider() { remoteViews.setTextColor(R.id.widget_title, textColor) val listViewItemIntent = Intent(context, InterwebsToApplication::class.java) - remoteViews.setPendingIntentTemplate(R.id.contentList, PendingIntent.getActivity(context, CanvasWidgetProvider.cycleBit++, listViewItemIntent, PendingIntent.FLAG_UPDATE_CURRENT)) + remoteViews.setPendingIntentTemplate( + R.id.contentList, + PendingIntent.getActivity( + context, + CanvasWidgetProvider.cycleBit++, + listViewItemIntent, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE + ) + ) - val pendingRefreshIntent = PendingIntent.getBroadcast(context, refreshIntentID, getRefreshIntent(appWidgetManager), PendingIntent.FLAG_UPDATE_CURRENT) + val pendingRefreshIntent = PendingIntent.getBroadcast( + context, + refreshIntentID, + getRefreshIntent(appWidgetManager), + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE + ) remoteViews.setOnClickPendingIntent(R.id.widget_refresh, pendingRefreshIntent) } diff --git a/apps/student/src/main/res/layout-sw720dp/fragment_elementary_course.xml b/apps/student/src/main/res/layout-sw720dp/fragment_elementary_course.xml index 997415dc81..c1cd5d7bfe 100644 --- a/apps/student/src/main/res/layout-sw720dp/fragment_elementary_course.xml +++ b/apps/student/src/main/res/layout-sw720dp/fragment_elementary_course.xml @@ -103,8 +103,7 @@ android:background="@{ColorUtils.parseColor(course.courseColor)}" android:importantForAccessibility="no" android:scaleType="centerCrop" - app:imageUrl="@{course.imageUrl}" - app:overlayColor="@{ColorUtils.parseColor(course.courseColor)}" + app:imageUrl="@{course.bannerImageUrl != null ? course.bannerImageUrl : course.imageUrl}" tools:background="@color/backgroundWarning" /> + + + android:orientation="horizontal" + android:id="@+id/statusDetails"> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 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 @@