diff --git a/android-vault b/android-vault
index 39c3a5110a..afe23ef985 160000
--- a/android-vault
+++ b/android-vault
@@ -1 +1 @@
-Subproject commit 39c3a5110ab8fafca721661f01091e995677e920
+Subproject commit afe23ef98500158907edba149d751cf5fe0ad376
diff --git a/apps/build.gradle b/apps/build.gradle
index 78a2cc62a7..82f0c84012 100644
--- a/apps/build.gradle
+++ b/apps/build.gradle
@@ -23,9 +23,7 @@ buildscript {
google()
mavenCentral()
maven { url "https://www.jitpack.io" }
- jcenter()
maven { url "https://plugins.gradle.org/m2/" }
- maven { url "https://dl.bintray.com/instructure/maven" }
}
dependencies {
@@ -34,9 +32,9 @@ buildscript {
classpath Plugins.GOOGLE_SERVICES
classpath Plugins.KOTLIN
classpath Plugins.FIREBASE_CRASHLYTICS
- classpath Plugins.BUILD_SCAN
if (project.coverageEnabled) { classpath Plugins.JACOCO_ANDROID }
classpath Plugins.SQLDELIGHT
+ classpath Plugins.HILT
}
}
@@ -44,7 +42,7 @@ allprojects {
repositories {
google()
mavenCentral()
- maven { url "https://www.jitpack.io" }
+ maven { url 'https://jitpack.io' }
maven {
credentials {
username pspdfMavenUser
@@ -53,8 +51,6 @@ allprojects {
url 'https://customers.pspdfkit.com/maven/'
}
maven { url "https://maven.google.com/" }
- jcenter()
- maven { url "https://dl.bintray.com/instructure/maven" }
}
}
diff --git a/apps/flutter_parent/android/build.gradle b/apps/flutter_parent/android/build.gradle
index 7599f7ecbe..485b50d68e 100644
--- a/apps/flutter_parent/android/build.gradle
+++ b/apps/flutter_parent/android/build.gradle
@@ -2,7 +2,7 @@ buildscript {
ext.kotlin_version = '1.2.71'
repositories {
google()
- jcenter()
+ mavenCentral()
}
dependencies {
@@ -16,7 +16,7 @@ buildscript {
allprojects {
repositories {
google()
- jcenter()
+ mavenCentral()
}
}
diff --git a/apps/flutter_parent/assets/svg/ic_change_user.svg b/apps/flutter_parent/assets/svg/ic_change_user.svg
new file mode 100644
index 0000000000..64741a7840
--- /dev/null
+++ b/apps/flutter_parent/assets/svg/ic_change_user.svg
@@ -0,0 +1,3 @@
+
diff --git a/apps/flutter_parent/assets/svg/ic_help.svg b/apps/flutter_parent/assets/svg/ic_help.svg
new file mode 100644
index 0000000000..d742a38403
--- /dev/null
+++ b/apps/flutter_parent/assets/svg/ic_help.svg
@@ -0,0 +1,3 @@
+
diff --git a/apps/flutter_parent/assets/svg/ic_inbox.svg b/apps/flutter_parent/assets/svg/ic_inbox.svg
new file mode 100644
index 0000000000..1f3f00aaec
--- /dev/null
+++ b/apps/flutter_parent/assets/svg/ic_inbox.svg
@@ -0,0 +1,3 @@
+
diff --git a/apps/flutter_parent/assets/svg/ic_logout.svg b/apps/flutter_parent/assets/svg/ic_logout.svg
new file mode 100644
index 0000000000..ea3aac62a0
--- /dev/null
+++ b/apps/flutter_parent/assets/svg/ic_logout.svg
@@ -0,0 +1,3 @@
+
diff --git a/apps/flutter_parent/assets/svg/ic_manage_student.svg b/apps/flutter_parent/assets/svg/ic_manage_student.svg
new file mode 100644
index 0000000000..fdcd984648
--- /dev/null
+++ b/apps/flutter_parent/assets/svg/ic_manage_student.svg
@@ -0,0 +1,3 @@
+
diff --git a/apps/flutter_parent/assets/svg/ic_settings.svg b/apps/flutter_parent/assets/svg/ic_settings.svg
new file mode 100644
index 0000000000..1b36c1b330
--- /dev/null
+++ b/apps/flutter_parent/assets/svg/ic_settings.svg
@@ -0,0 +1,3 @@
+
diff --git a/apps/flutter_parent/lib/l10n/res/intl_sv.arb b/apps/flutter_parent/lib/l10n/res/intl_sv.arb
index 109fa88cc7..4d3578f905 100644
--- a/apps/flutter_parent/lib/l10n/res/intl_sv.arb
+++ b/apps/flutter_parent/lib/l10n/res/intl_sv.arb
@@ -244,7 +244,7 @@
"type": "text",
"placeholders": {}
},
- "domainSearchHelpBody": "Försök med att söka efter namnet på skolan eller distrikten du vill ansluta till, t.ex. “Allmänna skolan” eller “Skolor i Skåne”. Du kan även ange en Canvas-domän direkt, t.ex. “smith.instructure.com.”\n\nMer information om hur du kan hitta din institutions Canvas-konto finns på {canvasGuides} eller kontakta {canvasSupport} eller din skola för att få hjälp.",
+ "domainSearchHelpBody": "Försök med att söka efter namnet på skolan eller distrikten du vill ansluta till, t.ex. “Allmänna skolan” eller “Skolor i Skåne”. Du kan även ange en Canvas-domän direkt, t.ex. “smith.instructure.com.”\n\nMer information om hur du kan hitta din lärosätes Canvas-konto finns på {canvasGuides} eller kontakta {canvasSupport} eller din skola för att få hjälp.",
"@domainSearchHelpBody": {
"description": "The body text shown in the help dialog on the domain search screen",
"type": "text",
@@ -511,7 +511,7 @@
"howMany": {}
}
},
- "Download": "Ladda ned",
+ "Download": "Ladda ner",
"@Download": {
"description": "Label for the button that will begin downloading a file",
"type": "text",
@@ -965,7 +965,7 @@
"type": "text",
"placeholders": {}
},
- "Institution Announcement": "Institutionsmeddelande",
+ "Institution Announcement": "Meddelande från lärosätet",
"@Institution Announcement": {
"description": "Title for alerts when there is an institution announcement",
"type": "text",
@@ -1081,7 +1081,7 @@
"type": "text",
"placeholders": {}
},
- "Incomplete": "Ej fullständig",
+ "Incomplete": "Inte färdig",
"@Incomplete": {
"description": "Grading status for an assignment marked as incomplete",
"type": "text",
@@ -1154,7 +1154,7 @@
"type": "text",
"placeholders": {}
},
- "Institution Announcements": "Institutionsannonseringar",
+ "Institution Announcements": "Meddelande från lärosätet",
"@Institution Announcements": {
"type": "text",
"placeholders": {}
@@ -2025,7 +2025,7 @@
"type": "text",
"placeholders": {}
},
- "Interactions on this page are limited by your institution.": "Interaktioner på den här sidan har begränsats av din institution.",
+ "Interactions on this page are limited by your institution.": "Interaktioner på den här sidan har begränsats av ditt lärosäte.",
"@Interactions on this page are limited by your institution.": {
"description": "Message describing how the webview has limited access due to an instution setting",
"type": "text",
@@ -2128,7 +2128,7 @@
"type": "text",
"placeholders": {}
},
- "We are unable to display this link, it may belong to an institution you currently aren't logged in to.": "Det går inte att visa den här länken. Den kan tillhöra en institution du för närvarande inte är inloggad på.",
+ "We are unable to display this link, it may belong to an institution you currently aren't logged in to.": "Det går inte att visa den här länken. Den kan tillhöra ett lärosäte du för närvarande inte är inloggad på.",
"@We are unable to display this link, it may belong to an institution you currently aren't logged in to.": {
"description": "Description for error page shown when clicking a link",
"type": "text",
diff --git a/apps/flutter_parent/lib/l10n/res/intl_sv_instk12.arb b/apps/flutter_parent/lib/l10n/res/intl_sv_instk12.arb
index 881ba02e32..2733f68557 100644
--- a/apps/flutter_parent/lib/l10n/res/intl_sv_instk12.arb
+++ b/apps/flutter_parent/lib/l10n/res/intl_sv_instk12.arb
@@ -511,7 +511,7 @@
"howMany": {}
}
},
- "Download": "Ladda ned",
+ "Download": "Ladda ner",
"@Download": {
"description": "Label for the button that will begin downloading a file",
"type": "text",
diff --git a/apps/flutter_parent/lib/l10n/res/intl_th.arb b/apps/flutter_parent/lib/l10n/res/intl_th.arb
new file mode 100644
index 0000000000..e7261f0072
--- /dev/null
+++ b/apps/flutter_parent/lib/l10n/res/intl_th.arb
@@ -0,0 +1,2185 @@
+{
+ "@@last_modified": "2020-09-18T11:03:20.748250",
+ "alertsLabel": "แจ้งเตือน",
+ "@alertsLabel": {
+ "description": "The label for the Alerts tab",
+ "type": "text",
+ "placeholders": {}
+ },
+ "calendarLabel": "ปฏิทิน",
+ "@calendarLabel": {
+ "description": "The label for the Calendar tab",
+ "type": "text",
+ "placeholders": {}
+ },
+ "coursesLabel": "บทเรียน",
+ "@coursesLabel": {
+ "description": "The label for the Courses tab",
+ "type": "text",
+ "placeholders": {}
+ },
+ "No Students": "ไม่มีผู้เรียน",
+ "@No Students": {
+ "description": "Text for when an observer has no students they are observing",
+ "type": "text",
+ "placeholders": {}
+ },
+ "Tap to show student selector": "กดเพื่อแสดงตัวเลือกผู้เรียน",
+ "@Tap to show student selector": {
+ "description": "Semantics label for the area that will show the student selector when tapped",
+ "type": "text",
+ "placeholders": {}
+ },
+ "Tap to pair with a new student": "กดเพื่อเข้าคู่กับผู้เรียนใหม่",
+ "@Tap to pair with a new student": {
+ "description": "Semantics label for the add student button in the student selector",
+ "type": "text",
+ "placeholders": {}
+ },
+ "Tap to select this student": "กดเพื่อเลือกผู้เรียนนี้",
+ "@Tap to select this student": {
+ "description": "Semantics label on individual students in the student switcher",
+ "type": "text",
+ "placeholders": {}
+ },
+ "Manage Students": "จัดการผู้เรียน",
+ "@Manage Students": {
+ "description": "Label text for the Manage Students nav drawer button as well as the title for the Manage Students screen",
+ "type": "text",
+ "placeholders": {}
+ },
+ "Help": "ความช่วยเหลือ",
+ "@Help": {
+ "description": "Label text for the help nav drawer button",
+ "type": "text",
+ "placeholders": {}
+ },
+ "Log Out": "ล็อกเอาท์",
+ "@Log Out": {
+ "description": "Label text for the Log Out nav drawer button",
+ "type": "text",
+ "placeholders": {}
+ },
+ "Switch Users": "สลับผู้ใช้",
+ "@Switch Users": {
+ "description": "Label text for the Switch Users nav drawer button",
+ "type": "text",
+ "placeholders": {}
+ },
+ "appVersion": "v. {version}",
+ "@appVersion": {
+ "description": "App version shown in the navigation drawer",
+ "type": "text",
+ "placeholders": {
+ "version": {}
+ }
+ },
+ "Are you sure you want to log out?": "แน่ใจว่าต้องการล็อกเอาท์หรือไม่",
+ "@Are you sure you want to log out?": {
+ "description": "Confirmation message displayed when the user tries to log out",
+ "type": "text",
+ "placeholders": {}
+ },
+ "Calendars": "ปฏิทิน",
+ "@Calendars": {
+ "description": "Label for button that lets users select which calendars to display",
+ "type": "text",
+ "placeholders": {}
+ },
+ "nextMonth": "เดือนถัดไป: {month}",
+ "@nextMonth": {
+ "description": "Label for the button that switches the calendar to the next month",
+ "type": "text",
+ "placeholders": {
+ "month": {}
+ }
+ },
+ "previousMonth": "เดือนก่อนหน้า: {month}",
+ "@previousMonth": {
+ "description": "Label for the button that switches the calendar to the previous month",
+ "type": "text",
+ "placeholders": {
+ "month": {}
+ }
+ },
+ "nextWeek": "สัปดาห์ถัดไป เริ่มต้น {date}",
+ "@nextWeek": {
+ "description": "Label for the button that switches the calendar to the next week",
+ "type": "text",
+ "placeholders": {
+ "date": {}
+ }
+ },
+ "previousWeek": "สัปดาห์ก่อนหน้า เริ่มต้น {date}",
+ "@previousWeek": {
+ "description": "Label for the button that switches the calendar to the previous week",
+ "type": "text",
+ "placeholders": {
+ "date": {}
+ }
+ },
+ "selectedMonthLabel": "เดือน {month}",
+ "@selectedMonthLabel": {
+ "description": "Accessibility label for the button that expands/collapses the month view",
+ "type": "text",
+ "placeholders": {
+ "month": {}
+ }
+ },
+ "expand": "ขยาย",
+ "@expand": {
+ "description": "Accessibility label for the on-tap hint for the button that expands/collapses the month view",
+ "type": "text",
+ "placeholders": {}
+ },
+ "collapse": "ย่อ",
+ "@collapse": {
+ "description": "Accessibility label for the on-tap hint for the button that expands/collapses the month view",
+ "type": "text",
+ "placeholders": {}
+ },
+ "pointsPossible": "{points} คะแนนที่เป็นไปได้",
+ "@pointsPossible": {
+ "description": "Screen reader label used for the points possible for an assignment, quiz, etc.",
+ "type": "text",
+ "placeholders": {
+ "points": {}
+ }
+ },
+ "No Events Today!": "ไม่มีกิจกรรมในวันนี้!",
+ "@No Events Today!": {
+ "description": "Title displayed when there are no calendar events for the current day",
+ "type": "text",
+ "placeholders": {}
+ },
+ "It looks like a great day to rest, relax, and recharge.": "ดูเหมือนนี่จะเป็นวันที่เหมาะสำหรับพัก ผ่อนคลายและเติมพลัง",
+ "@It looks like a great day to rest, relax, and recharge.": {
+ "description": "Message displayed when there are no calendar events for the current day",
+ "type": "text",
+ "placeholders": {}
+ },
+ "There was an error loading your student's calendar": "มีข้อผิดพลาดในการโหลดปฏิทินผู้เรียนของคุณ",
+ "@There was an error loading your student's calendar": {
+ "description": "Message displayed when calendar events could not be loaded for the current student",
+ "type": "text",
+ "placeholders": {}
+ },
+ "Tap to favorite the courses you want to see on the Calendar. Select up to 10.": "กดเลือกเพื่อกำหนดรายการบทเรียนโปรดที่คุณต้องการดูในปฏิทิน เลือกได้สูงสุด 10 รายการ",
+ "@Tap to favorite the courses you want to see on the Calendar. Select up to 10.": {
+ "description": "Description text on calendar filter screen.",
+ "type": "text",
+ "placeholders": {}
+ },
+ "You may only choose 10 calendars to display": "คุณสามารถเลือกปฏิทินได้เพียง 10 รายการที่จะจัดแสดง",
+ "@You may only choose 10 calendars to display": {
+ "description": "Error text when trying to select more than 10 calendars",
+ "type": "text",
+ "placeholders": {}
+ },
+ "You must select at least one calendar to display": "คุณจะต้องเลือกปฏิทินอย่างน้อยหนึ่งรายการที่จะจัดแสดง",
+ "@You must select at least one calendar to display": {
+ "description": "Error text when trying to de-select all calendars",
+ "type": "text",
+ "placeholders": {}
+ },
+ "Planner Note": "หมายเหตุสำหรับแผนงาน",
+ "@Planner Note": {
+ "description": "Label used for notes in the planner",
+ "type": "text",
+ "placeholders": {}
+ },
+ "Go to today": "ไปที่วันนี้",
+ "@Go to today": {
+ "description": "Accessibility label used for the today button in the planner",
+ "type": "text",
+ "placeholders": {}
+ },
+ "Previous Logins": "การล็อกอินก่อนหน้า",
+ "@Previous Logins": {
+ "description": "Label for the list of previous user logins",
+ "type": "text",
+ "placeholders": {}
+ },
+ "canvasLogoLabel": "โลโก้ Canvas",
+ "@canvasLogoLabel": {
+ "description": "The semantics label for the Canvas logo",
+ "type": "text",
+ "placeholders": {}
+ },
+ "findSchool": "ค้นหาสถานศึกษา",
+ "@findSchool": {
+ "description": "Text for the find-my-school button",
+ "type": "text",
+ "placeholders": {}
+ },
+ "domainSearchInputHint": "กรอกชื่อสถานศึกษาหรือเขตพื้นที่...",
+ "@domainSearchInputHint": {
+ "description": "Input hint for the text box on the domain search screen",
+ "type": "text",
+ "placeholders": {}
+ },
+ "noDomainResults": "ไม่พบสถานศึกษาที่ตรงกับ “{query}”",
+ "@noDomainResults": {
+ "description": "Message shown to users when the domain search query did not return any results",
+ "type": "text",
+ "placeholders": {
+ "query": {}
+ }
+ },
+ "domainSearchHelpLabel": "จะค้นหาสถานศึกษาหรือเขตพื้นที่ของฉันได้อย่างไร",
+ "@domainSearchHelpLabel": {
+ "description": "Label for the help button on the domain search screen",
+ "type": "text",
+ "placeholders": {}
+ },
+ "canvasGuides": "คู่มือ Canvas",
+ "@canvasGuides": {
+ "description": "Proper name for the Canvas Guides. This will be used in the domainSearchHelpBody text and will be highlighted and clickable",
+ "type": "text",
+ "placeholders": {}
+ },
+ "canvasSupport": "บริการจาก Canvas",
+ "@canvasSupport": {
+ "description": "Proper name for Canvas Support. This will be used in the domainSearchHelpBody text and will be highlighted and clickable",
+ "type": "text",
+ "placeholders": {}
+ },
+ "domainSearchHelpBody": "ลองค้นหาชื่อสถานศึกษาหรือเขตพื้นที่ที่คุณพยายามสืบค้น เช่น “Smith Private School” หรือ “Smith County Schools” นอกจากนี้คุณยังสามารถกรอกโดเมน Canvas ได้โดยตรง เช่น “smith.instructure.com.”\n\nดูรายละเอียดเพิ่มเติมในการค้นหาบัญชี Canvas สำหรับสถาบันของคุณโดยเข้าไปที่ {canvasGuides} ติดต่อ {canvasSupport} หรือติดต่อสถานศึกษาของคุณเพื่อขอความช่วยเหลือ",
+ "@domainSearchHelpBody": {
+ "description": "The body text shown in the help dialog on the domain search screen",
+ "type": "text",
+ "placeholders": {
+ "canvasGuides": {},
+ "canvasSupport": {}
+ }
+ },
+ "Uh oh!": "โอ๊ะ โอ!",
+ "@Uh oh!": {
+ "description": "Title of the screen that shows when a crash has occurred",
+ "type": "text",
+ "placeholders": {}
+ },
+ "We’re not sure what happened, but it wasn’t good. Contact us if this keeps happening.": "เราไม่แน่ใจว่าเกิดอะไรขึ้น แต่เชื่อว่าไม่ดี ติดต่อเราหากยังเกิดปัญหานี้อยู่",
+ "@We’re not sure what happened, but it wasn’t good. Contact us if this keeps happening.": {
+ "description": "Message shown when a crash has occurred",
+ "type": "text",
+ "placeholders": {}
+ },
+ "Contact Support": "บริการสำหรับการติดต่อ",
+ "@Contact Support": {
+ "description": "Label for the button that allows users to contact support after a crash has occurred",
+ "type": "text",
+ "placeholders": {}
+ },
+ "View error details": "ดูรายละเอียดข้อผิดพลาด",
+ "@View error details": {
+ "description": "Label for the button that allowed users to view crash details",
+ "type": "text",
+ "placeholders": {}
+ },
+ "Restart app": "รีสตาร์ทแอพ",
+ "@Restart app": {
+ "description": "Label for the button that will restart the entire application",
+ "type": "text",
+ "placeholders": {}
+ },
+ "Application version": "เวอร์ชั่นแอพพลิเคชั่น",
+ "@Application version": {
+ "description": "Label for the application version displayed in the crash details",
+ "type": "text",
+ "placeholders": {}
+ },
+ "Device model": "รุ่นอุปกรณ์",
+ "@Device model": {
+ "description": "Label for the device model displayed in the crash details",
+ "type": "text",
+ "placeholders": {}
+ },
+ "Android OS version": "เวอร์ชั่น Android OS",
+ "@Android OS version": {
+ "description": "Label for the Android operating system version displayed in the crash details",
+ "type": "text",
+ "placeholders": {}
+ },
+ "Full error message": "ข้อความแจ้งข้อผิดพลาดเต็ม",
+ "@Full error message": {
+ "description": "Label for the full error message displayed in the crash details",
+ "type": "text",
+ "placeholders": {}
+ },
+ "Inbox": "กล่องจดหมาย",
+ "@Inbox": {
+ "description": "Title for the Inbox screen",
+ "type": "text",
+ "placeholders": {}
+ },
+ "There was an error loading your inbox messages.": "มีข้อผิดพลาดในการโหลดข้อความในกล่องจดหมายของคุณ",
+ "@There was an error loading your inbox messages.": {
+ "type": "text",
+ "placeholders": {}
+ },
+ "No Subject": "ไม่มีหัวเรื่อง",
+ "@No Subject": {
+ "description": "Title used for inbox messages that have no subject",
+ "type": "text",
+ "placeholders": {}
+ },
+ "Unable to fetch courses. Please check your connection and try again.": "ไม่สามารถสืบค้นบทเรียนได้ กรุณาตรวจสอบการเชื่อมต่อของคุณและลองใหม่อีกครั้ง",
+ "@Unable to fetch courses. Please check your connection and try again.": {
+ "description": "Message shown when an error occured while loading courses",
+ "type": "text",
+ "placeholders": {}
+ },
+ "Choose a course to message": "เลือกบทเรียนที่จะส่งข้อความ",
+ "@Choose a course to message": {
+ "description": "Header in the course list shown when the user is choosing which course to associate with a new message",
+ "type": "text",
+ "placeholders": {}
+ },
+ "Inbox Zero": "Inbox Zero",
+ "@Inbox Zero": {
+ "description": "Title of the message shown when there are no inbox messages",
+ "type": "text",
+ "placeholders": {}
+ },
+ "You’re all caught up!": "แย่แล้ว!",
+ "@You’re all caught up!": {
+ "description": "Subtitle of the message shown when there are no inbox messages",
+ "type": "text",
+ "placeholders": {}
+ },
+ "There was an error loading recipients for this course": "มีข้อผิดพลาดในการโหลดผู้รับสำหรับบทเรียนนี้",
+ "@There was an error loading recipients for this course": {
+ "description": "Message shown when attempting to create a new message but the recipients list failed to load",
+ "type": "text",
+ "placeholders": {}
+ },
+ "Unable to send message. Check your connection and try again.": "ไม่สามารถส่งข้อความได้ ตรวจสอบการเชื่อมต่อและลองใหม่อีกครั้ง",
+ "@Unable to send message. Check your connection and try again.": {
+ "description": "Message show when there was an error creating or sending a new message",
+ "type": "text",
+ "placeholders": {}
+ },
+ "Unsaved changes": "การเปลี่ยนแปลงที่ไม่ได้บันทึก",
+ "@Unsaved changes": {
+ "description": "Title of the dialog shown when the user tries to leave with unsaved changes",
+ "type": "text",
+ "placeholders": {}
+ },
+ "Are you sure you wish to close this page? Your unsent message will be lost.": "แน่ใจว่าต้องการปิดหน้าเพจนี้หรือไม่ ข้อความที่ไม่ได้ส่งของคุณจะหายไป",
+ "@Are you sure you wish to close this page? Your unsent message will be lost.": {
+ "description": "Body text of the dialog shown when the user tries leave with unsaved changes",
+ "type": "text",
+ "placeholders": {}
+ },
+ "New message": "ข้อความใหม่",
+ "@New message": {
+ "description": "Title of the new-message screen",
+ "type": "text",
+ "placeholders": {}
+ },
+ "Add attachment": "เพิ่มเอกสารแนบ",
+ "@Add attachment": {
+ "description": "Tooltip for the add-attachment button in the new-message screen",
+ "type": "text",
+ "placeholders": {}
+ },
+ "Send message": "ส่งข้อความ",
+ "@Send message": {
+ "description": "Tooltip for the send-message button in the new-message screen",
+ "type": "text",
+ "placeholders": {}
+ },
+ "Select recipients": "เลือกผู้รับ",
+ "@Select recipients": {
+ "description": "Tooltip for the button that allows users to select message recipients",
+ "type": "text",
+ "placeholders": {}
+ },
+ "No recipients selected": "ไม่ได้เลือกผู้รับ",
+ "@No recipients selected": {
+ "description": "Hint displayed when the user has not selected any message recipients",
+ "type": "text",
+ "placeholders": {}
+ },
+ "Message subject": "หัวเรื่องข้อความ",
+ "@Message subject": {
+ "description": "Hint text displayed in the input field for the message subject",
+ "type": "text",
+ "placeholders": {}
+ },
+ "Message": "ข้อความ",
+ "@Message": {
+ "description": "Hint text displayed in the input field for the message body",
+ "type": "text",
+ "placeholders": {}
+ },
+ "Recipients": "ผู้รับ",
+ "@Recipients": {
+ "description": "Label for message recipients",
+ "type": "text",
+ "placeholders": {}
+ },
+ "plusRecipientCount": "+{count}",
+ "@plusRecipientCount": {
+ "description": "Shows the number of recipients that are selected but not displayed on screen.",
+ "type": "text",
+ "placeholders": {
+ "count": {
+ "example": 5
+ }
+ }
+ },
+ "Failed. Tap for options.": "ล้มเหลว กดเพื่อดูตัวเลือก",
+ "@Failed. Tap for options.": {
+ "description": "Short message shown on a message attachment when uploading has failed",
+ "type": "text",
+ "placeholders": {}
+ },
+ "courseForWhom": "สำหรับ {studentShortName}",
+ "@courseForWhom": {
+ "description": "Describes for whom a course is for (i.e. for Bill)",
+ "type": "text",
+ "placeholders": {
+ "studentShortName": {}
+ }
+ },
+ "messageLinkPostscript": "เกี่ยวกับ: {studentName}, {linkUrl}",
+ "@messageLinkPostscript": {
+ "description": "A postscript appended to new messages that clarifies which student is the subject of the message and also includes a URL for the related Canvas component (course, assignment, event, etc).",
+ "type": "text",
+ "placeholders": {
+ "studentName": {},
+ "linkUrl": {}
+ }
+ },
+ "There was an error loading this conversation": "มีข้อผิดพลาดในการโหลดการพูดคุยนี้",
+ "@There was an error loading this conversation": {
+ "description": "Message shown when a conversation fails to load",
+ "type": "text",
+ "placeholders": {}
+ },
+ "Reply": "ตอบกลับ",
+ "@Reply": {
+ "description": "Button label for replying to a conversation",
+ "type": "text",
+ "placeholders": {}
+ },
+ "Reply All": "ตอบกลับทั้งหมด",
+ "@Reply All": {
+ "description": "Button label for replying to all conversation participants",
+ "type": "text",
+ "placeholders": {}
+ },
+ "Unknown User": "ผู้ใช้ที่ไม่รู้จัก",
+ "@Unknown User": {
+ "description": "Label used where the user name is not known",
+ "type": "text",
+ "placeholders": {}
+ },
+ "me": "ฉัน",
+ "@me": {
+ "description": "First-person pronoun (i.e. 'me') that will be used in message author info, e.g. 'Me to 4 others' or 'Jon Snow to me'",
+ "type": "text",
+ "placeholders": {}
+ },
+ "authorToRecipient": "{authorName} ถึง {recipientName}",
+ "@authorToRecipient": {
+ "description": "Author info for a single-recipient message; includes both the author name and the recipient name.",
+ "type": "text",
+ "placeholders": {
+ "authorName": {},
+ "recipientName": {}
+ }
+ },
+ "authorToNOthers": "{howMany,plural, =1{{authorName} ถึงคนอื่นอีก 1 ราย}other{{authorName} กับคนอื่นอีก {howMany} ราย}}",
+ "@authorToNOthers": {
+ "description": "Author info for a mutli-recipient message; includes the author name and the number of recipients",
+ "type": "text",
+ "placeholders": {
+ "authorName": {},
+ "howMany": {}
+ }
+ },
+ "authorToRecipientAndNOthers": "{howMany,plural, =1{{authorName} กับ {recipientName} และคนอื่นอีก 1 ราย}other{{authorName} กับ {recipientName} และคนอื่นอีก {howMany} ราย}}",
+ "@authorToRecipientAndNOthers": {
+ "description": "Author info for a multi-recipient message; includes the author name, one recipient name, and the number of other recipients",
+ "type": "text",
+ "placeholders": {
+ "authorName": {},
+ "recipientName": {},
+ "howMany": {}
+ }
+ },
+ "Download": "ดาวน์โหลด",
+ "@Download": {
+ "description": "Label for the button that will begin downloading a file",
+ "type": "text",
+ "placeholders": {}
+ },
+ "Open with another app": "เปิดโดยใช้แอพอื่น",
+ "@Open with another app": {
+ "description": "Label for the button that will allow users to open a file with another app",
+ "type": "text",
+ "placeholders": {}
+ },
+ "There are no installed applications that can open this file": "ไม่มีแอพพลิเคชั่นติดตั้งที่เปิดไฟล์นี้ได้",
+ "@There are no installed applications that can open this file": {
+ "type": "text",
+ "placeholders": {}
+ },
+ "Unsupported File": "ไฟล์ไม่รองรับ",
+ "@Unsupported File": {
+ "type": "text",
+ "placeholders": {}
+ },
+ "This file is unsupported and can’t be viewed through the app": "ไม่รองรับไฟล์นี้และไม่สามารถเปิดดูได้ผ่านแอพนี้",
+ "@This file is unsupported and can’t be viewed through the app": {
+ "type": "text",
+ "placeholders": {}
+ },
+ "Unable to play this media file": "ไม่สามารถเปิดเล่นไฟล์สื่อนี้",
+ "@Unable to play this media file": {
+ "description": "Message shown when audio or video media could not be played",
+ "type": "text",
+ "placeholders": {}
+ },
+ "Unable to load this image": "ไม่สามารถโหลดภาพนี้",
+ "@Unable to load this image": {
+ "description": "Message shown when an image file could not be loaded or displayed",
+ "type": "text",
+ "placeholders": {}
+ },
+ "There was an error loading this file": "มีข้อผิดพลาดในการโหลดไฟล์นี้",
+ "@There was an error loading this file": {
+ "description": "Message shown when a file could not be loaded or displayed",
+ "type": "text",
+ "placeholders": {}
+ },
+ "No Courses": "ไม่มีบทเรียน",
+ "@No Courses": {
+ "description": "Title for having no courses",
+ "type": "text",
+ "placeholders": {}
+ },
+ "Your student’s courses might not be published yet.": "บทเรียนสำหรับผู้เรียนของคุณอาจยังไม่ได้เผยแพร่",
+ "@Your student’s courses might not be published yet.": {
+ "description": "Message for having no courses",
+ "type": "text",
+ "placeholders": {}
+ },
+ "There was an error loading your student’s courses.": "มีข้อผิดพลาดในการโหลดบทเรียนสำหรับผู้เรียน",
+ "@There was an error loading your student’s courses.": {
+ "description": "Message displayed when the list of student courses could not be loaded",
+ "type": "text",
+ "placeholders": {}
+ },
+ "No Grade": "ไม่มีเกรด",
+ "@No Grade": {
+ "description": "Message shown when there is currently no grade available for a course",
+ "type": "text",
+ "placeholders": {}
+ },
+ "Filter by": "กรองจาก",
+ "@Filter by": {
+ "description": "Title for list of terms to filter grades by",
+ "type": "text",
+ "placeholders": {}
+ },
+ "Grades": "เกรด",
+ "@Grades": {
+ "description": "Label for the \"Grades\" tab in course details",
+ "type": "text",
+ "placeholders": {}
+ },
+ "Syllabus": "หลักสูตร",
+ "@Syllabus": {
+ "description": "Label for the \"Syllabus\" tab in course details",
+ "type": "text",
+ "placeholders": {}
+ },
+ "Front Page": "หน้าแรก",
+ "@Front Page": {
+ "description": "Label for the \"Front Page\" tab in course details",
+ "type": "text",
+ "placeholders": {}
+ },
+ "Summary": "สรุป",
+ "@Summary": {
+ "description": "Label for the \"Summary\" tab in course details",
+ "type": "text",
+ "placeholders": {}
+ },
+ "Send a message about this course": "ส่งข้อความเกี่ยวกับบทเรียนนี้",
+ "@Send a message about this course": {
+ "description": "Accessibility hint for the course messaage floating action button",
+ "type": "text",
+ "placeholders": {}
+ },
+ "Total Grade": "เกรดรวม",
+ "@Total Grade": {
+ "description": "Label for the total grade in the course",
+ "type": "text",
+ "placeholders": {}
+ },
+ "Graded": "ให้เกรดแล้ว",
+ "@Graded": {
+ "description": "Label for assignments that have been graded",
+ "type": "text",
+ "placeholders": {}
+ },
+ "Submitted": "จัดส่งแล้ว",
+ "@Submitted": {
+ "description": "Label for assignments that have been submitted",
+ "type": "text",
+ "placeholders": {}
+ },
+ "Not Submitted": "ไม่ได้จัดส่ง",
+ "@Not Submitted": {
+ "description": "Label for assignments that have not been submitted",
+ "type": "text",
+ "placeholders": {}
+ },
+ "Late": "ล่าช้า",
+ "@Late": {
+ "description": "Label for assignments that have been marked late or submitted late",
+ "type": "text",
+ "placeholders": {}
+ },
+ "Missing": "ขาดหาย",
+ "@Missing": {
+ "description": "Label for assignments that have been marked missing or are not submitted and past the due date",
+ "type": "text",
+ "placeholders": {}
+ },
+ "-": "-",
+ "@-": {
+ "description": "Value representing no score for student submission",
+ "type": "text",
+ "placeholders": {}
+ },
+ "All Grading Periods": "ระยะเวลาการให้เกรดทั้งหมด",
+ "@All Grading Periods": {
+ "description": "Label for selecting all grading periods",
+ "type": "text",
+ "placeholders": {}
+ },
+ "No Assignments": "ไม่มีภารกิจ",
+ "@No Assignments": {
+ "description": "Title for the no assignments message",
+ "type": "text",
+ "placeholders": {}
+ },
+ "It looks like assignments haven't been created in this space yet.": "ดูเหมือนจะยังไม่ได้จัดทำภารกิจในพื้นที่นี้",
+ "@It looks like assignments haven't been created in this space yet.": {
+ "description": "Message for no assignments",
+ "type": "text",
+ "placeholders": {}
+ },
+ "There was an error loading the summary details for this course.": "มีข้อผิดพลาดในการโหลดรายละเอียดสรุปสำหรับบทเรียนนี้",
+ "@There was an error loading the summary details for this course.": {
+ "description": "Message shown when the course summary could not be loaded",
+ "type": "text",
+ "placeholders": {}
+ },
+ "No Summary": "ไม่มีข้อมูลสรุป",
+ "@No Summary": {
+ "description": "Title displayed when there are no items in the course summary",
+ "type": "text",
+ "placeholders": {}
+ },
+ "This course does not have any assignments or calendar events yet.": "บทเรียนนี้ไม่มีภารกิจหรือกิจกรรมในปฏิทินในตอนนี้",
+ "@This course does not have any assignments or calendar events yet.": {
+ "description": "Message displayed when there are no items in the course summary",
+ "type": "text",
+ "placeholders": {}
+ },
+ "gradeFormatScoreOutOfPointsPossible": "{score} / {pointsPossible}",
+ "@gradeFormatScoreOutOfPointsPossible": {
+ "description": "Formatted string for a student score out of the points possible",
+ "type": "text",
+ "placeholders": {
+ "score": {},
+ "pointsPossible": {}
+ }
+ },
+ "contentDescriptionScoreOutOfPointsPossible": "{score} จา {pointsPossible} คะแนน",
+ "@contentDescriptionScoreOutOfPointsPossible": {
+ "description": "Formatted string for a student score out of the points possible",
+ "type": "text",
+ "placeholders": {
+ "score": {},
+ "pointsPossible": {}
+ }
+ },
+ "gradesSubjectMessage": "เกี่ยวกับ: {studentName}, เกรด",
+ "@gradesSubjectMessage": {
+ "description": "The subject line for a message to a teacher regarding a student's grades",
+ "type": "text",
+ "placeholders": {
+ "studentName": {}
+ }
+ },
+ "syllabusSubjectMessage": "เกี่ยวกับ: {studentName}, หลักสูตร",
+ "@syllabusSubjectMessage": {
+ "description": "The subject line for a message to a teacher regarding a course syllabus",
+ "type": "text",
+ "placeholders": {
+ "studentName": {}
+ }
+ },
+ "frontPageSubjectMessage": "เกี่ยวกับ: {studentName}, หน้าแรก",
+ "@frontPageSubjectMessage": {
+ "description": "The subject line for a message to a teacher regarding a course front page",
+ "type": "text",
+ "placeholders": {
+ "studentName": {}
+ }
+ },
+ "assignmentSubjectMessage": "เกี่ยวกับ: {studentName}, ภารกิจ - {assignmentName}",
+ "@assignmentSubjectMessage": {
+ "description": "The subject line for a message to a teacher regarding a student's assignment",
+ "type": "text",
+ "placeholders": {
+ "studentName": {},
+ "assignmentName": {}
+ }
+ },
+ "eventSubjectMessage": "เกี่ยวกับ: {studentName}, กิจกรรม - {eventTitle}",
+ "@eventSubjectMessage": {
+ "description": "The subject line for a message to a teacher regarding a calendar event",
+ "type": "text",
+ "placeholders": {
+ "studentName": {},
+ "eventTitle": {}
+ }
+ },
+ "There is no page information available.": "ไม่มีข้อมูลเพจ",
+ "@There is no page information available.": {
+ "description": "Description for when no page information is available",
+ "type": "text",
+ "placeholders": {}
+ },
+ "Assignment Details": "รายละเอียดภารกิจ",
+ "@Assignment Details": {
+ "description": "Title for the page that shows details for an assignment",
+ "type": "text",
+ "placeholders": {}
+ },
+ "assignmentTotalPoints": "{points} คะแนน",
+ "@assignmentTotalPoints": {
+ "description": "Label used for the total points the assignment is worth",
+ "type": "text",
+ "placeholders": {
+ "points": {}
+ }
+ },
+ "assignmentTotalPointsAccessible": "{points} คะแนน",
+ "@assignmentTotalPointsAccessible": {
+ "description": "Screen reader label used for the total points the assignment is worth",
+ "type": "text",
+ "placeholders": {
+ "points": {}
+ }
+ },
+ "Due": "ครบกำหนด",
+ "@Due": {
+ "description": "Label for an assignment due date",
+ "type": "text",
+ "placeholders": {}
+ },
+ "Grade": "เกรด",
+ "@Grade": {
+ "description": "Label for the section that displays an assignment's grade",
+ "type": "text",
+ "placeholders": {}
+ },
+ "Locked": "ล็อคแล้ว",
+ "@Locked": {
+ "description": "Label for when an assignment is locked",
+ "type": "text",
+ "placeholders": {}
+ },
+ "assignmentLockedModule": "ภารกิจนี้ถูกล็อคโดยหน่วยการเรียน “{moduleName}”",
+ "@assignmentLockedModule": {
+ "description": "The locked description when an assignment is locked by a module",
+ "type": "text",
+ "placeholders": {
+ "moduleName": {}
+ }
+ },
+ "Remind Me": "เตือนฉัน",
+ "@Remind Me": {
+ "description": "Label for the row to set reminders",
+ "type": "text",
+ "placeholders": {}
+ },
+ "Set a date and time to be notified of this specific assignment.": "กำหนดวันที่และเวลาที่จะรับการแจ้งเตือนสำหรับภารกิจเฉพาะนี้",
+ "@Set a date and time to be notified of this specific assignment.": {
+ "description": "Description for row to set reminders",
+ "type": "text",
+ "placeholders": {}
+ },
+ "You will be notified about this assignment on…": "คุณจะได้รับแจ้งเกี่ยวกับภารกิจใน....",
+ "@You will be notified about this assignment on…": {
+ "description": "Description for when a reminder is set",
+ "type": "text",
+ "placeholders": {}
+ },
+ "Instructions": "คำแนะนำ",
+ "@Instructions": {
+ "description": "Label for the description of the assignment when it has quiz instructions",
+ "type": "text",
+ "placeholders": {}
+ },
+ "Send a message about this assignment": "ส่งข้อความเกี่ยวกับภารกิจนี้",
+ "@Send a message about this assignment": {
+ "description": "Accessibility hint for the assignment messaage floating action button",
+ "type": "text",
+ "placeholders": {}
+ },
+ "This app is not authorized for use.": "แอพนี้ไม่ได้รับอนุญาตให้ใช้งาน",
+ "@This app is not authorized for use.": {
+ "description": "The error shown when the app being used is not verified by Canvas",
+ "type": "text",
+ "placeholders": {}
+ },
+ "The server you entered is not authorized for this app.": "เซิร์ฟเวอร์ที่คุณกรอกไม่ได้รับอนุญาตสำหรับแอพนี้",
+ "@The server you entered is not authorized for this app.": {
+ "description": "The error shown when the desired login domain is not verified by Canvas",
+ "type": "text",
+ "placeholders": {}
+ },
+ "The user agent for this app is not authorized.": "ระบบตัวแทนของผู้ใช้สำหรับแอพนี้ไม่ได้รับอนุญาต",
+ "@The user agent for this app is not authorized.": {
+ "description": "The error shown when the user agent during verification is not verified by Canvas",
+ "type": "text",
+ "placeholders": {}
+ },
+ "We were unable to verify the server for use with this app.": "เราไม่สามารถยืนยันเซิร์ฟเวอร์สำหรับใช้กับแอพนี้",
+ "@We were unable to verify the server for use with this app.": {
+ "description": "The generic error shown when we are unable to verify with Canvas",
+ "type": "text",
+ "placeholders": {}
+ },
+ "Reminders": "การแจ้งเตือน",
+ "@Reminders": {
+ "description": "Name of the system notification channel for assignment and event reminders",
+ "type": "text",
+ "placeholders": {}
+ },
+ "Notifications for reminders about assignments and calendar events": "การแจ้งข้อมูลสำหรับการแจ้งเตือนเกี่ยวกับภารกิจและกิจกรรมในปฏิทิน",
+ "@Notifications for reminders about assignments and calendar events": {
+ "description": "Description of the system notification channel for assignment and event reminders",
+ "type": "text",
+ "placeholders": {}
+ },
+ "Reminders have changed!": "การแจ้งเตือนเปลี่ยนแปลงแล้ว!",
+ "@Reminders have changed!": {
+ "description": "Title of the dialog shown when the user needs to update their reminders",
+ "type": "text",
+ "placeholders": {}
+ },
+ "In order to provide you with a better experience, we have updated how reminders work. You can add new reminders by viewing an assignment or calendar event and tapping the switch under the \"Remind Me\" section.\n\nBe aware that any reminders created with older versions of this app will not be compatible with the new changes and you will need to create them again.": "เพื่อให้คุณใช้งานได้สะดวกมากขึ้น เราได้มีการอัพเดตระบบแจ้งเตือนใหม่ คุณสามารถเพิ่มการแจ้งเตือนใหม่โดยดูภารกิจหรือกิจกรรมในปฏิทินและกดเลือกสลับการใช้งานได้จากหัวข้อ “เตือนฉัน”\n\nการแจ้งเตือนใด ๆ ที่จัดทำผ่านแอพนี้ในเวอร์ชั่นเก่าจะไม่รองรับการเปลี่ยนแปลงใหม่นี้และคุณจะต้องจัดทำชุดข้อมูลใหม่อีกครั้ง",
+ "@In order to provide you with a better experience, we have updated how reminders work. You can add new reminders by viewing an assignment or calendar event and tapping the switch under the \"Remind Me\" section.\n\nBe aware that any reminders created with older versions of this app will not be compatible with the new changes and you will need to create them again.": {
+ "type": "text",
+ "placeholders": {}
+ },
+ "Not a parent?": "ไม่ใช่พ่อแม่ผู้ปกครองใช่หรือไม่",
+ "@Not a parent?": {
+ "description": "Title for the screen that shows when the user is not observing any students",
+ "type": "text",
+ "placeholders": {}
+ },
+ "We couldn't find any students associated with this account": "เราไม่พบผู้เรียนที่เชื่อมโยงกับบัญชีนี้",
+ "@We couldn't find any students associated with this account": {
+ "description": "Subtitle for the screen that shows when the user is not observing any students",
+ "type": "text",
+ "placeholders": {}
+ },
+ "Are you a student or teacher?": "คุณเป็นผู้เรียนหรือผู้สอน",
+ "@Are you a student or teacher?": {
+ "description": "Label for button that will show users the option to view other Canvas apps in the Play Store",
+ "type": "text",
+ "placeholders": {}
+ },
+ "One of our other apps might be a better fit. Tap one to visit the Play Store.": "แอพอื่น ๆ บางส่วนของเราอาจเหมาะสมมากกว่า กดเลือกหนึ่งรายการเพื่อไปยัง Play Store",
+ "@One of our other apps might be a better fit. Tap one to visit the Play Store.": {
+ "description": "Description of options to view other Canvas apps in the Play Store",
+ "type": "text",
+ "placeholders": {}
+ },
+ "Return to Login": "กลับไปที่ล็อกอิน",
+ "@Return to Login": {
+ "description": "Label for the button that returns the user to the login screen",
+ "type": "text",
+ "placeholders": {}
+ },
+ "STUDENT": "ผู้เรียน",
+ "@STUDENT": {
+ "description": "The \"student\" portion of the \"Canvas Student\" app name, in all caps. \"Canvas\" is excluded in this context as it will be displayed to the user as a wordmark image",
+ "type": "text",
+ "placeholders": {}
+ },
+ "TEACHER": "ผู้สอน",
+ "@TEACHER": {
+ "description": "The \"teacher\" portion of the \"Canvas Teacher\" app name, in all caps. \"Canvas\" is excluded in this context as it will be displayed to the user as a wordmark image",
+ "type": "text",
+ "placeholders": {}
+ },
+ "Canvas Student": "Canvas Student",
+ "@Canvas Student": {
+ "description": "The name of the Canvas Student app. Only \"Student\" should be translated as \"Canvas\" is a brand name in this context and should not be translated.",
+ "type": "text",
+ "placeholders": {}
+ },
+ "Canvas Teacher": "Canvas Teacher",
+ "@Canvas Teacher": {
+ "description": "The name of the Canvas Teacher app. Only \"Teacher\" should be translated as \"Canvas\" is a brand name in this context and should not be translated.",
+ "type": "text",
+ "placeholders": {}
+ },
+ "No Alerts": "ไม่มีการแจ้งเตือน",
+ "@No Alerts": {
+ "description": "The title for the empty message to show to users when there are no alerts for the student.",
+ "type": "text",
+ "placeholders": {}
+ },
+ "There’s nothing to be notified of yet.": "ไม่มีข้อมูลที่จะแจ้งในตอนนี้",
+ "@There’s nothing to be notified of yet.": {
+ "description": "The empty message to show to users when there are no alerts for the student.",
+ "type": "text",
+ "placeholders": {}
+ },
+ "dismissAlertLabel": "ล้มเลิก {alertTitle}",
+ "@dismissAlertLabel": {
+ "description": "Accessibility label to dismiss an alert",
+ "type": "text",
+ "placeholders": {
+ "alertTitle": {}
+ }
+ },
+ "Course Announcement": "ประกาศแจ้งสำหรับบทเรียน",
+ "@Course Announcement": {
+ "description": "Title for alerts when there is a course announcement",
+ "type": "text",
+ "placeholders": {}
+ },
+ "Institution Announcement": "ประกาศของสถาบัน",
+ "@Institution Announcement": {
+ "description": "Title for alerts when there is an institution announcement",
+ "type": "text",
+ "placeholders": {}
+ },
+ "assignmentGradeAboveThreshold": "เกรดภารกิจมากกว่า {threshold}",
+ "@assignmentGradeAboveThreshold": {
+ "description": "Title for alerts when an assignment grade is above the threshold value",
+ "type": "text",
+ "placeholders": {
+ "threshold": {}
+ }
+ },
+ "assignmentGradeBelowThreshold": "เกรดภารกิจต่ำกว่า {threshold}",
+ "@assignmentGradeBelowThreshold": {
+ "description": "Title for alerts when an assignment grade is below the threshold value",
+ "type": "text",
+ "placeholders": {
+ "threshold": {}
+ }
+ },
+ "courseGradeAboveThreshold": "เกรดบทเรียนมากกว่า {threshold}",
+ "@courseGradeAboveThreshold": {
+ "description": "Title for alerts when a course grade is above the threshold value",
+ "type": "text",
+ "placeholders": {
+ "threshold": {}
+ }
+ },
+ "courseGradeBelowThreshold": "เกรดบทเรียนน้อยกว่า {threshold}",
+ "@courseGradeBelowThreshold": {
+ "description": "Title for alerts when a course grade is below the threshold value",
+ "type": "text",
+ "placeholders": {
+ "threshold": {}
+ }
+ },
+ "Settings": "ค่าปรับตั้ง",
+ "@Settings": {
+ "description": "Title for the settings screen",
+ "type": "text",
+ "placeholders": {}
+ },
+ "Theme": "ธีม",
+ "@Theme": {
+ "description": "Label for the light/dark theme section in the settings page",
+ "type": "text",
+ "placeholders": {}
+ },
+ "Dark Mode": "โหมดมืด",
+ "@Dark Mode": {
+ "description": "Label for the button that enables dark mode",
+ "type": "text",
+ "placeholders": {}
+ },
+ "Light Mode": "โหมดสว่าง",
+ "@Light Mode": {
+ "description": "Label for the button that enables light mode",
+ "type": "text",
+ "placeholders": {}
+ },
+ "High Contrast Mode": "โหมดคอนทราสต์สูง",
+ "@High Contrast Mode": {
+ "description": "Label for the switch that toggles high contrast mode",
+ "type": "text",
+ "placeholders": {}
+ },
+ "Use Dark Theme in Web Content": "ใช้ธีมมืดในเนื้อหาบนเว็บ",
+ "@Use Dark Theme in Web Content": {
+ "description": "Label for the switch that toggles dark mode for webviews",
+ "type": "text",
+ "placeholders": {}
+ },
+ "Appearance": "ลักษณะภายนอก",
+ "@Appearance": {
+ "description": "Label for the appearance section in the settings page",
+ "type": "text",
+ "placeholders": {}
+ },
+ "Successfully submitted!": "ส่งเสร็จสิ้น!",
+ "@Successfully submitted!": {
+ "description": "Title displayed in the grade cell for an assignment that has been submitted",
+ "type": "text",
+ "placeholders": {}
+ },
+ "submissionStatusSuccessSubtitle": "ภารกิจนี้ถูกจัดส่งแล้วเมื่อ {date} เวลา {time} และกำลังรอการให้เกรด",
+ "@submissionStatusSuccessSubtitle": {
+ "description": "Subtitle displayed in the grade cell for an assignment that has been submitted and is awaiting a grade",
+ "type": "text",
+ "placeholders": {
+ "date": {},
+ "time": {}
+ }
+ },
+ "outOfPoints": "{howMany,plural, =1{จาก 1 คะแนน}other{จาก {points} คะแนน}}",
+ "@outOfPoints": {
+ "description": "Description for an assignment grade that has points without a current scoroe",
+ "type": "text",
+ "placeholders": {
+ "points": {},
+ "howMany": {}
+ }
+ },
+ "Excused": "ได้รับการยกเว้น",
+ "@Excused": {
+ "description": "Grading status for an assignment marked as excused",
+ "type": "text",
+ "placeholders": {}
+ },
+ "Complete": "เสร็จสิ้น",
+ "@Complete": {
+ "description": "Grading status for an assignment marked as complete",
+ "type": "text",
+ "placeholders": {}
+ },
+ "Incomplete": "ไม่เสร็จสิ้น",
+ "@Incomplete": {
+ "description": "Grading status for an assignment marked as incomplete",
+ "type": "text",
+ "placeholders": {}
+ },
+ "minus": "ลบ",
+ "@minus": {
+ "description": "Screen reader-friendly replacement for the \"-\" character in letter grades like \"A-\"",
+ "type": "text",
+ "placeholders": {}
+ },
+ "latePenalty": "โทษปรับล่าช้า (-{pointsLost})",
+ "@latePenalty": {
+ "description": "Text displayed when a late penalty has been applied to the assignment",
+ "type": "text",
+ "placeholders": {
+ "pointsLost": {}
+ }
+ },
+ "finalGrade": "เกรดสรุป: {grade}",
+ "@finalGrade": {
+ "description": "Text that displays the final grade of an assignment",
+ "type": "text",
+ "placeholders": {
+ "grade": {}
+ }
+ },
+ "Alert Settings": "ค่าการแจ้งเตือน",
+ "@Alert Settings": {
+ "type": "text",
+ "placeholders": {}
+ },
+ "Alert me when…": "แจ้งเตือนฉันเมื่อ...",
+ "@Alert me when…": {
+ "description": "Header for the screen where the observer chooses the thresholds that will determine when they receive alerts (e.g. when an assignment is graded below 70%)",
+ "type": "text",
+ "placeholders": {}
+ },
+ "Course grade below": "บทเรียน เกรดน้อยกว่า",
+ "@Course grade below": {
+ "description": "Label describing the threshold for when the course grade is below a certain percentage",
+ "type": "text",
+ "placeholders": {}
+ },
+ "Course grade above": "บทเรียน เกรดมากกว่า",
+ "@Course grade above": {
+ "description": "Label describing the threshold for when the course grade is above a certain percentage",
+ "type": "text",
+ "placeholders": {}
+ },
+ "Assignment missing": "ไม่มีภารกิจ",
+ "@Assignment missing": {
+ "type": "text",
+ "placeholders": {}
+ },
+ "Assignment grade below": "เกรดภารกิจน้อยกว่า",
+ "@Assignment grade below": {
+ "description": "Label describing the threshold for when an assignment is graded below a certain percentage",
+ "type": "text",
+ "placeholders": {}
+ },
+ "Assignment grade above": "เกรดภารกิจมากกว่า",
+ "@Assignment grade above": {
+ "description": "Label describing the threshold for when an assignment is graded above a certain percentage",
+ "type": "text",
+ "placeholders": {}
+ },
+ "Course Announcements": "ประกาศเกี่ยวกับบทเรียน",
+ "@Course Announcements": {
+ "type": "text",
+ "placeholders": {}
+ },
+ "Institution Announcements": "ประกาศของสถาบัน",
+ "@Institution Announcements": {
+ "type": "text",
+ "placeholders": {}
+ },
+ "Never": "ไม่เลย",
+ "@Never": {
+ "description": "Indication that tells the user they will not receive alert notifications of a specific kind",
+ "type": "text",
+ "placeholders": {}
+ },
+ "Grade percentage": "เปอร์เซ็นต์เกรด",
+ "@Grade percentage": {
+ "type": "text",
+ "placeholders": {}
+ },
+ "There was an error loading your student's alerts.": "มีข้อผิดพลาดในการโหลดการแจ้งเตือนสำหรับผู้เรียนของคุณ",
+ "@There was an error loading your student's alerts.": {
+ "type": "text",
+ "placeholders": {}
+ },
+ "Must be below 100": "จะต้องต่ำกว่า 100",
+ "@Must be below 100": {
+ "type": "text",
+ "placeholders": {}
+ },
+ "mustBeBelowN": "จะต้องต่ำกว่า {percentage}",
+ "@mustBeBelowN": {
+ "description": "Validation error to the user that they must choose a percentage below 'n'",
+ "type": "text",
+ "placeholders": {
+ "percentage": {
+ "example": 5
+ }
+ }
+ },
+ "mustBeAboveN": "จะต้องมากกว่า {percentage}",
+ "@mustBeAboveN": {
+ "description": "Validation error to the user that they must choose a percentage above 'n'",
+ "type": "text",
+ "placeholders": {
+ "percentage": {
+ "example": 5
+ }
+ }
+ },
+ "Select Student Color": "เลือกสีของผู้เรียน",
+ "@Select Student Color": {
+ "description": "Title for screen that allows users to assign a color to a specific student",
+ "type": "text",
+ "placeholders": {}
+ },
+ "Electric, blue": "อิเล็คทริค, น้ำเงิน",
+ "@Electric, blue": {
+ "description": "Name of the Electric (blue) color",
+ "type": "text",
+ "placeholders": {}
+ },
+ "Plum, Purple": "พลัม, ม่วง",
+ "@Plum, Purple": {
+ "description": "Name of the Plum (purple) color",
+ "type": "text",
+ "placeholders": {}
+ },
+ "Barney, Fuschia": "บาร์นีย์, ฟูเชีย",
+ "@Barney, Fuschia": {
+ "description": "Name of the Barney (fuschia) color",
+ "type": "text",
+ "placeholders": {}
+ },
+ "Raspberry, Red": "ราสเบอร์รี่, แดง",
+ "@Raspberry, Red": {
+ "description": "Name of the Raspberry (red) color",
+ "type": "text",
+ "placeholders": {}
+ },
+ "Fire, Orange": "ไฟเออร์, ส้ม",
+ "@Fire, Orange": {
+ "description": "Name of the Fire (orange) color",
+ "type": "text",
+ "placeholders": {}
+ },
+ "Shamrock, Green": "ชัมร็อค, เขียว",
+ "@Shamrock, Green": {
+ "description": "Name of the Shamrock (green) color",
+ "type": "text",
+ "placeholders": {}
+ },
+ "An error occurred while saving your selection. Please try again.": "เกิดข้อผิดพลาดขณะบันทึกรายการที่คุณเลือก กรุณาลองใหม่อีกครั้งในภายหลัง",
+ "@An error occurred while saving your selection. Please try again.": {
+ "type": "text",
+ "placeholders": {}
+ },
+ "changeStudentColorLabel": "เปลี่ยนสีสำหรับ {studentName}",
+ "@changeStudentColorLabel": {
+ "description": "Accessibility label for the button that lets users change the color associated with a specific student",
+ "type": "text",
+ "placeholders": {
+ "studentName": {}
+ }
+ },
+ "Teacher": "ผู้สอน",
+ "@Teacher": {
+ "description": "Label for the Teacher enrollment type",
+ "type": "text",
+ "placeholders": {}
+ },
+ "Student": "ผู้เรียน",
+ "@Student": {
+ "description": "Label for the Student enrollment type",
+ "type": "text",
+ "placeholders": {}
+ },
+ "TA": "TA",
+ "@TA": {
+ "description": "Label for the Teaching Assistant enrollment type (also known as Teacher Aid or Education Assistant), reduced to a short acronym/initialism if appropriate.",
+ "type": "text",
+ "placeholders": {}
+ },
+ "Observer": "ผู้สังเกตการณ์",
+ "@Observer": {
+ "description": "Label for the Observer enrollment type",
+ "type": "text",
+ "placeholders": {}
+ },
+ "Use Camera": "ใช้กล้อง",
+ "@Use Camera": {
+ "description": "Label for the action item that lets the user capture a photo using the device camera",
+ "type": "text",
+ "placeholders": {}
+ },
+ "Upload File": "อัพโหลดไฟล์",
+ "@Upload File": {
+ "description": "Label for the action item that lets the user upload a file from their device",
+ "type": "text",
+ "placeholders": {}
+ },
+ "Choose from Gallery": "เลือกจากแกลเลอรี่",
+ "@Choose from Gallery": {
+ "description": "Label for the action item that lets the user select a photo from their device gallery",
+ "type": "text",
+ "placeholders": {}
+ },
+ "Preparing…": "กำลังจัดเตรียม...",
+ "@Preparing…": {
+ "description": "Message shown while a file is being prepared to attach to a message",
+ "type": "text",
+ "placeholders": {}
+ },
+ "Add student with…": "เพิ่มผู้เรียนกับ...",
+ "@Add student with…": {
+ "type": "text",
+ "placeholders": {}
+ },
+ "Add Student": "เพิ่มผู้เรียน",
+ "@Add Student": {
+ "type": "text",
+ "placeholders": {}
+ },
+ "You are not observing any students.": "คุณไม่ได้สังเกตการณ์ผู้เรียนรายใด",
+ "@You are not observing any students.": {
+ "type": "text",
+ "placeholders": {}
+ },
+ "There was an error loading your students.": "มีข้อผิดพลาดในการโหลดผู้เรียนของคุณ",
+ "@There was an error loading your students.": {
+ "type": "text",
+ "placeholders": {}
+ },
+ "Pairing Code": "รหัสเข้าคู่",
+ "@Pairing Code": {
+ "type": "text",
+ "placeholders": {}
+ },
+ "Students can obtain a pairing code through the Canvas website": "ผู้เรียนสามารถขอรหัสเข้าคู่ได้จากเว็บไซต์ Canvas",
+ "@Students can obtain a pairing code through the Canvas website": {
+ "type": "text",
+ "placeholders": {}
+ },
+ "Enter the student pairing code provided to you. If the pairing code doesn't work, it may have expired": "กรอกรหัสเข้าคู่ของผู้เรียนที่แจ้งไว้กับคุณ หากรหัสเข้าคู่ไม่สามารถใช้ได้ แสดงว่าหมดอายุแล้ว",
+ "@Enter the student pairing code provided to you. If the pairing code doesn't work, it may have expired": {
+ "type": "text",
+ "placeholders": {}
+ },
+ "Your code is incorrect or expired.": "รหัสของคุณไม่ถูกต้องหรือหมดอายุแล้ว",
+ "@Your code is incorrect or expired.": {
+ "type": "text",
+ "placeholders": {}
+ },
+ "Something went wrong trying to create your account, please reach out to your school for assistance.": "มีบางอย่างผิดพลาดขณะพยายามจัดทำบัญชีผู้ใช้ของคุณ กรุณาติดต่อสถานศึกษาของคุณเพื่อขอความช่วยเหลือ",
+ "@Something went wrong trying to create your account, please reach out to your school for assistance.": {
+ "type": "text",
+ "placeholders": {}
+ },
+ "QR Code": "รหัส QR",
+ "@QR Code": {
+ "type": "text",
+ "placeholders": {}
+ },
+ "Students can create a QR code using the Canvas Student app on their mobile device": "ผู้เรียนสามารถจัดทำรหัส QR โดยใช้แอพ Canvas Student ผ่านอุปกรณ์พกพาของตน",
+ "@Students can create a QR code using the Canvas Student app on their mobile device": {
+ "type": "text",
+ "placeholders": {}
+ },
+ "Add new student": "เพิ่มผู้เรียนใหม่",
+ "@Add new student": {
+ "description": "Semantics label for the FAB on the Manage Students Screen",
+ "type": "text",
+ "placeholders": {}
+ },
+ "Select": "เลือก",
+ "@Select": {
+ "description": "Hint text to tell the user to choose one of two options",
+ "type": "text",
+ "placeholders": {}
+ },
+ "I have a Canvas account": "ฉันมีบัญชี Canvas",
+ "@I have a Canvas account": {
+ "description": "Option to select for users that have a canvas account",
+ "type": "text",
+ "placeholders": {}
+ },
+ "I don't have a Canvas account": "ฉันไม่มีบัญชี Canvas",
+ "@I don't have a Canvas account": {
+ "description": "Option to select for users that don't have a canvas account",
+ "type": "text",
+ "placeholders": {}
+ },
+ "Create Account": "จัดทำบัญชีผู้ใช้",
+ "@Create Account": {
+ "description": "Button text for account creation confirmation",
+ "type": "text",
+ "placeholders": {}
+ },
+ "Full Name": "ชื่อนามสกุล",
+ "@Full Name": {
+ "type": "text",
+ "placeholders": {}
+ },
+ "Email Address": "อีเมลแอดเดรส",
+ "@Email Address": {
+ "type": "text",
+ "placeholders": {}
+ },
+ "Password": "รหัสผ่าน",
+ "@Password": {
+ "type": "text",
+ "placeholders": {}
+ },
+ "Full Name…": "ชื่อนามสกุล...",
+ "@Full Name…": {
+ "description": "hint label for inside form field",
+ "type": "text",
+ "placeholders": {}
+ },
+ "Email…": "อีเมล...",
+ "@Email…": {
+ "description": "hint label for inside form field",
+ "type": "text",
+ "placeholders": {}
+ },
+ "Password…": "รหัสผ่าน...",
+ "@Password…": {
+ "description": "hint label for inside form field",
+ "type": "text",
+ "placeholders": {}
+ },
+ "Please enter full name": "กรุณาระบุชื่อและนามสกุล",
+ "@Please enter full name": {
+ "description": "Error message for form field",
+ "type": "text",
+ "placeholders": {}
+ },
+ "Please enter an email address": "กรุณากรอกอีเมลแอดเดรส",
+ "@Please enter an email address": {
+ "description": "Error message for form field",
+ "type": "text",
+ "placeholders": {}
+ },
+ "Please enter a valid email address": "กรุณากรอกอีเมลแอดเดรสที่ถูกต้อง",
+ "@Please enter a valid email address": {
+ "description": "Error message for form field",
+ "type": "text",
+ "placeholders": {}
+ },
+ "Password is required": "ต้องระบุรหัสผ่าน",
+ "@Password is required": {
+ "description": "Error message for form field",
+ "type": "text",
+ "placeholders": {}
+ },
+ "Password must contain at least 8 characters": "รหัสผ่านต้องยาวอย่างน้อย 8 ตัวอักษร",
+ "@Password must contain at least 8 characters": {
+ "description": "Error message for form field",
+ "type": "text",
+ "placeholders": {}
+ },
+ "qrCreateAccountTos": "หลังจากกดเลือก “จัดทำบัญชีผู้ใช้” คุณยินยอมภายใต้ {termsOfService} และรับทราบเกี่ยวกับ {privacyPolicy}",
+ "@qrCreateAccountTos": {
+ "description": "The text show on the account creation screen",
+ "type": "text",
+ "placeholders": {
+ "termsOfService": {},
+ "privacyPolicy": {}
+ }
+ },
+ "Terms of Service": "เงื่อนไขการให้บริการ",
+ "@Terms of Service": {
+ "description": "Label for the Canvas Terms of Service agreement. This will be used in the qrCreateAccountTos text and will be highlighted and clickable",
+ "type": "text",
+ "placeholders": {}
+ },
+ "Privacy Policy": "นโยบายความเป็นส่วนตัว",
+ "@Privacy Policy": {
+ "description": "Label for the Canvas Privacy Policy agreement. This will be used in the qrCreateAccountTos text and will be highlighted and clickable",
+ "type": "text",
+ "placeholders": {}
+ },
+ "View the Privacy Policy": "ดูนโยบายความเป็นส่วนตัว",
+ "@View the Privacy Policy": {
+ "type": "text",
+ "placeholders": {}
+ },
+ "Already have an account? ": "มีบัญชีอยู่แล้วหรือไม่ ",
+ "@Already have an account? ": {
+ "description": "Part of multiline text span, includes AccountSignIn1-2, in that order",
+ "type": "text",
+ "placeholders": {}
+ },
+ "Sign In": "ลงชื่อเข้าใช้",
+ "@Sign In": {
+ "description": "Part of multiline text span, includes AccountSignIn1-2, in that order",
+ "type": "text",
+ "placeholders": {}
+ },
+ "Hide Password": "ซ่อนรหัสผ่าน",
+ "@Hide Password": {
+ "description": "content description for password hide button",
+ "type": "text",
+ "placeholders": {}
+ },
+ "Show Password": "แสดงรหัสผ่าน",
+ "@Show Password": {
+ "description": "content description for password show button",
+ "type": "text",
+ "placeholders": {}
+ },
+ "Terms of Service Link": "ลิงค์เงื่อนไขการให้บริการ",
+ "@Terms of Service Link": {
+ "description": "content description for terms of service link",
+ "type": "text",
+ "placeholders": {}
+ },
+ "Privacy Policy Link": "ลิงค์นโยบายความเป็นส่วนตัว",
+ "@Privacy Policy Link": {
+ "description": "content description for privacy policy link",
+ "type": "text",
+ "placeholders": {}
+ },
+ "Event": "กิจกรรม",
+ "@Event": {
+ "description": "Title for the event details screen",
+ "type": "text",
+ "placeholders": {}
+ },
+ "Date": "วันที่",
+ "@Date": {
+ "description": "Label for the event date",
+ "type": "text",
+ "placeholders": {}
+ },
+ "Location": "ตำแหน่ง",
+ "@Location": {
+ "description": "Label for the location information",
+ "type": "text",
+ "placeholders": {}
+ },
+ "No Location Specified": "ไม่ได้ระบุตำแหน่ง",
+ "@No Location Specified": {
+ "description": "Description for events that do not have a location",
+ "type": "text",
+ "placeholders": {}
+ },
+ "eventTime": "{startAt} - {endAt}",
+ "@eventTime": {
+ "description": "The time the event is happening, example: \"2:00 pm - 4:00 pm\"",
+ "type": "text",
+ "placeholders": {
+ "startAt": {},
+ "endAt": {}
+ }
+ },
+ "Set a date and time to be notified of this event.": "กำหนดวันที่และเวลาที่จะแจ้งเกี่ยวกับกิจกรรมนี้",
+ "@Set a date and time to be notified of this event.": {
+ "description": "Description for row to set event reminders",
+ "type": "text",
+ "placeholders": {}
+ },
+ "You will be notified about this event on…": "คุณจะได้รรับแจ้งเกี่ยวกับกิจกรรมนี้เมื่อ...",
+ "@You will be notified about this event on…": {
+ "description": "Description for when an event reminder is set",
+ "type": "text",
+ "placeholders": {}
+ },
+ "Share Your Love for the App": "แบ่งปันความชื่นชอบที่คุณมีเกี่ยวกับแอพ",
+ "@Share Your Love for the App": {
+ "description": "Label for option to open the app store",
+ "type": "text",
+ "placeholders": {}
+ },
+ "Tell us about your favorite parts of the app": "บอกให้เราทราบเกี่ยวกับส่วนที่คุณชอบมากที่สุดเกี่ยวกับแอพ",
+ "@Tell us about your favorite parts of the app": {
+ "description": "Description for option to open the app store",
+ "type": "text",
+ "placeholders": {}
+ },
+ "Legal": "ประเด็นทางกฎหมาย",
+ "@Legal": {
+ "description": "Label for legal information option",
+ "type": "text",
+ "placeholders": {}
+ },
+ "Privacy policy, terms of use, open source": "นโยบายความเป็นส่วนตัว, เงื่อนไขการใช้งาน, สาธารณะ",
+ "@Privacy policy, terms of use, open source": {
+ "description": "Description for legal information option",
+ "type": "text",
+ "placeholders": {}
+ },
+ "Idea for Canvas Parent App [Android]": "แนวคิดสำหรับแอพ Canvas Parent [Android]",
+ "@Idea for Canvas Parent App [Android]": {
+ "description": "The subject for the email to request a feature",
+ "type": "text",
+ "placeholders": {}
+ },
+ "The following information will help us better understand your idea:": "ข้อมูลต่อไปนี้จะช่วยให้เราเข้าใจเกี่ยวกับแนวคิดของคุณได้ดียิ่งขึ้น:",
+ "@The following information will help us better understand your idea:": {
+ "description": "The header for the users information that is attached to a feature request",
+ "type": "text",
+ "placeholders": {}
+ },
+ "Domain:": "โดเมน:",
+ "@Domain:": {
+ "description": "The label for the Canvas domain of the logged in user",
+ "type": "text",
+ "placeholders": {}
+ },
+ "User ID:": "ID ผู้ใช้:",
+ "@User ID:": {
+ "description": "The label for the Canvas user ID of the logged in user",
+ "type": "text",
+ "placeholders": {}
+ },
+ "Email:": "อีเมล:",
+ "@Email:": {
+ "description": "The label for the eamil of the logged in user",
+ "type": "text",
+ "placeholders": {}
+ },
+ "Locale:": "พื้นที่:",
+ "@Locale:": {
+ "description": "The label for the locale of the logged in user",
+ "type": "text",
+ "placeholders": {}
+ },
+ "Terms of Use": "เงื่อนไขการใช้งาน",
+ "@Terms of Use": {
+ "description": "Label for the terms of use",
+ "type": "text",
+ "placeholders": {}
+ },
+ "Canvas on GitHub": "Canvas on GitHub",
+ "@Canvas on GitHub": {
+ "description": "Label for the button that opens the Canvas project on GitHub's website",
+ "type": "text",
+ "placeholders": {}
+ },
+ "There was a problem loading the Terms of Use": "มีปัญหาในการโหลดเงื่อนไขการใช้งาน",
+ "@There was a problem loading the Terms of Use": {
+ "type": "text",
+ "placeholders": {}
+ },
+ "Device": "อุปกรณ์",
+ "@Device": {
+ "description": "Label used for device manufacturer/model in the error report",
+ "type": "text",
+ "placeholders": {}
+ },
+ "OS Version": "เวอร์ชั่น OS",
+ "@OS Version": {
+ "description": "Label used for device operating system version in the error report",
+ "type": "text",
+ "placeholders": {}
+ },
+ "Version Number": "เลขเวอร์ชั่น",
+ "@Version Number": {
+ "description": "Label used for the app version number in the error report",
+ "type": "text",
+ "placeholders": {}
+ },
+ "Report A Problem": "แจ้งปัญหา",
+ "@Report A Problem": {
+ "description": "Title used for generic dialog to report problems",
+ "type": "text",
+ "placeholders": {}
+ },
+ "Subject": "วิชา",
+ "@Subject": {
+ "description": "Label used for Subject text field",
+ "type": "text",
+ "placeholders": {}
+ },
+ "A subject is required.": "ต้องระบุหัวเรื่อง",
+ "@A subject is required.": {
+ "description": "Error shown when the subject field is empty",
+ "type": "text",
+ "placeholders": {}
+ },
+ "An email address is required.": "ต้องระบุอีเมลแอดเดรส",
+ "@An email address is required.": {
+ "description": "Error shown when the email field is empty",
+ "type": "text",
+ "placeholders": {}
+ },
+ "Description": "รายละเอียด",
+ "@Description": {
+ "description": "Label used for Description text field",
+ "type": "text",
+ "placeholders": {}
+ },
+ "A description is required.": "ต้องระบุรายละเอียด",
+ "@A description is required.": {
+ "description": "Error shown when the description field is empty",
+ "type": "text",
+ "placeholders": {}
+ },
+ "How is this affecting you?": "สิ่งนี้มีผลกับคุณอย่างไร",
+ "@How is this affecting you?": {
+ "description": "Label used for the dropdown to select how severe the issue is",
+ "type": "text",
+ "placeholders": {}
+ },
+ "send": "ส่ง",
+ "@send": {
+ "description": "Label used for send button when reporting a problem",
+ "type": "text",
+ "placeholders": {}
+ },
+ "Just a casual question, comment, idea, suggestion…": "แค่คำถาม ความเห็น แนวคิดหรือข้อเสนอแนะทั่ว ๆ ไป...",
+ "@Just a casual question, comment, idea, suggestion…": {
+ "type": "text",
+ "placeholders": {}
+ },
+ "I need some help but it's not urgent.": "ฉันต้องการความช่วยเหลือ แต่ไม่เร่งด่วนอะไร",
+ "@I need some help but it's not urgent.": {
+ "type": "text",
+ "placeholders": {}
+ },
+ "Something's broken but I can work around it to get what I need done.": "มีบางอย่างไม่ถูกต้อง แต่ฉันสามารถแก้ไขได้",
+ "@Something's broken but I can work around it to get what I need done.": {
+ "type": "text",
+ "placeholders": {}
+ },
+ "I can't get things done until I hear back from you.": "ฉันไม่สามารถดำเนินการใด ๆ ได้จนกว่าจะได้รับการติดต่อกลับจากคุณ",
+ "@I can't get things done until I hear back from you.": {
+ "type": "text",
+ "placeholders": {}
+ },
+ "EXTREME CRITICAL EMERGENCY!!": "กรณีฉุกเฉินอย่างยิ่ง!!",
+ "@EXTREME CRITICAL EMERGENCY!!": {
+ "type": "text",
+ "placeholders": {}
+ },
+ "Not Graded": "ไม่ได้ลงเกรด",
+ "@Not Graded": {
+ "description": "Description for an assignment has not been graded.",
+ "type": "text",
+ "placeholders": {}
+ },
+ "Login flow: Normal": "โครงสร้างการล็อกอิน: ปกติ",
+ "@Login flow: Normal": {
+ "description": "Description for the normal login flow",
+ "type": "text",
+ "placeholders": {}
+ },
+ "Login flow: Canvas": "โครงสร้างการล็อกอิน: Canvas",
+ "@Login flow: Canvas": {
+ "description": "Description for the Canvas login flow",
+ "type": "text",
+ "placeholders": {}
+ },
+ "Login flow: Site Admin": "โครงสร้างการล็อกอิน: ผู้ดูแลไซต์",
+ "@Login flow: Site Admin": {
+ "description": "Description for the Site Admin login flow",
+ "type": "text",
+ "placeholders": {}
+ },
+ "Login flow: Skip mobile verify": "โครงสร้างการล็อกอิน: ข้ามการยืนยันผ่านอุปกรณ์พกพา",
+ "@Login flow: Skip mobile verify": {
+ "description": "Description for the login flow that skips domain verification for mobile",
+ "type": "text",
+ "placeholders": {}
+ },
+ "Act As User": "ดำเนินการในฐานะผู้ใช้",
+ "@Act As User": {
+ "description": "Label for the button that allows the user to act (masquerade) as another user",
+ "type": "text",
+ "placeholders": {}
+ },
+ "Stop Acting as User": "หยุดดำเนินการในฐานะผู้ใช้",
+ "@Stop Acting as User": {
+ "description": "Label for the button that allows the user to stop acting (masquerading) as another user",
+ "type": "text",
+ "placeholders": {}
+ },
+ "actingAsUser": "กำลังกำลังดำเนินการในฐานะ {userName}",
+ "@actingAsUser": {
+ "description": "Message shown while acting (masquerading) as another user",
+ "type": "text",
+ "placeholders": {
+ "userName": {}
+ }
+ },
+ "\"Act as\" is essentially logging in as this user without a password. You will be able to take any action as if you were this user, and from other users' points of views, it will be as if this user performed them. However, audit logs record that you were the one who performed the actions on behalf of this user.": "“ดำเนินการในฐานะ” เป็นการล็อกอินเป็นผู้ใช้รายนี้โดยไม่มีรหัสผ่าน คุณสามารถดำเนินการใด ๆ ก็ได้เสมือนเป็นผู้ใช้รายนี้ และผู้ใช้อื่น ๆ จะเข้าใจว่าผู้ใช้รายนี้เป็นผู้ดำเนินการ อย่างไรก็ตาม บันทึกประวัติจะมีจัดทำไว้เพื่อแจ้งว่าคุณเป็นบุคคลที่ดำเนินการในนามของผู้ใช้รายนี้",
+ "@\"Act as\" is essentially logging in as this user without a password. You will be able to take any action as if you were this user, and from other users' points of views, it will be as if this user performed them. However, audit logs record that you were the one who performed the actions on behalf of this user.": {
+ "type": "text",
+ "placeholders": {}
+ },
+ "Domain": "โดเมน",
+ "@Domain": {
+ "description": "Text field hint for domain url input",
+ "type": "text",
+ "placeholders": {}
+ },
+ "You must enter a valid domain": "คุณจะต้องกรอกโดเมนที่ถูกต้อง",
+ "@You must enter a valid domain": {
+ "description": "Message displayed for domain input error",
+ "type": "text",
+ "placeholders": {}
+ },
+ "User ID": "ID ผู้ใช้",
+ "@User ID": {
+ "description": "Text field hint for user ID input",
+ "type": "text",
+ "placeholders": {}
+ },
+ "You must enter a user id": "คุณจะต้องกรอก id ผู้ใช้",
+ "@You must enter a user id": {
+ "description": "Message displayed for user Id input error",
+ "type": "text",
+ "placeholders": {}
+ },
+ "There was an error trying to act as this user. Please check the Domain and User ID and try again.": "มีข้อผิดพลาดในการดำเนินการในฐานะผู้ใช้รายนี้ กรุณาตรวจสอบโดเมนและ ID ผู้ใช้ และลองใหม่อีกครั้ง",
+ "@There was an error trying to act as this user. Please check the Domain and User ID and try again.": {
+ "type": "text",
+ "placeholders": {}
+ },
+ "endMasqueradeMessage": "คุณจะหยุดดำเนินการในฐานะ {userName} และกลับไปที่บัญชีเดิมของคุณ",
+ "@endMasqueradeMessage": {
+ "description": "Confirmation message displayed when the user wants to stop acting (masquerading) as another user",
+ "type": "text",
+ "placeholders": {
+ "userName": {}
+ }
+ },
+ "endMasqueradeLogoutMessage": "คุณจะหยุดดำเนินการในฐานะ {userName} และออกจากระบบ",
+ "@endMasqueradeLogoutMessage": {
+ "description": "Confirmation message displayed when the user wants to stop acting (masquerading) as another user and will be logged out.",
+ "type": "text",
+ "placeholders": {
+ "userName": {}
+ }
+ },
+ "How are we doing?": "เราเป็นอย่างไรบ้าง",
+ "@How are we doing?": {
+ "description": "Title for dialog asking user to rate the app out of 5 stars.",
+ "type": "text",
+ "placeholders": {}
+ },
+ "Don't show again": "ไม่ต้องแสดงอีก",
+ "@Don't show again": {
+ "description": "Button to prevent the rating dialog from showing again.",
+ "type": "text",
+ "placeholders": {}
+ },
+ "What can we do better?": "เราสามารถทำอะไรให้ดีกว่านี้",
+ "@What can we do better?": {
+ "description": "Hint text for providing a comment with the rating.",
+ "type": "text",
+ "placeholders": {}
+ },
+ "Send Feedback": "ส่งความเห็น",
+ "@Send Feedback": {
+ "description": "Button to send rating with feedback",
+ "type": "text",
+ "placeholders": {}
+ },
+ "ratingDialogEmailSubject": "ข้อเสนอแนะสำหรับ Android - Canvas Parent {version}",
+ "@ratingDialogEmailSubject": {
+ "description": "The subject for an email to provide feedback for CanvasParent.",
+ "type": "text",
+ "placeholders": {
+ "version": {}
+ }
+ },
+ "starRating": "{position,plural, =1{{position} ดาว}other{{position} ดาว}}",
+ "@starRating": {
+ "description": "Accessibility label for the 1 stars to 5 stars rating",
+ "type": "text",
+ "placeholders": {
+ "position": {
+ "example": 1
+ }
+ }
+ },
+ "Student Pairing": "การเข้าคู่ผู้เรียน",
+ "@Student Pairing": {
+ "description": "Title for the screen where users can pair to students using a QR code",
+ "type": "text",
+ "placeholders": {}
+ },
+ "Open Canvas Student": "เปิด Canvas Student",
+ "@Open Canvas Student": {
+ "description": "Title for QR pairing tutorial screen instructing users to open the Canvas Student app",
+ "type": "text",
+ "placeholders": {}
+ },
+ "You'll need to open your student's Canvas Student app to continue. Go into Main Menu > Settings > Pair with Observer and scan the QR code you see there.": "คุณจะต้องเปิดแอพ Canvas Student ของผู้เรียนเพื่อดำเนินการต่อ ไปที่เมนูหลัก > ค่าปรับตั้ง > เข้าคู่กับผู้สังเกตการณ์ และสแกนรหัส QR ที่ปรากฏขึ้น",
+ "@You'll need to open your student's Canvas Student app to continue. Go into Main Menu > Settings > Pair with Observer and scan the QR code you see there.": {
+ "description": "Message explaining how QR code pairing works",
+ "type": "text",
+ "placeholders": {}
+ },
+ "Screenshot showing location of pairing QR code generation in the Canvas Student app": "ภาพหน้าจอแสดงตำแหน่งการจัดทำรหัส QR สำหรับเข้าคู่ในแอพ Canvas Student",
+ "@Screenshot showing location of pairing QR code generation in the Canvas Student app": {
+ "description": "Content Description for qr pairing tutorial screenshot",
+ "type": "text",
+ "placeholders": {}
+ },
+ "Expired QR Code": "รหัส QR หมดอายุ",
+ "@Expired QR Code": {
+ "description": "Error title shown when the users scans a QR code that has expired",
+ "type": "text",
+ "placeholders": {}
+ },
+ "The QR code you scanned may have expired. Refresh the code on the student's device and try again.": "รหัส QR ที่คุณสแกนอาจหมดอายุแล้ว รีเฟรชรหัสจากอุปกรณ์ของผู้เรียนแล้วลองใหม่อีกครั้ง",
+ "@The QR code you scanned may have expired. Refresh the code on the student's device and try again.": {
+ "type": "text",
+ "placeholders": {}
+ },
+ "A network error occurred when adding this student. Check your connection and try again.": "เกิดข้อผิดพลาดทางเครือข่ายขณะเพิ่มผู้เรียนนี้ ตรวจสอบการเชื่อมต่อและลองใหม่อีกครั้ง",
+ "@A network error occurred when adding this student. Check your connection and try again.": {
+ "type": "text",
+ "placeholders": {}
+ },
+ "Invalid QR Code": "รหัส QR ไม่ถูกต้อง",
+ "@Invalid QR Code": {
+ "description": "Error title shown when the user scans an invalid QR code",
+ "type": "text",
+ "placeholders": {}
+ },
+ "Incorrect Domain": "โดเมนไม่ถูกต้อง",
+ "@Incorrect Domain": {
+ "description": "Error title shown when the users scane a QR code for a student that belongs to a different domain",
+ "type": "text",
+ "placeholders": {}
+ },
+ "The student you are trying to add belongs to a different school. Log in or create an account with that school to scan this code.": "ผู้เรียนที่คุณพยายามเพิ่มเป็นของสถานศึกษาอื่น ล็อกอินหรือจัดทำบัญชีผู้ใช้กับทางสถานศึกษาดังกล่าวเพื่อสแกนรหัสนี้",
+ "@The student you are trying to add belongs to a different school. Log in or create an account with that school to scan this code.": {
+ "type": "text",
+ "placeholders": {}
+ },
+ "Camera Permission": "สิทธิ์ใช้งานกล้อง",
+ "@Camera Permission": {
+ "description": "Error title shown when the user wans to scan a QR code but has denied the camera permission",
+ "type": "text",
+ "placeholders": {}
+ },
+ "This will unpair and remove all enrollments for this student from your account.": "นี่เป็นการเลิกการเข้าคู่และลบการลงทะเบียนทั้งหมดสำหรับผู้เรียนนี้จากบัญชีของคุณ",
+ "@This will unpair and remove all enrollments for this student from your account.": {
+ "description": "Confirmation message shown when the user tries to delete a student from their account",
+ "type": "text",
+ "placeholders": {}
+ },
+ "There was a problem removing this student from your account. Please check your connection and try again.": "มีปัญหาในการลบผู้เรียนนี้จากบัญชีของคุณ กรุณาตรวจสอบการเชื่อมต่อของคุณและลองใหม่อีกครั้ง",
+ "@There was a problem removing this student from your account. Please check your connection and try again.": {
+ "type": "text",
+ "placeholders": {}
+ },
+ "Cancel": "ยกเลิก",
+ "@Cancel": {
+ "type": "text",
+ "placeholders": {}
+ },
+ "next": "ถัดไป",
+ "@next": {
+ "type": "text",
+ "placeholders": {}
+ },
+ "ok": "OK",
+ "@ok": {
+ "type": "text",
+ "placeholders": {}
+ },
+ "Yes": "ใช่",
+ "@Yes": {
+ "type": "text",
+ "placeholders": {}
+ },
+ "No": "ไม่",
+ "@No": {
+ "type": "text",
+ "placeholders": {}
+ },
+ "Retry": "ลองใหม่",
+ "@Retry": {
+ "type": "text",
+ "placeholders": {}
+ },
+ "Delete": "ลบ",
+ "@Delete": {
+ "description": "Label used for general delete/remove actions",
+ "type": "text",
+ "placeholders": {}
+ },
+ "Done": "เสร็จสิ้น",
+ "@Done": {
+ "description": "Label for general done/finished actions",
+ "type": "text",
+ "placeholders": {}
+ },
+ "Refresh": "รีเฟรช",
+ "@Refresh": {
+ "description": "Label for button to refresh data from the web",
+ "type": "text",
+ "placeholders": {}
+ },
+ "View Description": "ดูรายละเอียด",
+ "@View Description": {
+ "description": "Button to view the description for an event or assignment",
+ "type": "text",
+ "placeholders": {}
+ },
+ "expanded": "ขยายแล้ว",
+ "@expanded": {
+ "description": "Description for the accessibility reader for list groups that are expanded",
+ "type": "text",
+ "placeholders": {}
+ },
+ "collapsed": "ย่อแล้ว",
+ "@collapsed": {
+ "description": "Description for the accessibility reader for list groups that are expanded",
+ "type": "text",
+ "placeholders": {}
+ },
+ "An unexpected error occurred": "เกิดข้อผิดพลาดที่ไม่คาดคิด",
+ "@An unexpected error occurred": {
+ "type": "text",
+ "placeholders": {}
+ },
+ "No description": "ไม่มีรายละเอียด",
+ "@No description": {
+ "description": "Message used when the assignment has no description",
+ "type": "text",
+ "placeholders": {}
+ },
+ "Launch External Tool": "เรียกใช้ชุดเครื่องมือจากภายนอก",
+ "@Launch External Tool": {
+ "description": "Button text added to webviews to let users open external tools in their browser",
+ "type": "text",
+ "placeholders": {}
+ },
+ "Interactions on this page are limited by your institution.": "การโต้ตอบในหน้าเพจนี้จำกัดไว้สำหรับสถาบันของคุณเท่านั้น",
+ "@Interactions on this page are limited by your institution.": {
+ "description": "Message describing how the webview has limited access due to an instution setting",
+ "type": "text",
+ "placeholders": {}
+ },
+ "dateAtTime": "{date} ที่ {time}",
+ "@dateAtTime": {
+ "description": "The string to format dates",
+ "type": "text",
+ "placeholders": {
+ "date": {},
+ "time": {}
+ }
+ },
+ "dueDateAtTime": "ครบกำหนด {date} เมื่อ {time}",
+ "@dueDateAtTime": {
+ "description": "The string to format due dates",
+ "type": "text",
+ "placeholders": {
+ "date": {},
+ "time": {}
+ }
+ },
+ "No Due Date": "ไม่มีวันครบกำหนด",
+ "@No Due Date": {
+ "description": "Label for assignments that do not have a due date",
+ "type": "text",
+ "placeholders": {}
+ },
+ "Filter": "ตัวกรอง",
+ "@Filter": {
+ "description": "Label for buttons to filter what items are visible",
+ "type": "text",
+ "placeholders": {}
+ },
+ "unread": "ไม่ได้อ่าน",
+ "@unread": {
+ "description": "Label for things that are marked as unread",
+ "type": "text",
+ "placeholders": {}
+ },
+ "unreadCount": "{count} ที่ไม่ได้อ่าน",
+ "@unreadCount": {
+ "description": "Formatted string for when there are a number of unread items",
+ "type": "text",
+ "placeholders": {
+ "count": {}
+ }
+ },
+ "badgeNumberPlus": "{count}+",
+ "@badgeNumberPlus": {
+ "description": "Formatted string for when too many items are being notified in a badge, generally something like: 99+",
+ "type": "text",
+ "placeholders": {
+ "count": {}
+ }
+ },
+ "There was an error loading this announcement": "มีข้อผิดพลาดในการโหลดประกาศนี้",
+ "@There was an error loading this announcement": {
+ "description": "Message shown when an announcement detail screen fails to load",
+ "type": "text",
+ "placeholders": {}
+ },
+ "Network error": "ข้อผิดพลาดเครือข่าย",
+ "@Network error": {
+ "type": "text",
+ "placeholders": {}
+ },
+ "Under Construction": "อยู่ระหว่างจัดทำ",
+ "@Under Construction": {
+ "type": "text",
+ "placeholders": {}
+ },
+ "We are currently building this feature for your viewing pleasure.": "เรากำลังจัดทำคุณสมบัตินี้เพื่อให้คุณรับชมได้อย่างสะดวก",
+ "@We are currently building this feature for your viewing pleasure.": {
+ "type": "text",
+ "placeholders": {}
+ },
+ "Request Login Help Button": "ปุ่มขอความช่วยเหลือในการล็อกอิน",
+ "@Request Login Help Button": {
+ "description": "Accessibility hint for button that opens help dialog for a login help request",
+ "type": "text",
+ "placeholders": {}
+ },
+ "Request Login Help": "ขอความช่วยเหลือในการล็อกอิน",
+ "@Request Login Help": {
+ "description": "Title of help dialog for a login help request",
+ "type": "text",
+ "placeholders": {}
+ },
+ "I'm having trouble logging in": "ฉันมีปัญหาในการล็อกอิน",
+ "@I'm having trouble logging in": {
+ "description": "Subject of help dialog for a login help request",
+ "type": "text",
+ "placeholders": {}
+ },
+ "An error occurred when trying to display this link": "เกิดข้อผิดพลาดขณะพยายามแสดงลิงค์นี้",
+ "@An error occurred when trying to display this link": {
+ "description": "Error message shown when a link can't be opened",
+ "type": "text",
+ "placeholders": {}
+ },
+ "We are unable to display this link, it may belong to an institution you currently aren't logged in to.": "เราไม่สามารถแสดงลิงค์นี้ เนื่องจากอาจเป็นของสถาบันที่คุณไม่ได้ล็อกอินอยู่ในปัจจุบัน",
+ "@We are unable to display this link, it may belong to an institution you currently aren't logged in to.": {
+ "description": "Description for error page shown when clicking a link",
+ "type": "text",
+ "placeholders": {}
+ },
+ "Link Error": "ข้อผิดพลาดลิงค์",
+ "@Link Error": {
+ "description": "Title for error page shown when clicking a link",
+ "type": "text",
+ "placeholders": {}
+ },
+ "Open In Browser": "เปิดในเบราเซอร์",
+ "@Open In Browser": {
+ "description": "Text for button to open a link in the browswer",
+ "type": "text",
+ "placeholders": {}
+ },
+ "You'll find the QR code on the web in your account profile. Click 'QR for Mobile Login' in the list.": "คุณจะพบรหัส QR ในเว็บจากโพรไฟล์บัญชีของคุณ คลิกที่ “QR สำหรับล็อกอินผ่านอุปกรณ์พกพา” จากรายการ",
+ "@You'll find the QR code on the web in your account profile. Click 'QR for Mobile Login' in the list.": {
+ "description": "Text for qr login tutorial screen",
+ "type": "text",
+ "placeholders": {}
+ },
+ "Locate QR Code": "ระบุตำแหน่งรหัส QR",
+ "@Locate QR Code": {
+ "description": "Text for qr login button",
+ "type": "text",
+ "placeholders": {}
+ },
+ "Please scan a QR code generated by Canvas": "กรุณาสแกนรหัส QR ที่จัดทำโดย Canvas",
+ "@Please scan a QR code generated by Canvas": {
+ "description": "Text for qr login error with incorrect qr code",
+ "type": "text",
+ "placeholders": {}
+ },
+ "There was an error logging in. Please generate another QR Code and try again.": "มีข้อผิดพลาดในการล็อกอิน กรุณาจัดทำรหัส QR อื่นและลองใหม่อีกครั้ง",
+ "@There was an error logging in. Please generate another QR Code and try again.": {
+ "description": "Text for qr login error",
+ "type": "text",
+ "placeholders": {}
+ },
+ "Screenshot showing location of QR code generation in browser": "ภาพหน้าจอแสดงตำแหน่งการจัดทำรหัส QR ในเบราเซอร์",
+ "@Screenshot showing location of QR code generation in browser": {
+ "description": "Content Description for qr login tutorial screenshot",
+ "type": "text",
+ "placeholders": {}
+ },
+ "QR scanning requires camera access": "การสแกน QR จะต้องใช้กล้อง",
+ "@QR scanning requires camera access": {
+ "description": "placeholder for camera error for QR code scan",
+ "type": "text",
+ "placeholders": {}
+ }
+}
\ No newline at end of file
diff --git a/apps/flutter_parent/lib/models/planner_item.dart b/apps/flutter_parent/lib/models/planner_item.dart
index c0987bdd5a..c239515a55 100644
--- a/apps/flutter_parent/lib/models/planner_item.dart
+++ b/apps/flutter_parent/lib/models/planner_item.dart
@@ -45,6 +45,7 @@ abstract class PlannerItem implements Built {
Plannable get plannable;
+ @nullable
@BuiltValueField(wireName: 'plannable_date')
DateTime get plannableDate;
diff --git a/apps/flutter_parent/lib/models/planner_item.g.dart b/apps/flutter_parent/lib/models/planner_item.g.dart
index b9da39dcbc..869868e315 100644
--- a/apps/flutter_parent/lib/models/planner_item.g.dart
+++ b/apps/flutter_parent/lib/models/planner_item.g.dart
@@ -24,9 +24,6 @@ class _$PlannerItemSerializer implements StructuredSerializer {
'plannable',
serializers.serialize(object.plannable,
specifiedType: const FullType(Plannable)),
- 'plannable_date',
- serializers.serialize(object.plannableDate,
- specifiedType: const FullType(DateTime)),
];
result.add('course_id');
if (object.courseId == null) {
@@ -49,6 +46,13 @@ class _$PlannerItemSerializer implements StructuredSerializer {
result.add(serializers.serialize(object.contextName,
specifiedType: const FullType(String)));
}
+ result.add('plannable_date');
+ if (object.plannableDate == null) {
+ result.add(null);
+ } else {
+ result.add(serializers.serialize(object.plannableDate,
+ specifiedType: const FullType(DateTime)));
+ }
result.add('submissions');
if (object.submissionStatusRaw == null) {
result.add(null);
@@ -169,9 +173,6 @@ class _$PlannerItem extends PlannerItem {
if (plannable == null) {
throw new BuiltValueNullFieldError('PlannerItem', 'plannable');
}
- if (plannableDate == null) {
- throw new BuiltValueNullFieldError('PlannerItem', 'plannableDate');
- }
}
@override
diff --git a/apps/flutter_parent/lib/screens/calendar/planner_fetcher.dart b/apps/flutter_parent/lib/screens/calendar/planner_fetcher.dart
index cfa1edf095..3cd13e7c91 100644
--- a/apps/flutter_parent/lib/screens/calendar/planner_fetcher.dart
+++ b/apps/flutter_parent/lib/screens/calendar/planner_fetcher.dart
@@ -189,8 +189,10 @@ class PlannerFetcher extends ChangeNotifier {
dayItems[dayKeyForYearMonthDay(date.year, date.month, i)] = [];
}
items.forEach((item) {
- String dayKey = dayKeyForDate(item.plannableDate.toLocal());
- dayItems[dayKey].add(item);
+ if (item.plannableDate != null) {
+ String dayKey = dayKeyForDate(item.plannableDate.toLocal());
+ dayItems[dayKey].add(item);
+ }
});
dayItems.forEach((dayKey, items) {
diff --git a/apps/flutter_parent/lib/screens/dashboard/dashboard_screen.dart b/apps/flutter_parent/lib/screens/dashboard/dashboard_screen.dart
index 38e3a46e0f..d59fbe3df2 100644
--- a/apps/flutter_parent/lib/screens/dashboard/dashboard_screen.dart
+++ b/apps/flutter_parent/lib/screens/dashboard/dashboard_screen.dart
@@ -443,7 +443,7 @@ class DashboardState extends State {
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
// Header
_navDrawerHeader(user),
-
+ Divider(),
// Tiles (Inbox, Manage Students, Sign Out, etc)
Expanded(
child: _navDrawerItemsList(),
@@ -573,41 +573,40 @@ class DashboardState extends State {
}
_navDrawerHeader(User user) => Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Padding(
- padding: const EdgeInsets.fromLTRB(16, 16, 0, 12),
- child: Avatar(user.avatarUrl, name: user.shortName, radius: 28),
- ),
- Padding(
- padding: const EdgeInsets.symmetric(horizontal: 16),
- child: UserName.fromUser(user, style: TextStyle(fontSize: 20.0, fontWeight: FontWeight.bold)),
- ),
- Padding(
- padding: const EdgeInsets.symmetric(horizontal: 16),
- child: Text(
- user?.primaryEmail ?? '',
- style: Theme.of(context).textTheme.caption,
- ),
- ),
- SizedBox(height: 36)
- ],
- );
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Padding(
+ padding: const EdgeInsets.fromLTRB(24, 16, 0, 8),
+ child: Avatar(user.avatarUrl, name: user.shortName, radius: 40),
+ ),
+ Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 24),
+ child: UserName.fromUser(user, style: TextStyle(fontSize: 16.0, fontWeight: FontWeight.bold)),
+ ),
+ Padding(
+ padding: const EdgeInsets.fromLTRB(24, 4, 24, 16),
+ child: Text(
+ user?.primaryEmail ?? '',
+ style: Theme.of(context).textTheme.caption,
+ overflow: TextOverflow.fade,
+ ),
+ )
+ ],
+ );
Widget _navDrawerItemsList() {
var items = [
_navDrawerInbox(),
_navDrawerManageStudents(),
_navDrawerSettings(),
+ Divider(),
_navDrawerHelp(),
_navDrawerSwitchUsers(),
_navDrawerLogOut(),
- null // to get trailing divider
];
- return ListView.separated(
+ return ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) => items[index],
- separatorBuilder: (context, index) => const Divider(height: 0, indent: 16),
);
}
@@ -615,6 +614,10 @@ class DashboardState extends State {
_navDrawerInbox() => ListTile(
title: Text(L10n(context).inbox),
onTap: () => _navigateToInbox(context),
+ leading: Padding(
+ padding: const EdgeInsets.only(left: 8.0),
+ child: SvgPicture.asset('assets/svg/ic_inbox.svg', height: 24, width: 24),
+ ),
trailing: NumberBadge(
listenable: _interactor.getInboxCountNotifier(),
options: BadgeOptions(maxCount: null),
@@ -622,18 +625,39 @@ class DashboardState extends State {
),
);
- _navDrawerManageStudents() =>
- ListTile(title: Text(L10n(context).manageStudents), onTap: () => _navigateToManageStudents(context));
+ _navDrawerManageStudents() => ListTile(
+ title: Text(L10n(context).manageStudents),
+ onTap: () => _navigateToManageStudents(context),
+ leading: Padding(
+ padding: const EdgeInsets.only(left: 8.0),
+ child: SvgPicture.asset('assets/svg/ic_manage_student.svg', height: 24, width: 24),
+ ),
+ );
- _navDrawerSettings() => ListTile(title: Text(L10n(context).settings), onTap: () => _navigateToSettings(context));
+ _navDrawerSettings() => ListTile(
+ title: Text(L10n(context).settings),
+ onTap: () => _navigateToSettings(context),
+ leading: Padding(
+ padding: const EdgeInsets.only(left: 8.0),
+ child: SvgPicture.asset('assets/svg/ic_settings.svg', height: 24, width: 24),
+ ),
+ );
_navDrawerHelp() => ListTile(
title: Text(L10n(context).help),
onTap: () => _navigateToHelp(context),
+ leading: Padding(
+ padding: const EdgeInsets.only(left: 8.0),
+ child: SvgPicture.asset('assets/svg/ic_help.svg', height: 24, width: 24),
+ ),
);
_navDrawerLogOut() => ListTile(
title: Text(L10n(context).logOut),
+ leading: Padding(
+ padding: const EdgeInsets.only(left: 8.0),
+ child: SvgPicture.asset('assets/svg/ic_logout.svg', height: 24, width: 24,),
+ ),
onTap: () {
showDialog(
context: context,
@@ -642,11 +666,13 @@ class DashboardState extends State {
content: Text(L10n(context).logoutConfirmation),
actions: [
FlatButton(
- child: Text(MaterialLocalizations.of(context).cancelButtonLabel),
+ child: Text(
+ MaterialLocalizations.of(context).cancelButtonLabel),
onPressed: () => Navigator.of(context).pop(),
),
FlatButton(
- child: Text(MaterialLocalizations.of(context).okButtonLabel),
+ child:
+ Text(MaterialLocalizations.of(context).okButtonLabel),
onPressed: () => _performLogOut(context),
)
],
@@ -658,11 +684,18 @@ class DashboardState extends State {
_navDrawerSwitchUsers() => ListTile(
title: Text(L10n(context).switchUsers),
+ leading: Padding(
+ padding: const EdgeInsets.only(left: 8.0),
+ child: SvgPicture.asset('assets/svg/ic_change_user.svg', height: 24, width: 24),
+ ),
onTap: () => _performLogOut(context, switchingUsers: true),
);
_navDrawerActAsUser() => ListTile(
- leading: Icon(CanvasIcons.masquerade),
+ leading: Padding(
+ padding: const EdgeInsets.only(left: 8.0),
+ child: Icon(CanvasIcons.masquerade),
+ ),
title: Text(L10n(context).actAsUser),
onTap: () {
Navigator.of(context).pop();
@@ -671,7 +704,10 @@ class DashboardState extends State {
);
_navDrawerStopActingAsUser() => ListTile(
- leading: Icon(CanvasIcons.masquerade),
+ leading: Padding(
+ padding: const EdgeInsets.only(left: 8.0),
+ child: Icon(CanvasIcons.masquerade),
+ ),
title: Text(L10n(context).stopActAsUser),
onTap: () {
Navigator.of(context).pop();
diff --git a/apps/flutter_parent/plugins/encrypted_shared_preferences/android/build.gradle b/apps/flutter_parent/plugins/encrypted_shared_preferences/android/build.gradle
index fc637c9714..0aaa7b0e81 100644
--- a/apps/flutter_parent/plugins/encrypted_shared_preferences/android/build.gradle
+++ b/apps/flutter_parent/plugins/encrypted_shared_preferences/android/build.gradle
@@ -4,7 +4,7 @@ version '1.0-SNAPSHOT'
buildscript {
repositories {
google()
- jcenter()
+ mavenCentral()
}
dependencies {
@@ -15,7 +15,7 @@ buildscript {
rootProject.allprojects {
repositories {
google()
- jcenter()
+ mavenCentral()
}
}
diff --git a/apps/flutter_parent/plugins/webview_flutter/android/build.gradle b/apps/flutter_parent/plugins/webview_flutter/android/build.gradle
index 23936394ef..1e4b00e7f1 100644
--- a/apps/flutter_parent/plugins/webview_flutter/android/build.gradle
+++ b/apps/flutter_parent/plugins/webview_flutter/android/build.gradle
@@ -4,7 +4,7 @@ version '1.0-SNAPSHOT'
buildscript {
repositories {
google()
- jcenter()
+ mavenCentral()
}
dependencies {
@@ -15,7 +15,7 @@ buildscript {
rootProject.allprojects {
repositories {
google()
- jcenter()
+ mavenCentral()
}
}
diff --git a/apps/flutter_parent/pubspec.yaml b/apps/flutter_parent/pubspec.yaml
index d7f10a384c..e8873f0441 100644
--- a/apps/flutter_parent/pubspec.yaml
+++ b/apps/flutter_parent/pubspec.yaml
@@ -25,7 +25,7 @@ description: Canvas Parent
# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
# Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
-version: 3.3.3+38
+version: 3.3.4+39
module:
androidX: true
diff --git a/apps/student/build.gradle b/apps/student/build.gradle
index 39139b8c4f..96fdb91be5 100644
--- a/apps/student/build.gradle
+++ b/apps/student/build.gradle
@@ -24,7 +24,9 @@ apply plugin: 'kotlin-kapt'
apply plugin: 'com.google.firebase.crashlytics'
apply from: '../../gradle/coverage.gradle'
apply plugin: 'com.squareup.sqldelight'
+apply plugin: 'dagger.hilt.android.plugin'
+def updatePriority = 2
def coverageEnabled = project.hasProperty('coverage')
if (coverageEnabled) {
@@ -57,12 +59,12 @@ android {
applicationId "com.instructure.candroid"
minSdkVersion Versions.MIN_SDK
targetSdkVersion Versions.TARGET_SDK
- versionCode = 225
- versionName = '6.10.0'
+ versionCode = 231
+ versionName = '6.14.1'
vectorDrawables.useSupportLibrary = true
multiDexEnabled = true
- testInstrumentationRunner 'com.instructure.canvas.espresso.CanvasRunner'
+ testInstrumentationRunner 'com.instructure.student.espresso.StudentHiltTestRunner'
testInstrumentationRunnerArguments disableAnalytics: 'true'
buildConfigField "boolean", "IS_TESTING", "false"
@@ -184,8 +186,7 @@ android {
*/
resolutionStrategy.force Libs.ANDROIDX_ANNOTATION
- // Fix for Robolectric 4.x
- resolutionStrategy.force "org.ow2.asm:asm:7.0"
+ resolutionStrategy.force Libs.KOTLIN_COROUTINES_CORE
}
/*
@@ -210,7 +211,7 @@ android {
android,
new MasqueradeUITransformer('com.instructure.student.activity.NavigationActivity.class'),
new PageViewTransformer(),
- new LocaleTransformer(),
+ new LocaleTransformer(project),
new FlutterA11yOffsetTransformer(),
new FlutterTextureDisconnectFix()
)
@@ -220,6 +221,18 @@ android {
sourceCompatibility 1.8
targetCompatibility 1.8
}
+
+ kotlinOptions {
+ jvmTarget = JavaVersion.VERSION_1_8.toString()
+ }
+
+ buildFeatures {
+ dataBinding true
+ }
+
+ hilt {
+ enableTransformForLocalTests = true
+ }
}
dependencies {
@@ -241,11 +254,6 @@ dependencies {
androidTestImplementation project(path: ':espresso')
androidTestImplementation project(':dataseedingapi')
- /* OkHttp Idling Resource */
- implementation('com.jakewharton.espresso:okhttp3-idling-resource:1.0.0') {
- exclude module: 'espresso-idling-resource'
- }
-
/* Unit Test Dependencies */
testImplementation Libs.JUNIT
testImplementation Libs.ROBOLECTRIC
@@ -255,6 +263,7 @@ dependencies {
testImplementation Libs.KOTLIN_COROUTINES_TEST
testImplementation Libs.FIREBASE_CORE
testImplementation Libs.THREETEN_BP
+ testImplementation Libs.ANDROIDX_CORE_TESTING
implementation (Libs.FIREBASE_ANALYTICS) {
transitive = true
@@ -274,15 +283,14 @@ dependencies {
implementation Libs.MOBIUS_EXTRAS
/* Media Handling */
- implementation 'com.makeramen:roundedimageview:2.3.0'
implementation Libs.PHOTO_VIEW
- implementation 'com.airbnb.android:lottie:3.4.1'
+ implementation Libs.LOTTIE
/* Sliding Panel */
- implementation 'com.sothree.slidinguppanel:library:3.4.0'
+ implementation Libs.SLIDING_UP_PANEL
/* Apache Commons */
- implementation 'org.apache.commons:commons-text:1.6'
+ implementation Libs.APACHE_COMMONS_TEXT
/* Support dependencies */
implementation Libs.ANDROIDX_ANNOTATION
@@ -297,16 +305,29 @@ dependencies {
implementation Libs.ANDROIDX_PALETTE
implementation Libs.PLAY_CORE
- /* Job Scheduler */
- implementation Libs.FIREBASE_JOB_DISPATCHER
-
/* Database */
- implementation "com.squareup.sqldelight:android-driver:1.4.3"
+ implementation Libs.SQLDELIGHT
/* Qr Code */
implementation(Libs.JOURNEY_ZXING) { transitive = false }
implementation Libs.ZXING
- implementation "com.google.firebase:firebase-crashlytics-ndk:17.2.1"
+
+ /* AAC */
+ implementation Libs.VIEW_MODEL
+ implementation Libs.LIVE_DATA
+ implementation Libs.VIEW_MODE_SAVED_STATE
+ implementation Libs.FRAGMENT_KTX
+ kapt Libs.LIFECYCLE_COMPILER
+
+ /* DI */
+ implementation Libs.HILT
+ kapt Libs.HILT_COMPILER
+ androidTestImplementation Libs.HILT_TESTING
+ kaptAndroidTestQa Libs.HILT_TESTING_COMPILER
+
+ androidTestImplementation Libs.UI_AUTOMATOR
+
+ implementation Libs.FIREBASE_CRASHLYTICS_NDK
}
// Comment out this line if the reporting logic starts going wonky.
diff --git a/apps/student/flank_landscape.yml b/apps/student/flank_landscape.yml
index ba1199ceb1..8096fd6d79 100644
--- a/apps/student/flank_landscape.yml
+++ b/apps/student/flank_landscape.yml
@@ -12,9 +12,9 @@ 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.StubLandscape
device:
- - model: NexusLowRes
+ - model: Nexus6P
version: 25
locale: en_US
orientation: landscape
diff --git a/apps/student/src/androidTest/java/com/instructure/student/espresso/StudentHiltTestApplication.kt b/apps/student/src/androidTest/java/com/instructure/student/espresso/StudentHiltTestApplication.kt
new file mode 100644
index 0000000000..9d6d33910d
--- /dev/null
+++ b/apps/student/src/androidTest/java/com/instructure/student/espresso/StudentHiltTestApplication.kt
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2021 - 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.espresso
+
+import com.instructure.student.util.BaseAppManager
+import dagger.hilt.android.testing.CustomTestApplication
+
+@CustomTestApplication(BaseAppManager::class)
+interface StudentHiltTestApplication
\ No newline at end of file
diff --git a/apps/student/src/androidTest/java/com/instructure/student/espresso/StudentHiltTestRunner.kt b/apps/student/src/androidTest/java/com/instructure/student/espresso/StudentHiltTestRunner.kt
new file mode 100644
index 0000000000..bb466c5008
--- /dev/null
+++ b/apps/student/src/androidTest/java/com/instructure/student/espresso/StudentHiltTestRunner.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2021 - 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.espresso
+
+import android.app.Application
+import android.content.Context
+import com.instructure.canvas.espresso.CanvasRunner
+import dagger.hilt.android.testing.HiltTestApplication
+
+class StudentHiltTestRunner : CanvasRunner() {
+
+ override fun newApplication(cl: ClassLoader?, className: String?, context: Context?): Application {
+ return super.newApplication(cl, StudentHiltTestApplication_Application::class.java.name, context)
+ }
+}
\ No newline at end of file
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/LoginFindSchoolPageTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/LoginFindSchoolPageTest.kt
index 2166236c9d..3e59511e4a 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/LoginFindSchoolPageTest.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/LoginFindSchoolPageTest.kt
@@ -17,8 +17,10 @@
package com.instructure.student.ui
import com.instructure.student.ui.utils.StudentTest
+import dagger.hilt.android.testing.HiltAndroidTest
import org.junit.Test
+@HiltAndroidTest
class LoginFindSchoolPageTest: StudentTest() {
// Runs live; no MockCanvas
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/LoginLandingPageTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/LoginLandingPageTest.kt
index 2fb1250d39..c4e6f156b2 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/LoginLandingPageTest.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/LoginLandingPageTest.kt
@@ -18,8 +18,10 @@ package com.instructure.student.ui
import com.instructure.student.ui.utils.StudentTest
import com.instructure.espresso.filters.P1
+import dagger.hilt.android.testing.HiltAndroidTest
import org.junit.Test
+@HiltAndroidTest
class LoginLandingPageTest: StudentTest() {
// Runs live; no MockCanvas
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/LoginSignInPageTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/LoginSignInPageTest.kt
index 8808db55fd..a09366509d 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/LoginSignInPageTest.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/LoginSignInPageTest.kt
@@ -18,8 +18,10 @@ package com.instructure.student.ui
import com.instructure.student.ui.utils.StudentTest
import com.instructure.student.ui.utils.enterDomain
+import dagger.hilt.android.testing.HiltAndroidTest
import org.junit.Test
+@HiltAndroidTest
class LoginSignInPageTest: StudentTest() {
// Runs live; no MockCanvas
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 1c28da0a17..f19e8695cb 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
@@ -36,9 +36,11 @@ import com.instructure.student.ui.utils.StudentTest
import com.instructure.student.ui.utils.seedData
import com.instructure.student.ui.utils.tokenLogin
import com.instructure.student.ui.utils.uploadTextFile
+import dagger.hilt.android.testing.HiltAndroidTest
import org.junit.Rule
import org.junit.Test
+@HiltAndroidTest
class AssignmentsE2ETest: StudentTest() {
override fun displaysPageObjects() = Unit
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/CollaborationsE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/CollaborationsE2ETest.kt
index f348b5c731..ddf44b5d4f 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/CollaborationsE2ETest.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/CollaborationsE2ETest.kt
@@ -9,6 +9,7 @@ import com.instructure.student.ui.pages.CollaborationsPage
import com.instructure.student.ui.utils.StudentTest
import com.instructure.student.ui.utils.seedData
import com.instructure.student.ui.utils.tokenLogin
+import dagger.hilt.android.testing.HiltAndroidTest
import org.junit.Test
/**
@@ -16,6 +17,7 @@ import org.junit.Test
* We make no attempt to actually start a collaboration.
* This test could break if changes are made to the web page that we bring up.
*/
+@HiltAndroidTest
class CollaborationsE2ETest: StudentTest() {
override fun displaysPageObjects() {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
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 f42dfe5a3c..9fe0d38fe6 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
@@ -10,8 +10,10 @@ 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
+import dagger.hilt.android.testing.HiltAndroidTest
import org.junit.Test
+@HiltAndroidTest
class ConferencesE2ETest: StudentTest() {
override fun displaysPageObjects() {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/DashboardE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/DashboardE2ETest.kt
index a5e715dbc8..c936147809 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/DashboardE2ETest.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/DashboardE2ETest.kt
@@ -26,9 +26,11 @@ import com.instructure.panda_annotations.TestMetaData
import com.instructure.student.ui.utils.StudentTest
import com.instructure.student.ui.utils.seedData
import com.instructure.student.ui.utils.tokenLogin
+import dagger.hilt.android.testing.HiltAndroidTest
import junit.framework.Assert.assertEquals
import org.junit.Test
+@HiltAndroidTest
class DashboardE2ETest : StudentTest() {
override fun displaysPageObjects() {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/DiscussionsE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/DiscussionsE2ETest.kt
index b7ca7ba5ee..7110e1a383 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/DiscussionsE2ETest.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/DiscussionsE2ETest.kt
@@ -28,8 +28,10 @@ import com.instructure.panda_annotations.TestMetaData
import com.instructure.student.ui.utils.StudentTest
import com.instructure.student.ui.utils.seedData
import com.instructure.student.ui.utils.tokenLogin
+import dagger.hilt.android.testing.HiltAndroidTest
import org.junit.Test
+@HiltAndroidTest
class DiscussionsE2ETest: StudentTest() {
override fun displaysPageObjects() {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/EventsE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/EventsE2ETest.kt
index 21e4eb8d4a..50ad0e4ade 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/EventsE2ETest.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/EventsE2ETest.kt
@@ -16,8 +16,10 @@ import com.instructure.student.ui.utils.StudentTest
import com.instructure.student.ui.utils.seedAssignments
import com.instructure.student.ui.utils.seedData
import com.instructure.student.ui.utils.tokenLogin
+import dagger.hilt.android.testing.HiltAndroidTest
import org.junit.Test
+@HiltAndroidTest
class EventsE2ETest: StudentTest() {
override fun displaysPageObjects() {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/FilesE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/FilesE2ETest.kt
index 304bb8899b..dea26eccf1 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/FilesE2ETest.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/FilesE2ETest.kt
@@ -41,12 +41,14 @@ import com.instructure.student.ui.utils.StudentTest
import com.instructure.student.ui.utils.seedData
import com.instructure.student.ui.utils.tokenLogin
import com.instructure.student.ui.utils.uploadTextFile
+import dagger.hilt.android.testing.HiltAndroidTest
import org.junit.Test
import java.io.File
import java.io.FileWriter
// Tests that files (assignment uploads, assignment comment attachments, discussion attachments)
// are properly displayed
+@HiltAndroidTest
class FilesE2ETest: StudentTest() {
override fun displaysPageObjects() {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/GradesE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/GradesE2ETest.kt
index 9e7ef377cf..d9fcb6ab32 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/GradesE2ETest.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/GradesE2ETest.kt
@@ -24,8 +24,10 @@ import com.instructure.student.R
import com.instructure.student.ui.utils.StudentTest
import com.instructure.student.ui.utils.seedData
import com.instructure.student.ui.utils.tokenLogin
+import dagger.hilt.android.testing.HiltAndroidTest
import org.junit.Test
+@HiltAndroidTest
class GradesE2ETest: StudentTest() {
override fun displaysPageObjects() {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/InboxE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/InboxE2ETest.kt
index 8fb9a83e88..02fe645387 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/InboxE2ETest.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/InboxE2ETest.kt
@@ -27,8 +27,10 @@ import com.instructure.panda_annotations.TestMetaData
import com.instructure.student.ui.utils.StudentTest
import com.instructure.student.ui.utils.seedData
import com.instructure.student.ui.utils.tokenLogin
+import dagger.hilt.android.testing.HiltAndroidTest
import org.junit.Test
+@HiltAndroidTest
class InboxE2ETest: StudentTest() {
override fun displaysPageObjects() {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
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 7b7469d426..c79f3fe5ce 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
@@ -30,8 +30,10 @@ import com.instructure.dataseeding.util.CanvasRestAdapter
import com.instructure.panda_annotations.*
import com.instructure.student.ui.utils.StudentTest
import com.instructure.student.ui.utils.seedData
+import dagger.hilt.android.testing.HiltAndroidTest
import org.junit.Test
+@HiltAndroidTest
class LoginE2ETest : StudentTest() {
override fun displaysPageObjects() {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/ModulesE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/ModulesE2ETest.kt
index 16dd481d5a..44571d2e1a 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/ModulesE2ETest.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/ModulesE2ETest.kt
@@ -35,8 +35,10 @@ import com.instructure.panda_annotations.TestMetaData
import com.instructure.student.ui.utils.StudentTest
import com.instructure.student.ui.utils.seedData
import com.instructure.student.ui.utils.tokenLogin
+import dagger.hilt.android.testing.HiltAndroidTest
import org.junit.Test
+@HiltAndroidTest
class ModulesE2ETest: StudentTest() {
override fun displaysPageObjects() {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/PagesE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/PagesE2ETest.kt
index c8d5a5c346..ce20c4ceb0 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/PagesE2ETest.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/PagesE2ETest.kt
@@ -28,8 +28,10 @@ import com.instructure.student.ui.pages.WebViewTextCheck
import com.instructure.student.ui.utils.StudentTest
import com.instructure.student.ui.utils.seedData
import com.instructure.student.ui.utils.tokenLogin
+import dagger.hilt.android.testing.HiltAndroidTest
import org.junit.Test
+@HiltAndroidTest
class PagesE2ETest: StudentTest() {
override fun displaysPageObjects() {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/PeopleE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/PeopleE2ETest.kt
index 23c908f340..92bc754f1f 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/PeopleE2ETest.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/PeopleE2ETest.kt
@@ -26,8 +26,10 @@ import com.instructure.panda_annotations.TestMetaData
import com.instructure.student.ui.utils.StudentTest
import com.instructure.student.ui.utils.seedData
import com.instructure.student.ui.utils.tokenLogin
+import dagger.hilt.android.testing.HiltAndroidTest
import org.junit.Test
+@HiltAndroidTest
class PeopleE2ETest: StudentTest() {
override fun displaysPageObjects() {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/QuizzesE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/QuizzesE2ETest.kt
index c54c918aae..38e314f322 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/QuizzesE2ETest.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/QuizzesE2ETest.kt
@@ -44,9 +44,11 @@ import com.instructure.student.ui.pages.WebViewTextCheck
import com.instructure.student.ui.utils.StudentTest
import com.instructure.student.ui.utils.seedData
import com.instructure.student.ui.utils.tokenLogin
+import dagger.hilt.android.testing.HiltAndroidTest
import org.hamcrest.Matchers.containsString
import org.junit.Test
+@HiltAndroidTest
class QuizzesE2ETest: StudentTest() {
override fun displaysPageObjects() {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/SettingsE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/SettingsE2ETest.kt
index 9426583a22..4b80ff1a06 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/SettingsE2ETest.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/SettingsE2ETest.kt
@@ -27,8 +27,10 @@ import com.instructure.panda_annotations.TestMetaData
import com.instructure.student.ui.utils.StudentTest
import com.instructure.student.ui.utils.seedData
import com.instructure.student.ui.utils.tokenLogin
+import dagger.hilt.android.testing.HiltAndroidTest
import org.junit.Test
+@HiltAndroidTest
class SettingsE2ETest : StudentTest() {
override fun displaysPageObjects() {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
@@ -53,11 +55,6 @@ class SettingsE2ETest : StudentTest() {
legalPage.assertPageObjects()
Espresso.pressBack() // Exit legal page
-
- settingsPage.launchHelpPage()
-
- // May be brittle. See comments in HelpPage.kt.
- helpPage.assertPageObjects()
}
// The remote config settings page (only available on debug builds) used to do some
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/ShardE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/ShardE2ETest.kt
index a28e612fcf..8f90364da7 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/ShardE2ETest.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/ShardE2ETest.kt
@@ -6,8 +6,10 @@ 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 dagger.hilt.android.testing.HiltAndroidTest
import org.junit.Test
+@HiltAndroidTest
class ShardE2ETest: StudentTest() {
override fun displaysPageObjects() {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/SyllabusE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/SyllabusE2ETest.kt
index be7582d8de..0d270ee43c 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/SyllabusE2ETest.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/SyllabusE2ETest.kt
@@ -32,8 +32,10 @@ import com.instructure.panda_annotations.TestMetaData
import com.instructure.student.ui.utils.StudentTest
import com.instructure.student.ui.utils.seedData
import com.instructure.student.ui.utils.tokenLogin
+import dagger.hilt.android.testing.HiltAndroidTest
import org.junit.Test
+@HiltAndroidTest
class SyllabusE2ETest: StudentTest() {
override fun displaysPageObjects() {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/TodoE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/TodoE2ETest.kt
index 1e92bb8693..5f734e9055 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/TodoE2ETest.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/TodoE2ETest.kt
@@ -13,9 +13,11 @@ import com.instructure.student.ui.utils.StudentTest
import com.instructure.student.ui.utils.seedAssignments
import com.instructure.student.ui.utils.seedData
import com.instructure.student.ui.utils.tokenLogin
+import dagger.hilt.android.testing.HiltAndroidTest
import org.junit.Test
import java.util.Calendar
+@HiltAndroidTest
class TodoE2ETest: StudentTest() {
override fun displaysPageObjects() {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/AnnouncementInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/AnnouncementInteractionTest.kt
index 7fdfe478f7..f5942cb2e7 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/AnnouncementInteractionTest.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/AnnouncementInteractionTest.kt
@@ -17,7 +17,6 @@ package com.instructure.student.ui.interaction
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.addCoursePermissions
import com.instructure.canvas.espresso.mockCanvas.addDiscussionTopicToCourse
@@ -33,8 +32,10 @@ import com.instructure.panda_annotations.TestMetaData
import com.instructure.student.ui.pages.WebViewTextCheck
import com.instructure.student.ui.utils.StudentTest
import com.instructure.student.ui.utils.tokenLogin
+import dagger.hilt.android.testing.HiltAndroidTest
import org.junit.Test
+@HiltAndroidTest
class AnnouncementInteractionTest : StudentTest() {
override fun displaysPageObjects() = Unit // Not used for interaction tests
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/AssignmentDetailsInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/AssignmentDetailsInteractionTest.kt
index d0adb26743..3ee24c2e4a 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/AssignmentDetailsInteractionTest.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/AssignmentDetailsInteractionTest.kt
@@ -26,10 +26,12 @@ import com.instructure.panda_annotations.TestMetaData
import com.instructure.student.ui.utils.StudentTest
import com.instructure.student.ui.utils.routeTo
import com.instructure.student.ui.utils.tokenLogin
+import dagger.hilt.android.testing.HiltAndroidTest
import org.junit.Assert.assertNotNull
import org.junit.Test
import java.util.*
+@HiltAndroidTest
class AssignmentDetailsInteractionTest : StudentTest() {
override fun displaysPageObjects() = Unit // Not used for interaction tests
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/AssignmentListInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/AssignmentListInteractionTest.kt
index c62e9f6380..26a7b82099 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/AssignmentListInteractionTest.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/AssignmentListInteractionTest.kt
@@ -24,8 +24,10 @@ 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 dagger.hilt.android.testing.HiltAndroidTest
import org.junit.Test
+@HiltAndroidTest
class AssignmentListInteractionTest : StudentTest() {
@Test
@@ -49,6 +51,26 @@ class AssignmentListInteractionTest : StudentTest() {
assignmentListPage.assertHasAssignment(assignment)
}
+ @Test
+ @TestMetaData(Priority.P1, FeatureCategory.ASSIGNMENTS, TestCategory.INTERACTION)
+ fun sortAssignmentsByTimeByDefault() {
+ val assignment = getToAssignmentsPage()[0]
+ assignmentListPage.assertHasAssignment(assignment)
+ assignmentListPage.assertSortByButtonShowsSortByTime()
+ assignmentListPage.assertFindsUndatedAssignmentLabel()
+ }
+
+ @Test
+ @TestMetaData(Priority.P1, FeatureCategory.ASSIGNMENTS, TestCategory.INTERACTION)
+ fun sortAssignmentsByTypeWhenTypeIsSelectedInTheDialog() {
+ val assignment = getToAssignmentsPage()[0]
+
+ assignmentListPage.selectSortByType()
+
+ assignmentListPage.assertHasAssignment(assignment)
+ assignmentListPage.assertSortByButtonShowsSortByType()
+ }
+
private fun getToAssignmentsPage(assignmentCount: Int = 1): List {
val data = MockCanvas.init(
courseCount = 1,
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/BookmarkInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/BookmarkInteractionTest.kt
index bd3c7beef6..5ac9d0a61e 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/BookmarkInteractionTest.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/BookmarkInteractionTest.kt
@@ -28,8 +28,10 @@ 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.tokenLogin
+import dagger.hilt.android.testing.HiltAndroidTest
import org.junit.Test
+@HiltAndroidTest
class BookmarkInteractionTest : StudentTest() {
override fun displaysPageObjects() = Unit // Not used for interaction tests
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/CalendarInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/CalendarInteractionTest.kt
index 674eba987e..ae6153df60 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/CalendarInteractionTest.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/CalendarInteractionTest.kt
@@ -21,8 +21,10 @@ 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 dagger.hilt.android.testing.HiltAndroidTest
import org.junit.Test
+@HiltAndroidTest
class CalendarInteractionTest : StudentTest() {
override fun displaysPageObjects() = Unit // Not used for interaction tests
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/CommentsInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/CommentsInteractionTest.kt
index c14fef5f6d..65fd96f8db 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/CommentsInteractionTest.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/CommentsInteractionTest.kt
@@ -21,8 +21,10 @@ 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 dagger.hilt.android.testing.HiltAndroidTest
import org.junit.Test
+@HiltAndroidTest
class CommentsInteractionTest: StudentTest() {
override fun displaysPageObjects() = Unit // Not used for interaction tests
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/CourseInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/CourseInteractionTest.kt
index d7b7944464..8c5502ece1 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/CourseInteractionTest.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/CourseInteractionTest.kt
@@ -34,9 +34,11 @@ 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.tokenLogin
+import dagger.hilt.android.testing.HiltAndroidTest
import org.hamcrest.CoreMatchers.containsString
import org.junit.Test
+@HiltAndroidTest
class CourseInteractionTest : StudentTest() {
override fun displaysPageObjects() = Unit // Not used for interaction tests
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/DashboardInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/DashboardInteractionTest.kt
index 96c7189034..fe373f005a 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/DashboardInteractionTest.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/DashboardInteractionTest.kt
@@ -19,6 +19,7 @@ package com.instructure.student.ui.interaction
import androidx.test.espresso.Espresso
import com.instructure.canvas.espresso.mockCanvas.MockCanvas
import com.instructure.canvas.espresso.mockCanvas.addAccountNotification
+import com.instructure.canvas.espresso.mockCanvas.addGroupToCourse
import com.instructure.canvas.espresso.mockCanvas.init
import com.instructure.panda_annotations.FeatureCategory
import com.instructure.panda_annotations.Priority
@@ -26,10 +27,11 @@ 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.tokenLogin
+import dagger.hilt.android.testing.HiltAndroidTest
import junit.framework.TestCase.assertNotNull
import org.junit.Test
-
+@HiltAndroidTest
class DashboardInteractionTest : StudentTest() {
override fun displaysPageObjects() = Unit // Not used for interaction tests
@@ -61,16 +63,16 @@ class DashboardInteractionTest : StudentTest() {
fun testDashboardCourses_addFavorite() {
// Starring should add course to favorite list
- val data = getToDashboard(courseCount = 2,favoriteCourseCount = 1)
- val nonFavorite = data.courses.values.filter {x -> !x.isFavorite}.first()
+ val data = getToDashboard(courseCount = 2, favoriteCourseCount = 1)
+ val nonFavorite = data.courses.values.filter { x -> !x.isFavorite }.first()
dashboardPage.assertCourseNotShown(nonFavorite)
dashboardPage.editFavorites()
- editFavoritesPage.assertCourseDisplayed(nonFavorite)
- editFavoritesPage.assertCourseNotFavorited(nonFavorite)
- editFavoritesPage.toggleCourse(nonFavorite)
- editFavoritesPage.assertCourseFavorited(nonFavorite)
+ editDashboardPage.assertCourseDisplayed(nonFavorite)
+ editDashboardPage.assertCourseNotFavorited(nonFavorite)
+ editDashboardPage.favoriteCourse(nonFavorite)
+ editDashboardPage.assertCourseFavorited(nonFavorite)
Espresso.pressBack()
@@ -83,15 +85,15 @@ class DashboardInteractionTest : StudentTest() {
// Un-starring should remove course from favorite list
val data = getToDashboard(courseCount = 2, favoriteCourseCount = 2)
- val favorite = data.courses.values.filter {x -> x.isFavorite}.first()
+ val favorite = data.courses.values.filter { x -> x.isFavorite }.first()
dashboardPage.assertDisplaysCourse(favorite)
dashboardPage.editFavorites()
- editFavoritesPage.assertCourseDisplayed(favorite)
- editFavoritesPage.assertCourseFavorited(favorite)
- editFavoritesPage.toggleCourse(favorite)
- editFavoritesPage.assertCourseNotFavorited(favorite)
+ editDashboardPage.assertCourseDisplayed(favorite)
+ editDashboardPage.assertCourseFavorited(favorite)
+ editDashboardPage.unfavoriteCourse(favorite)
+ editDashboardPage.assertCourseNotFavorited(favorite)
Espresso.pressBack()
@@ -102,25 +104,38 @@ class DashboardInteractionTest : StudentTest() {
@Test
@TestMetaData(Priority.P1, FeatureCategory.DASHBOARD, TestCategory.INTERACTION)
- fun testDashboardCourses_seeAll() {
- // Clicking "See all" should show all courses
-
- // Verify that favorite courses are showing
- val data = getToDashboard(courseCount=2, favoriteCourseCount=1)
- val favorites = data.courses.values.filter {x -> x.isFavorite}
- val all = data.courses.values
- dashboardPage.assertDisplaysCourses()
- for(course in favorites) {
- dashboardPage.assertDisplaysCourse(course)
- }
+ fun testDashboardCourses_addAllToFavorites() {
+ val data = getToDashboard(courseCount = 3, favoriteCourseCount = 0)
+ val toFavorite = data.courses.values
- // Verify that all courses show in "See All" page
- dashboardPage.assertSeeAllDisplayed()
- dashboardPage.clickSeeAll()
- for(course in all) {
- allCoursesPage.assertDisplaysCourse(course)
- }
- allCoursesPage.assertDisplaysAllCourses()
+ data.courses.values.forEach { dashboardPage.assertDisplaysCourse(it) }
+
+ dashboardPage.editFavorites()
+ toFavorite.forEach { editDashboardPage.assertCourseNotFavorited(it) }
+ editDashboardPage.selectAllCourses()
+ toFavorite.forEach { editDashboardPage.assertCourseFavorited(it) }
+
+ Espresso.pressBack()
+
+ toFavorite.forEach { dashboardPage.assertDisplaysCourse(it) }
+ }
+
+ @Test
+ @TestMetaData(Priority.P1, FeatureCategory.DASHBOARD, TestCategory.INTERACTION)
+ fun testDashboardCourses_removeAllFromFavorites() {
+ val data = getToDashboard(courseCount = 3, favoriteCourseCount = 2)
+ val toRemove = data.courses.values.filter { it.isFavorite }
+
+ toRemove.forEach { dashboardPage.assertDisplaysCourse(it) }
+
+ dashboardPage.editFavorites()
+ toRemove.forEach { editDashboardPage.assertCourseFavorited(it) }
+ editDashboardPage.unselectAllCourses()
+ toRemove.forEach { editDashboardPage.assertCourseNotFavorited(it) }
+
+ Espresso.pressBack()
+
+ data.courses.values.forEach { dashboardPage.assertDisplaysCourse(it) }
}
@Test
@@ -138,7 +153,7 @@ class DashboardInteractionTest : StudentTest() {
@TestMetaData(Priority.P1, FeatureCategory.DASHBOARD, TestCategory.INTERACTION)
fun testDashboardAnnouncement_dismiss() {
// Tapping dismiss should remove the announcement. Refresh should not display it again.
- val data = getToDashboard(courseCount = 1,favoriteCourseCount = 1,announcementCount = 1)
+ val data = getToDashboard(courseCount = 1, favoriteCourseCount = 1, announcementCount = 1)
val announcement = data.accountNotifications.values.first()
dashboardPage.assertAnnouncementShowing(announcement)
dashboardPage.dismissAnnouncement()
@@ -151,7 +166,7 @@ class DashboardInteractionTest : StudentTest() {
@TestMetaData(Priority.P1, FeatureCategory.DASHBOARD, TestCategory.INTERACTION)
fun testDashboardAnnouncement_view() {
// Tapping global announcement displays the content
- val data = getToDashboard(courseCount = 1,favoriteCourseCount = 1,announcementCount = 1)
+ val data = getToDashboard(courseCount = 1, favoriteCourseCount = 1, announcementCount = 1)
val announcement = data.accountNotifications.values.first()
dashboardPage.assertAnnouncementShowing(announcement)
dashboardPage.tapAnnouncementAndAssertDisplayed(announcement)
@@ -169,7 +184,7 @@ class DashboardInteractionTest : StudentTest() {
courseBrowserPage.assertTitleCorrect(course)
var tabs = data.courseTabs[course.id]
assertNotNull("Expected course tabs to be populated", tabs)
- for(tab in tabs!!) {
+ for (tab in tabs!!) {
courseBrowserPage.assertTabDisplayed(tab)
}
}
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 f5fb26fadd..81cc22044a 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
@@ -37,10 +37,12 @@ import com.instructure.panda_annotations.TestMetaData
import com.instructure.student.ui.pages.WebViewTextCheck
import com.instructure.student.ui.utils.StudentTest
import com.instructure.student.ui.utils.tokenLogin
+import dagger.hilt.android.testing.HiltAndroidTest
import org.junit.Assert.assertNotNull
import org.junit.Test
// Note: Tests course discussions, not group discussions.
+@HiltAndroidTest
class DiscussionsInteractionTest : StudentTest() {
override fun displaysPageObjects() = Unit // Not used for interaction tests
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ElementaryDashboardInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ElementaryDashboardInteractionTest.kt
new file mode 100644
index 0000000000..1e82f452bb
--- /dev/null
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ElementaryDashboardInteractionTest.kt
@@ -0,0 +1,106 @@
+/*
+ * Copyright (C) 2021 - 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 com.instructure.canvas.espresso.mockCanvas.MockCanvas
+import com.instructure.canvas.espresso.mockCanvas.init
+import com.instructure.canvasapi2.utils.RemoteConfigParam
+import com.instructure.canvasapi2.utils.RemoteConfigPrefs
+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.tokenLoginElementary
+import com.instructure.student.util.FeatureFlagPrefs
+import dagger.hilt.android.testing.HiltAndroidTest
+import org.junit.Test
+
+@HiltAndroidTest
+class ElementaryDashboardInteractionTest : StudentTest() {
+
+ override fun displaysPageObjects() = Unit
+
+ @Test
+ @TestMetaData(Priority.P0, FeatureCategory.K5_DASHBOARD, TestCategory.INTERACTION)
+ fun testNavigateToElementaryDashboard() {
+ // User should be able to tap and navigate to dashboard page
+ goToElementaryDashboard(courseCount = 1, favoriteCourseCount = 1)
+ elementaryDashboardPage.assertPageObjects()
+ elementaryDashboardPage.clickInboxTab()
+ inboxPage.goToDashboard()
+ elementaryDashboardPage.assertToolbarTitle()
+ elementaryDashboardPage.assertHomeroomTabVisibleAndSelected()
+ }
+
+ @Test
+ @TestMetaData(Priority.P0, FeatureCategory.K5_DASHBOARD, TestCategory.INTERACTION)
+ fun testTabsNavigation() {
+ goToElementaryDashboard(courseCount = 1, favoriteCourseCount = 1)
+ elementaryDashboardPage.assertHomeroomTabVisibleAndSelected()
+ homeroomPage.assertPageObjects()
+
+ elementaryDashboardPage.selectScheduleTab()
+ elementaryDashboardPage.assertScheduleTabVisibleAndSelected()
+ schedulePage.assertPageObjects()
+
+ elementaryDashboardPage.selectGradesTab()
+ elementaryDashboardPage.assertGradesTabVisibleAndSelected()
+ gradesPage.assertPageObjects()
+
+ elementaryDashboardPage.selectResourcesTab()
+ elementaryDashboardPage.assertResourcesTabVisibleAndSelected()
+ resourcesPage.assertPageObjects()
+
+ elementaryDashboardPage.selectHomeroomTab()
+ elementaryDashboardPage.assertHomeroomTabVisibleAndSelected()
+ homeroomPage.assertPageObjects()
+ }
+
+ @Test
+ @TestMetaData(Priority.P0, FeatureCategory.K5_DASHBOARD, TestCategory.INTERACTION)
+ fun testOnlyElementarySpecificNavigationItemsShownInTheNavigationDrawer() {
+ goToElementaryDashboard(courseCount = 1, favoriteCourseCount = 1)
+ elementaryDashboardPage.openDrawer()
+ elementaryDashboardPage.assertElementaryMenuItemsShownInDrawer()
+ elementaryDashboardPage.assertNotElementaryMenuItemsDontShowInDrawer()
+ }
+
+ private fun goToElementaryDashboard(
+ courseCount: Int = 1,
+ pastCourseCount: Int = 0,
+ favoriteCourseCount: Int = 0,
+ announcementCount: Int = 0): MockCanvas {
+
+ // We have to add this delay to be sure that the remote config is already fetched before we want to override remote config values.
+ Thread.sleep(3000)
+ RemoteConfigPrefs.putString(RemoteConfigParam.K5_DESIGN.rc_name, "true")
+
+ val data = MockCanvas.init(
+ studentCount = 1,
+ courseCount = courseCount,
+ pastCourseCount = pastCourseCount,
+ favoriteCourseCount = favoriteCourseCount,
+ accountNotificationCount = announcementCount)
+
+ val student = data.students[0]
+ val token = data.tokenFor(student)!!
+ tokenLoginElementary(data.domain, token, student)
+ elementaryDashboardPage.waitForRender()
+ return data
+ }
+}
\ No newline at end of file
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/GradesInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/GradesInteractionTest.kt
new file mode 100644
index 0000000000..4a384575d3
--- /dev/null
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/GradesInteractionTest.kt
@@ -0,0 +1,134 @@
+/*
+ * Copyright (C) 2021 - 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 com.instructure.canvas.espresso.containsTextCaseInsensitive
+import com.instructure.canvas.espresso.mockCanvas.MockCanvas
+import com.instructure.canvas.espresso.mockCanvas.addCourseWithEnrollment
+import com.instructure.canvas.espresso.mockCanvas.init
+import com.instructure.canvasapi2.models.Enrollment
+import com.instructure.canvasapi2.utils.RemoteConfigParam
+import com.instructure.canvasapi2.utils.RemoteConfigPrefs
+import com.instructure.espresso.page.getStringFromResource
+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.tokenLoginElementary
+import dagger.hilt.android.testing.HiltAndroidTest
+import org.junit.Test
+
+@HiltAndroidTest
+class GradesInteractionTest : StudentTest() {
+
+ override fun displaysPageObjects() = Unit
+
+ @Test
+ @TestMetaData(Priority.P0, FeatureCategory.K5_DASHBOARD, TestCategory.INTERACTION)
+ fun testShowGrades() {
+ val data = createMockData(courseCount = 3)
+ goToGrades(data)
+
+ gradesPage.assertPageObjects()
+
+ data.courses.forEach {
+ gradesPage.assertCourseShownWithGrades(it.value.name, "B+")
+ }
+ }
+
+ @Test
+ @TestMetaData(Priority.P1, FeatureCategory.K5_DASHBOARD, TestCategory.INTERACTION)
+ fun testRefresh() {
+ val data = createMockData(courseCount = 3)
+ goToGrades(data)
+
+ gradesPage.assertPageObjects()
+
+ data.courses.forEach {
+ gradesPage.assertCourseShownWithGrades(it.value.name, "B+")
+ }
+
+ val newCourse = data.addCourseWithEnrollment(data.students[0], Enrollment.EnrollmentType.Student, 50.0)
+
+ gradesPage.refresh()
+
+ gradesPage.assertCourseShownWithGrades(newCourse.name, "50%")
+ }
+
+ @Test
+ @TestMetaData(Priority.P2, FeatureCategory.K5_DASHBOARD, TestCategory.INTERACTION)
+ fun testEmptyView() {
+ val data = createMockData(homeroomCourseCount = 1)
+ goToGrades(data)
+
+ gradesPage.assertEmptyViewVisible()
+ gradesPage.assertRecyclerViewNotVisible()
+ }
+
+ @Test
+ @TestMetaData(Priority.P1, FeatureCategory.K5_DASHBOARD, TestCategory.INTERACTION)
+ fun testOpenCourseGrades() {
+ val data = createMockData(courseCount = 3)
+ goToGrades(data)
+
+ val course = data.courses.values.first()
+
+ gradesPage.clickGradeRow(course.name)
+ courseGradesPage.assertPageObjects()
+ courseGradesPage.assertTotalGrade(containsTextCaseInsensitive("B+"))
+ }
+
+ @Test
+ @TestMetaData(Priority.P1, FeatureCategory.K5_DASHBOARD, TestCategory.INTERACTION)
+ fun testChangeGradingPeriod() {
+ val data = createMockData(courseCount = 3, withGradingPeriods = true)
+ goToGrades(data)
+
+ gradesPage.assertSelectedGradingPeriod(gradesPage.getStringFromResource(R.string.currentGradingPeriod))
+ gradesPage.clickGradingPeriodSelector()
+
+ val gradingPeriod = data.courseGradingPeriods.values.first().first()
+ gradesPage.selectGradingPeriod(gradingPeriod.title!!)
+ gradesPage.assertSelectedGradingPeriod(gradingPeriod.title!!)
+ }
+
+ private fun createMockData(
+ courseCount: Int = 0,
+ withGradingPeriods: Boolean = false,
+ homeroomCourseCount: Int = 0): MockCanvas {
+
+ // We have to add this delay to be sure that the remote config is already fetched before we want to override remote config values.
+ Thread.sleep(3000)
+ RemoteConfigPrefs.putString(RemoteConfigParam.K5_DESIGN.rc_name, "true")
+
+ return MockCanvas.init(
+ studentCount = 1,
+ courseCount = courseCount,
+ withGradingPeriods = withGradingPeriods,
+ homeroomCourseCount = homeroomCourseCount)
+ }
+
+ private fun goToGrades(data: MockCanvas) {
+ val student = data.students[0]
+ val token = data.tokenFor(student)!!
+ tokenLoginElementary(data.domain, token, student)
+ elementaryDashboardPage.waitForRender()
+ elementaryDashboardPage.selectGradesTab()
+ }
+}
\ No newline at end of file
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/GroupLinksInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/GroupLinksInteractionTest.kt
index d0796d1134..7288017c9d 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/GroupLinksInteractionTest.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/GroupLinksInteractionTest.kt
@@ -40,8 +40,10 @@ import com.instructure.panda_annotations.TestMetaData
import com.instructure.student.ui.pages.WebViewTextCheck
import com.instructure.student.ui.utils.StudentTest
import com.instructure.student.ui.utils.tokenLogin
+import dagger.hilt.android.testing.HiltAndroidTest
import org.junit.Test
+@HiltAndroidTest
class GroupLinksInteractionTest : StudentTest() {
override fun displaysPageObjects() = Unit // Not used for interaction tests
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/HomeroomInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/HomeroomInteractionTest.kt
new file mode 100644
index 0000000000..117e1276aa
--- /dev/null
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/HomeroomInteractionTest.kt
@@ -0,0 +1,312 @@
+/*
+ * Copyright (C) 2021 - 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 com.instructure.canvas.espresso.StubLandscape
+import com.instructure.canvas.espresso.mockCanvas.*
+import com.instructure.canvasapi2.models.Assignment
+import com.instructure.canvasapi2.models.Enrollment
+import com.instructure.canvasapi2.utils.RemoteConfigParam
+import com.instructure.canvasapi2.utils.RemoteConfigPrefs
+import com.instructure.espresso.page.getStringFromResource
+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.tokenLoginElementary
+import dagger.hilt.android.testing.HiltAndroidTest
+import org.junit.Test
+
+@HiltAndroidTest
+class HomeroomInteractionTest : StudentTest() {
+
+ override fun displaysPageObjects() = Unit
+
+ @Test
+ @TestMetaData(Priority.P0, FeatureCategory.K5_DASHBOARD, TestCategory.INTERACTION)
+ fun testAnnouncementsAndCoursesShowUpOnHomeroom() {
+ val data = createMockDataWithHomeroomCourse(courseCount = 3)
+ val homeroomCourse = data.courses.values.first { it.homeroomCourse }
+ val user = data.users.values.first()
+
+ val homeroomAnnouncement = data.addDiscussionTopicToCourse(homeroomCourse, user, isAnnouncement = true)
+
+ goToHomeroom(data)
+
+ homeroomPage.assertPageObjects()
+
+ val student = data.students[0]
+ homeroomPage.assertWelcomeText(student.shortName!!)
+ homeroomPage.assertAnnouncementDisplayed(homeroomCourse.name, homeroomAnnouncement.title!!, homeroomAnnouncement.message!!)
+
+ homeroomPage.assertCourseItemsCount(3)
+ data.courses.values
+ .filter { !it.homeroomCourse }
+ .forEach {
+ homeroomPage.assertCourseDisplayed(it.name, homeroomPage.getStringFromResource(R.string.nothingDueToday), "")
+ }
+ }
+
+ @Test
+ @TestMetaData(Priority.P0, FeatureCategory.K5_DASHBOARD, TestCategory.INTERACTION)
+ fun testOnlyCoursesShowUpOnHomeroomIfNoHomeroomAnnouncement() {
+ val data = createMockDataWithHomeroomCourse(courseCount = 3, homeroomCourseCount = 0)
+
+ goToHomeroom(data)
+
+ homeroomPage.assertPageObjects()
+
+ val student = data.students[0]
+ homeroomPage.assertWelcomeText(student.shortName!!)
+ homeroomPage.assertAnnouncementNotDisplayed()
+
+ homeroomPage.assertCourseItemsCount(3)
+ data.courses.values
+ .filter { !it.homeroomCourse }
+ .forEach {
+ homeroomPage.assertCourseDisplayed(it.name, homeroomPage.getStringFromResource(R.string.nothingDueToday), "")
+ }
+ }
+
+ @Test
+ @TestMetaData(Priority.P0, FeatureCategory.K5_DASHBOARD, TestCategory.INTERACTION)
+ fun testOnlyAnnouncementShowsUpOnHomeroomIfNoHomeroomAnnouncement() {
+ val data = createMockDataWithHomeroomCourse()
+ val homeroomCourse = data.courses.values.first { it.homeroomCourse }
+ val user = data.users.values.first()
+
+ val homeroomAnnouncement = data.addDiscussionTopicToCourse(homeroomCourse, user, isAnnouncement = true)
+
+ goToHomeroom(data)
+
+ val student = data.students[0]
+ homeroomPage.assertWelcomeText(student.shortName!!)
+ homeroomPage.assertAnnouncementDisplayed(homeroomCourse.name, homeroomAnnouncement.title!!, homeroomAnnouncement.message!!)
+
+ homeroomPage.assertCourseItemsCount(0)
+ homeroomPage.assertNoSubjectsTextDisplayed()
+ }
+
+ @Test
+ @TestMetaData(Priority.P2, FeatureCategory.K5_DASHBOARD, TestCategory.INTERACTION)
+ fun testEmptyState() {
+ val data = createMockDataWithHomeroomCourse()
+
+ goToHomeroom(data)
+
+ homeroomPage.assertHomeroomContentNotDisplayed()
+ homeroomPage.assertCourseItemsCount(0)
+ homeroomPage.assertEmptyViewDisplayed()
+ }
+
+ @Test
+ @TestMetaData(Priority.P1, FeatureCategory.K5_DASHBOARD, TestCategory.INTERACTION)
+ fun testRefresh() {
+ val data = createMockDataWithHomeroomCourse()
+
+ goToHomeroom(data)
+
+ homeroomPage.assertHomeroomContentNotDisplayed()
+ homeroomPage.assertCourseItemsCount(0)
+ homeroomPage.assertEmptyViewDisplayed()
+
+ val homeroomCourse = data.courses.values.first { it.homeroomCourse }
+ val user = data.users.values.first()
+
+ val homeroomAnnouncement = data.addDiscussionTopicToCourse(homeroomCourse, user, isAnnouncement = true)
+
+ val student = data.students[0]
+ data.addCourseWithEnrollment(student, Enrollment.EnrollmentType.Student)
+ data.addCourseWithEnrollment(student, Enrollment.EnrollmentType.Student)
+
+ homeroomPage.refresh()
+
+ homeroomPage.assertWelcomeText(student.shortName!!)
+ homeroomPage.assertAnnouncementDisplayed(homeroomCourse.name, homeroomAnnouncement.title!!, homeroomAnnouncement.message!!)
+
+ homeroomPage.assertCourseItemsCount(2)
+ data.courses.values
+ .filter { !it.homeroomCourse }
+ .forEach {
+ homeroomPage.assertCourseDisplayed(it.name, homeroomPage.getStringFromResource(R.string.nothingDueToday), "")
+ }
+ }
+
+ @Test
+ @TestMetaData(Priority.P1, FeatureCategory.K5_DASHBOARD, TestCategory.INTERACTION)
+ fun testOpenHomeroomCourseAnnouncements() {
+ val data = createMockDataWithHomeroomCourse(courseCount = 3)
+ val homeroomCourse = data.courses.values.first { it.homeroomCourse }
+ val user = data.users.values.first()
+
+ val homeroomAnnouncement = data.addDiscussionTopicToCourse(homeroomCourse, user, isAnnouncement = true)
+
+ goToHomeroom(data)
+
+ homeroomPage.assertPageObjects()
+
+ val student = data.students[0]
+ homeroomPage.assertWelcomeText(student.shortName!!)
+ homeroomPage.assertAnnouncementDisplayed(homeroomCourse.name, homeroomAnnouncement.title!!, homeroomAnnouncement.message!!)
+
+ homeroomPage.openHomeroomAnnouncements()
+
+ announcementListPage.assertToolbarTitle()
+ announcementListPage.assertAnnouncementTitleVisible(homeroomAnnouncement.title!!)
+ }
+
+ @Test
+ @TestMetaData(Priority.P1, FeatureCategory.K5_DASHBOARD, TestCategory.INTERACTION)
+ fun testShowCourseCardWithAnnouncement() {
+ val data = createMockDataWithHomeroomCourse(courseCount = 3)
+ val homeroomCourse = data.courses.values.first { it.homeroomCourse }
+ val user = data.users.values.first()
+
+ data.addDiscussionTopicToCourse(homeroomCourse, user, isAnnouncement = true)
+
+ val courses = data.courses.values.filter { !it.homeroomCourse }
+ val courseAnnouncement = data.addDiscussionTopicToCourse(courses[0], user, isAnnouncement = true)
+
+ goToHomeroom(data)
+
+ homeroomPage.assertPageObjects()
+
+ homeroomPage.assertCourseDisplayed(courses[0].name, homeroomPage.getStringFromResource(R.string.nothingDueToday), courseAnnouncement.title!!)
+ homeroomPage.assertCourseDisplayed(courses[1].name, homeroomPage.getStringFromResource(R.string.nothingDueToday), "")
+ homeroomPage.assertCourseDisplayed(courses[2].name, homeroomPage.getStringFromResource(R.string.nothingDueToday), "")
+ }
+
+ @Test
+ @TestMetaData(Priority.P1, FeatureCategory.K5_DASHBOARD, TestCategory.INTERACTION)
+ fun testOpenCourseAnnouncements() {
+ val data = createMockDataWithHomeroomCourse(courseCount = 1)
+
+ val user = data.users.values.first()
+ val courses = data.courses.values.filter { !it.homeroomCourse }
+ val courseAnnouncement = data.addDiscussionTopicToCourse(courses[0], user, isAnnouncement = true)
+
+ goToHomeroom(data)
+
+ homeroomPage.assertPageObjects()
+
+ homeroomPage.openCourseAnnouncement(courseAnnouncement.title!!)
+
+ discussionDetailsPage.assertPageObjects()
+ discussionDetailsPage.assertTitleText(courseAnnouncement.title!!)
+ }
+
+ @Test
+ @TestMetaData(Priority.P0, FeatureCategory.K5_DASHBOARD, TestCategory.INTERACTION)
+ fun testOpenCourse() {
+ val data = createMockDataWithHomeroomCourse(courseCount = 3)
+ val homeroomCourse = data.courses.values.first { it.homeroomCourse }
+ val user = data.users.values.first()
+
+ data.addDiscussionTopicToCourse(homeroomCourse, user, isAnnouncement = true)
+
+ val courses = data.courses.values.filter { !it.homeroomCourse }
+
+ goToHomeroom(data)
+
+ homeroomPage.assertPageObjects()
+
+ homeroomPage.openCourse(courses[0].name)
+
+ courseBrowserPage.assertPageObjects()
+ courseBrowserPage.assertTitleCorrect(courses[0])
+ }
+
+ @Test
+ @TestMetaData(Priority.P1, FeatureCategory.K5_DASHBOARD, TestCategory.INTERACTION)
+ fun testDueTodayAndMissingAssignments() {
+ val data = createMockDataWithHomeroomCourse(courseCount = 1)
+ val homeroomCourse = data.courses.values.first { it.homeroomCourse }
+ val user = data.users.values.first()
+
+ data.addDiscussionTopicToCourse(homeroomCourse, user, isAnnouncement = true)
+
+ val courses = data.courses.values.filter { !it.homeroomCourse }
+
+ data.addAssignment(courses[0].id, submissionType = Assignment.SubmissionType.ONLINE_TEXT_ENTRY)
+ data.addAssignment(courses[0].id, submissionType = Assignment.SubmissionType.ONLINE_TEXT_ENTRY)
+
+ goToHomeroom(data)
+
+ homeroomPage.assertPageObjects()
+
+ // With the current implementation of MockCanvas, all the assignments will show up as due today and missing, because both Mock endpoints will return all the assignments.
+ // This cannot happen in normal circumstances, but for testing the UI it's fine.
+ // We can add a more sophisticated approach when other tests will need it.
+ // Veryfing the logic that one can be due today or missing only is covered by unit tests.
+ homeroomPage.assertToDoText("2 due today | 2 missing")
+ }
+
+ @StubLandscape
+ @Test
+ @TestMetaData(Priority.P1, FeatureCategory.K5_DASHBOARD, TestCategory.INTERACTION)
+ fun testOpenAssignments() {
+ val data = createMockDataWithHomeroomCourse(courseCount = 1)
+ val homeroomCourse = data.courses.values.first { it.homeroomCourse }
+ val user = data.users.values.first()
+
+ data.addDiscussionTopicToCourse(homeroomCourse, user, isAnnouncement = true)
+
+ val courses = data.courses.values.filter { !it.homeroomCourse }
+
+ val assignment1 = data.addAssignment(courses[0].id, submissionType = Assignment.SubmissionType.ONLINE_TEXT_ENTRY)
+ data.addAssignment(courses[0].id, submissionType = Assignment.SubmissionType.ONLINE_TEXT_ENTRY)
+
+ goToHomeroom(data)
+
+ homeroomPage.assertPageObjects()
+ homeroomPage.openAssignments("2 due today | 2 missing")
+
+ assignmentListPage.assertPageObjects()
+ assignmentListPage.assertHasAssignment(assignment1)
+ }
+
+ private fun createMockDataWithHomeroomCourse(
+ courseCount: Int = 0,
+ pastCourseCount: Int = 0,
+ favoriteCourseCount: Int = 0,
+ announcementCount: Int = 0,
+ homeroomCourseCount: Int = 1): MockCanvas {
+
+ // We have to add this delay to be sure that the remote config is already fetched before we want to override remote config values.
+ Thread.sleep(3000)
+ RemoteConfigPrefs.putString(RemoteConfigParam.K5_DESIGN.rc_name, "true")
+
+ val data = MockCanvas.init(
+ studentCount = 1,
+ courseCount = courseCount,
+ pastCourseCount = pastCourseCount,
+ favoriteCourseCount = favoriteCourseCount,
+ accountNotificationCount = announcementCount,
+ homeroomCourseCount = homeroomCourseCount)
+
+ return data
+ }
+
+ private fun goToHomeroom(data: MockCanvas) {
+ val student = data.students[0]
+ val token = data.tokenFor(student)!!
+ tokenLoginElementary(data.domain, token, student)
+ elementaryDashboardPage.waitForRender()
+ }
+}
\ No newline at end of file
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/InAppUpdateInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/InAppUpdateInteractionTest.kt
new file mode 100644
index 0000000000..75bd92c7fb
--- /dev/null
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/InAppUpdateInteractionTest.kt
@@ -0,0 +1,321 @@
+/*
+ * Copyright (C) 2021 - 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.NotificationManager
+import android.content.Context
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.uiautomator.By
+import androidx.test.uiautomator.UiDevice
+import androidx.test.uiautomator.Until
+import com.google.android.play.core.appupdate.testing.FakeAppUpdateManager
+import com.instructure.canvas.espresso.mockCanvas.MockCanvas
+import com.instructure.canvas.espresso.mockCanvas.init
+import com.instructure.canvasapi2.utils.toApiString
+import com.instructure.pandautils.di.UpdateModule
+import com.instructure.pandautils.update.UpdateManager
+import com.instructure.pandautils.update.UpdatePrefs
+import com.instructure.student.R
+import com.instructure.student.ui.utils.StudentTest
+import com.instructure.student.ui.utils.tokenLogin
+import dagger.hilt.android.testing.BindValue
+import dagger.hilt.android.testing.HiltAndroidTest
+import dagger.hilt.android.testing.UninstallModules
+import junit.framework.Assert.assertEquals
+import junit.framework.Assert.assertFalse
+import org.junit.Test
+import org.threeten.bp.OffsetDateTime
+
+@UninstallModules(UpdateModule::class)
+@HiltAndroidTest
+class InAppUpdateInteractionTest : StudentTest() {
+
+ private val context = InstrumentationRegistry.getInstrumentation().targetContext
+
+ private val notificationManager: NotificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
+
+ @BindValue
+ @JvmField
+ val appUpdateManager: FakeAppUpdateManager = FakeAppUpdateManager(context)
+
+ @BindValue
+ @JvmField
+ val updatePrefs: UpdatePrefs = UpdatePrefs
+
+ @BindValue
+ @JvmField
+ val updateManager: UpdateManager = UpdateManager(appUpdateManager, notificationManager, updatePrefs)
+
+ private val uiDevice by lazy { UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) }
+
+ override fun displaysPageObjects()= Unit
+
+ private fun goToDashboard() {
+ val data = MockCanvas.init(
+ studentCount = 1,
+ courseCount = 1,
+ favoriteCourseCount = 1,
+ createSections = true)
+ val user = data.students[0]
+ val token = data.tokenFor(user)!!
+ tokenLogin(data.domain, token, user)
+ dashboardPage.waitForRender()
+ }
+
+ @Test
+ fun showFlexibleConfirmationDialogForFlexibleUpdate() {
+ updatePrefs.clearPrefs()
+ with(appUpdateManager) {
+ setUpdateAvailable(400)
+ setUpdatePriority(2)
+ setClientVersionStalenessDays(10)
+ }
+
+ goToDashboard()
+
+ assert(appUpdateManager.isConfirmationDialogVisible)
+ }
+
+ @Test
+ fun hideFlexibleConfirmationDialogIfUserDeclines() {
+ updatePrefs.clearPrefs()
+ with(appUpdateManager) {
+ setUpdateAvailable(400)
+ setUpdatePriority(2)
+ setClientVersionStalenessDays(10)
+ }
+
+ goToDashboard()
+
+ assert(appUpdateManager.isConfirmationDialogVisible)
+
+ appUpdateManager.userRejectsUpdate()
+
+ assertFalse(appUpdateManager.isConfirmationDialogVisible)
+ }
+
+ @Test
+ fun hideFlexibleConfirmationDialogIfUserAccepts() {
+ updatePrefs.clearPrefs()
+ with(appUpdateManager) {
+ setUpdateAvailable(400)
+ setUpdatePriority(2)
+ setClientVersionStalenessDays(10)
+ }
+
+ goToDashboard()
+
+ assert(appUpdateManager.isConfirmationDialogVisible)
+
+ appUpdateManager.userAcceptsUpdate()
+
+ assertFalse(appUpdateManager.isConfirmationDialogVisible)
+ }
+
+ @Test
+ fun hideFlexibleConfirmationDialogIfItHasBeenShownToday() {
+ with(updatePrefs) {
+ lastUpdateNotificationCount = 1
+ lastUpdateNotificationVersionCode = 400
+ lastUpdateNotificationDate = OffsetDateTime.now().toApiString()!!
+ }
+
+ with(appUpdateManager) {
+ setUpdateAvailable(400)
+ setUpdatePriority(2)
+ setClientVersionStalenessDays(10)
+ }
+
+ goToDashboard()
+
+ assertFalse(appUpdateManager.isConfirmationDialogVisible)
+ }
+
+ @Test
+ fun hideFlexibleConfirmationDialogIfItHasBeenShownTwice() {
+ with(updatePrefs) {
+ lastUpdateNotificationCount = 2
+ lastUpdateNotificationVersionCode = 400
+ lastUpdateNotificationDate = OffsetDateTime.now().toApiString()!!
+ }
+
+ with(appUpdateManager) {
+ setUpdateAvailable(400)
+ setUpdatePriority(2)
+ setClientVersionStalenessDays(10)
+ }
+
+ goToDashboard()
+
+ assertFalse(appUpdateManager.isConfirmationDialogVisible)
+ }
+
+ @Test
+ fun showFlexibleConfirmationDialogForNewVersion() {
+ with(updatePrefs) {
+ lastUpdateNotificationCount = 2
+ lastUpdateNotificationVersionCode = 399
+ lastUpdateNotificationDate = OffsetDateTime.now().toApiString()!!
+ }
+
+ with(appUpdateManager) {
+ setUpdateAvailable(400)
+ setUpdatePriority(2)
+ setClientVersionStalenessDays(10)
+ }
+
+ goToDashboard()
+
+ assert(appUpdateManager.isConfirmationDialogVisible)
+ }
+
+ @Test
+ fun hideFlexibleConfirmationIfItWasShownToday() {
+ with(updatePrefs) {
+ lastUpdateNotificationCount = 1
+ lastUpdateNotificationVersionCode = 400
+ lastUpdateNotificationDate = OffsetDateTime.now().toApiString()!!
+ hasShownThisStart = true
+ }
+
+ with(appUpdateManager) {
+ setUpdateAvailable(400)
+ setUpdatePriority(2)
+ setClientVersionStalenessDays(10)
+ }
+
+ goToDashboard()
+
+ assertFalse(appUpdateManager.isConfirmationDialogVisible)
+ }
+
+ @Test
+ fun showImmediateFlow() {
+ with(appUpdateManager) {
+ setUpdateAvailable(400)
+ setUpdatePriority(4)
+ setClientVersionStalenessDays(10)
+ }
+
+ goToDashboard()
+
+ assert(appUpdateManager.isImmediateFlowVisible)
+ }
+
+ @Test
+ fun hideImmediateFlowIfItWasShownThisStart() {
+ with(updatePrefs) {
+ lastUpdateNotificationCount = 1
+ lastUpdateNotificationVersionCode = 400
+ lastUpdateNotificationDate = OffsetDateTime.now().minusDays(2).toApiString()!!
+ hasShownThisStart = true
+ }
+
+ with(appUpdateManager) {
+ setUpdateAvailable(400)
+ setUpdatePriority(4)
+ setClientVersionStalenessDays(10)
+ }
+
+ goToDashboard()
+
+ assertFalse(appUpdateManager.isImmediateFlowVisible)
+ }
+
+ @Test
+ fun showNotificationOnFlexibleDownloadFinish() {
+ updatePrefs.clearPrefs()
+ val expectedTitle = context.getString(R.string.appUpdateReadyTitle)
+ val expectedDescription = context.getString(R.string.appUpdateReadyDescription)
+
+ with(appUpdateManager) {
+ setUpdateAvailable(400)
+ setUpdatePriority(2)
+ setClientVersionStalenessDays(10)
+ }
+
+ goToDashboard()
+
+ assert(appUpdateManager.isConfirmationDialogVisible)
+
+ appUpdateManager.userAcceptsUpdate()
+ appUpdateManager.downloadStarts()
+ appUpdateManager.downloadCompletes()
+
+ uiDevice.openNotification()
+ uiDevice.wait(Until.hasObject(By.textStartsWith(context.getString(R.string.student_app_name))), 2)
+ val title = uiDevice.findObject(By.text(expectedTitle))
+ val description = uiDevice.findObject(By.text(expectedDescription))
+
+ assertEquals(expectedTitle, title.text)
+ assertEquals(expectedDescription, description.text)
+
+ uiDevice.pressBack()
+ }
+
+ @Test
+ fun flexibleUpdateCompletesIfAppRestarts() {
+ with(appUpdateManager) {
+ setUpdateAvailable(400)
+ setUpdatePriority(2)
+ setClientVersionStalenessDays(10)
+ userAcceptsUpdate()
+ downloadStarts()
+ downloadCompletes()
+ }
+
+ goToDashboard()
+
+ assert(appUpdateManager.isInstallSplashScreenVisible)
+ }
+
+ @Test
+ fun immediateUpdateCompletion() {
+ with(appUpdateManager) {
+ setUpdateAvailable(400)
+ setUpdatePriority(4)
+ setClientVersionStalenessDays(10)
+ }
+
+ goToDashboard()
+
+ assert(appUpdateManager.isImmediateFlowVisible)
+
+ appUpdateManager.userAcceptsUpdate()
+ appUpdateManager.downloadStarts()
+ appUpdateManager.downloadCompletes()
+
+ assert(appUpdateManager.isInstallSplashScreenVisible)
+ }
+
+ @Test
+ fun hideImmediateUpdateFlowIfUserCancels() {
+ with(appUpdateManager) {
+ setUpdateAvailable(400)
+ setUpdatePriority(4)
+ setClientVersionStalenessDays(10)
+ }
+
+ goToDashboard()
+
+ assert(appUpdateManager.isImmediateFlowVisible)
+
+ appUpdateManager.userRejectsUpdate()
+
+ assertFalse(appUpdateManager.isImmediateFlowVisible)
+ }
+}
\ No newline at end of file
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/InboxInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/InboxInteractionTest.kt
index c898e7dc67..83c53f822b 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/InboxInteractionTest.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/InboxInteractionTest.kt
@@ -26,8 +26,10 @@ 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.tokenLogin
+import dagger.hilt.android.testing.HiltAndroidTest
import org.junit.Test
+@HiltAndroidTest
class InboxInteractionTest : StudentTest() {
override fun displaysPageObjects() = Unit // Not used for interaction tests
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/LoginInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/LoginInteractionTest.kt
index fab98f5b0e..2f6d9670c8 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/LoginInteractionTest.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/LoginInteractionTest.kt
@@ -21,8 +21,10 @@ 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 dagger.hilt.android.testing.HiltAndroidTest
import org.junit.Test
+@HiltAndroidTest
class LoginInteractionTest : StudentTest() {
override fun displaysPageObjects() = Unit // Not used for interaction tests
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 2adcf8c548..64a74afb6f 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
@@ -51,9 +51,11 @@ import com.instructure.student.R
import com.instructure.student.ui.pages.WebViewTextCheck
import com.instructure.student.ui.utils.StudentTest
import com.instructure.student.ui.utils.tokenLogin
+import dagger.hilt.android.testing.HiltAndroidTest
import org.junit.Test
import java.net.URLEncoder
+@HiltAndroidTest
class ModuleInteractionTest : StudentTest() {
override fun displaysPageObjects() = Unit // Not used for interaction tests
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 ed5c4f7dd7..f1e3d4e8f3 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
@@ -15,10 +15,18 @@
*/
package com.instructure.student.ui.interaction
+import android.app.Activity
+import android.app.Instrumentation
+import android.content.Intent
import android.os.Build
+import android.util.Log
+import androidx.test.espresso.intent.Intents
+import androidx.test.espresso.intent.matcher.IntentMatchers
+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.init
+import com.instructure.canvasapi2.models.Course
import com.instructure.canvasapi2.models.User
import com.instructure.canvasapi2.utils.ApiPrefs
import com.instructure.canvasapi2.utils.ContextKeeper
@@ -28,15 +36,33 @@ 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.WebViewTextCheck
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
import org.junit.Test
+@HiltAndroidTest
class NavigationDrawerInteractionTest : StudentTest() {
override fun displaysPageObjects() = Unit // Not used for interaction tests
private lateinit var student1: User
private lateinit var student2: User
+ private lateinit var course: Course
+
+ private lateinit var activity: Activity
+
+ @Before
+ fun setUp() {
+ // If we try to read this later, it may be null, possibly because we will have navigated
+ // away from our initial activity.
+ activity = activityRule.activity
+
+
+ }
// Should be able to change the user from the navigation drawer
@Test
@@ -106,17 +132,136 @@ class NavigationDrawerInteractionTest : StudentTest() {
val data = MockCanvas.init(
studentCount = 2,
courseCount = 1,
- favoriteCourseCount = 1,
- teacherCount = 1
+ favoriteCourseCount = 1
)
student1 = data.students.first()
student2 = data.students.last()
+ course = data.courses.values.first()
+
val token = data.tokenFor(student1)!!
tokenLogin(data.domain, token, student1)
dashboardPage.waitForRender()
return data
}
+
+ // Should open a dialog and send a question for the selected course
+ // (Checks to see that we can fill out the question and the SEND button exists.)
+ @Test
+ @TestMetaData(Priority.P0, FeatureCategory.SETTINGS, TestCategory.INTERACTION, false)
+ fun testHelp_askQuestion() {
+
+ signInStudent()
+
+ dashboardPage.goToHelp()
+ helpPage.verifyAskAQuestion(course, "Here's a question")
+ }
+
+ // Should open the Canvas guides in a WebView
+ @Test
+ @TestMetaData(Priority.P0, FeatureCategory.SETTINGS, TestCategory.INTERACTION, false)
+ fun testHelp_searchCanvasGuides() {
+ signInStudent()
+
+ dashboardPage.goToHelp()
+ helpPage.launchGuides()
+ canvasWebViewPage.verifyTitle(R.string.searchGuides)
+ }
+
+ // Should send an error report
+ // (Checks to see that we can fill out an error report and that the SEND button is displayed.)
+ @Test
+ @TestMetaData(Priority.P0, FeatureCategory.SETTINGS, TestCategory.INTERACTION, false)
+ fun testHelp_reportAProblem() {
+
+ signInStudent()
+
+ dashboardPage.goToHelp()
+ helpPage.verifyReportAProblem("Problem", "It's a problem!")
+ }
+
+ // Should send a pre-filled email intent. Should be addressed to mobilesupport@instructure.com.
+ //
+ // There is a LOT of aspirational code here, in that we would like to be able to handle
+ // an intent for a specific email app if one is present. However, our app always launches
+ // an email app chooser, even if there is only one option.
+ //
+ // So this is a watered-down test that just checks whether an email app chooser gets displayed.
+ @Test
+ @TestMetaData(Priority.P0, FeatureCategory.SETTINGS, TestCategory.INTERACTION, false)
+ fun testHelp_submitFeatureIdea() {
+ signInStudent()
+
+ dashboardPage.goToHelp()
+
+ // Figure out which email apps we have installed on the device
+ var pkgMgr = activity.packageManager
+ var intent = Intent(Intent.ACTION_SEND)
+ intent.type = "message/rfc822"
+ val activities = pkgMgr.queryIntentActivities(intent, 0)
+ val matchedChooserActivities = activities.count()
+ for (activity in activities) {
+ Log.d("submitFeatureIdea","Resolved activity = $activity")
+ }
+
+ Intents.init()
+ try {
+ // Try to formulate what an email app chooser intent would look like, and how we might resolve it
+ val chooserIntentMatcher = IntentMatchers.hasAction(Intent.ACTION_CHOOSER)
+ val expectedChooserIntent = Intent(Intent.ACTION_SEND)
+ expectedChooserIntent.type = "message/rfc822"
+ expectedChooserIntent.`package` = "com.google.android.gm"
+
+ // Formulate what an actual email intent (NOT a chooser intent) would look like
+ val emailIntentMatcher = CoreMatchers.allOf(
+ IntentMatchers.hasAction(Intent.ACTION_SEND),
+ IntentMatchers.hasType("message/rfc822"),
+ CoreMatchers.anyOf(
+ IntentMatchers.hasExtra(Intent.EXTRA_EMAIL, arrayOf("support@instructure.com")),
+ IntentMatchers.hasExtra(Intent.EXTRA_EMAIL, arrayOf("mobilesupport@instructure.com"))
+ )
+ )
+
+ // Set up our intent catchers
+ Intents.intending(chooserIntentMatcher).respondWith(Instrumentation.ActivityResult(0, expectedChooserIntent))
+ Intents.intending(emailIntentMatcher).respondWith(Instrumentation.ActivityResult(0, null))
+
+ // Press the "Submit Feature" button
+ helpPage.submitFeature()
+
+ // :-( Our production code creates a chooser every time, even if there is only one email app option...
+ Intents.intended(chooserIntentMatcher)
+ }
+ finally {
+ Intents.release()
+ }
+ }
+
+ // Should send an intent to open the listing for Student App in the Play Store
+ @Test
+ @TestMetaData(Priority.P0, FeatureCategory.SETTINGS, TestCategory.INTERACTION, false)
+ fun testHelp_shareYourLove() {
+ signInStudent()
+
+ dashboardPage.goToHelp()
+ Intents.init()
+ try {
+ val expectedIntent = CoreMatchers.allOf(
+ IntentMatchers.hasAction(Intent.ACTION_VIEW),
+ CoreMatchers.anyOf(
+ // Could be either of these, depending on whether the play store app is installed
+ IntentMatchers.hasData("market://details?id=com.instructure.candroid"),
+ IntentMatchers.hasData("https://play.google.com/store/apps/details?id=com.instructure.candroid")
+ )
+ )
+ Intents.intending(expectedIntent).respondWith(Instrumentation.ActivityResult(0, null))
+ helpPage.shareYourLove()
+ Intents.intended(expectedIntent)
+ }
+ finally {
+ Intents.release()
+ }
+ }
}
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/PdfInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/PdfInteractionTest.kt
index 6266d7595f..b156f61556 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/PdfInteractionTest.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/PdfInteractionTest.kt
@@ -34,6 +34,7 @@ import com.instructure.pandautils.loaders.OpenMediaAsyncTaskLoader
import com.instructure.student.ui.utils.StudentTest
import com.instructure.student.ui.utils.routeTo
import com.instructure.student.ui.utils.tokenLogin
+import dagger.hilt.android.testing.HiltAndroidTest
import org.junit.Test
import java.io.File
import java.io.FileOutputStream
@@ -41,6 +42,7 @@ import java.io.InputStream
import java.io.OutputStream
import java.util.*
+@HiltAndroidTest
class PdfInteractionTest : StudentTest() {
override fun displaysPageObjects() = Unit // Not used for interaction tests
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/PeopleInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/PeopleInteractionTest.kt
index fd25f546e1..4d9b01cd38 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/PeopleInteractionTest.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/PeopleInteractionTest.kt
@@ -26,8 +26,10 @@ import com.instructure.panda_annotations.TestMetaData
import com.instructure.student.ui.utils.StudentTest
import com.instructure.student.ui.utils.routeTo
import com.instructure.student.ui.utils.tokenLogin
+import dagger.hilt.android.testing.HiltAndroidTest
import org.junit.Test
+@HiltAndroidTest
class PeopleInteractionTest : StudentTest() {
override fun displaysPageObjects() = Unit // Not used for interaction tests
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/PickerSubmissionUploadInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/PickerSubmissionUploadInteractionTest.kt
index a57a36aab3..932e7ebbd7 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/PickerSubmissionUploadInteractionTest.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/PickerSubmissionUploadInteractionTest.kt
@@ -33,11 +33,13 @@ 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.tokenLogin
+import dagger.hilt.android.testing.HiltAndroidTest
import org.hamcrest.core.AllOf
import org.junit.Before
import org.junit.Test
import java.io.File
+@HiltAndroidTest
class PickerSubmissionUploadInteractionTest : StudentTest() {
override fun displaysPageObjects() = Unit
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ProfileSettingsInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ProfileSettingsInteractionTest.kt
index a9ee17151c..f0bd7b8659 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ProfileSettingsInteractionTest.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ProfileSettingsInteractionTest.kt
@@ -12,10 +12,12 @@ 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 dagger.hilt.android.testing.HiltAndroidTest
import org.junit.Assert.assertTrue
import org.junit.Rule
import org.junit.Test
+@HiltAndroidTest
class ProfileSettingsInteractionTest : StudentTest() {
override fun displaysPageObjects() = Unit // Not used for interaction tests
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/PushNotificationInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/PushNotificationInteractionTest.kt
index 8cfb83203e..78e9e751b1 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/PushNotificationInteractionTest.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/PushNotificationInteractionTest.kt
@@ -30,9 +30,11 @@ 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.tokenLogin
+import dagger.hilt.android.testing.HiltAndroidTest
import org.junit.Test
import java.util.*
+@HiltAndroidTest
class PushNotificationInteractionTest : StudentTest() {
override fun displaysPageObjects() = Unit // Not used for interaction tests
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ResourcesInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ResourcesInteractionTest.kt
new file mode 100644
index 0000000000..2d6d90176b
--- /dev/null
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ResourcesInteractionTest.kt
@@ -0,0 +1,266 @@
+/*
+ * Copyright (C) 2021 - 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 com.instructure.canvas.espresso.mockCanvas.*
+import com.instructure.canvasapi2.models.Enrollment
+import com.instructure.canvasapi2.utils.RemoteConfigParam
+import com.instructure.canvasapi2.utils.RemoteConfigPrefs
+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.tokenLoginElementary
+import com.instructure.student.util.FeatureFlagPrefs
+import dagger.hilt.android.testing.HiltAndroidTest
+import org.junit.Test
+
+@HiltAndroidTest
+class ResourcesInteractionTest : StudentTest() {
+
+ override fun displaysPageObjects() = Unit
+
+ @Test
+ @TestMetaData(Priority.P0, FeatureCategory.K5_DASHBOARD, TestCategory.INTERACTION)
+ fun testImportantLinksAndActionItemsShowUpInResourcesScreen() {
+ val data = createMockDataWithHomeroomCourse(courseCount = 2)
+
+ val homeroomCourse = data.courses.values.first { it.homeroomCourse }
+ val courseWithSyllabus = homeroomCourse.copy(syllabusBody = "Important links content")
+ data.courses[homeroomCourse.id] = courseWithSyllabus
+
+ val nonHomeroomCourses = data.courses.values.filter { !it.homeroomCourse }
+ nonHomeroomCourses.forEach {
+ data.addLTITool("Google Drive", "http://google.com", it, 1234L)
+ data.addLTITool("Media Gallery", "http://instructure.com", it, 12345L)
+ }
+
+ goToResources(data)
+
+ resourcesPage.assertPageObjects()
+ resourcesPage.assertImportantLinksDisplayed(courseWithSyllabus.syllabusBody!!)
+
+ resourcesPage.assertStudentApplicationsHeaderDisplayed()
+ resourcesPage.assertLtiToolDisplayed("Google Drive")
+ resourcesPage.assertLtiToolDisplayed("Media Gallery")
+
+ val teacher = data.teachers[0]
+ resourcesPage.assertStaffInfoHeaderDisplayed()
+ resourcesPage.assertStaffDisplayed(teacher.shortName!!)
+ }
+
+ @Test
+ @TestMetaData(Priority.P0, FeatureCategory.K5_DASHBOARD, TestCategory.INTERACTION)
+ fun testImportantLinksForTwoCourses() {
+ val data = createMockDataWithHomeroomCourse(courseCount = 2)
+
+ val homeroomCourse = data.courses.values.first { it.homeroomCourse }
+ val courseWithSyllabus = homeroomCourse.copy(syllabusBody = "Important links content")
+ data.courses[homeroomCourse.id] = courseWithSyllabus
+
+ val homeroomCourse2 = data.addCourseWithEnrollment(data.students[0], Enrollment.EnrollmentType.Student, isHomeroom = true)
+ data.addEnrollment(data.teachers[0], homeroomCourse, Enrollment.EnrollmentType.Teacher)
+
+ val courseWithSyllabus2 = homeroomCourse2.copy(syllabusBody = "Important links 2")
+ data.courses[homeroomCourse2.id] = courseWithSyllabus2
+
+ goToResources(data)
+
+ resourcesPage.assertPageObjects()
+
+ // We only assert the course names, because can't differentiate between the two WebViews.
+ resourcesPage.assertCourseNameDisplayed(courseWithSyllabus.name)
+ resourcesPage.assertCourseNameDisplayed(courseWithSyllabus2.name)
+ }
+
+ @Test
+ @TestMetaData(Priority.P0, FeatureCategory.K5_DASHBOARD, TestCategory.INTERACTION)
+ fun testOnlyActionItemsShowIfSyllabusIsEmpty() {
+ val data = createMockDataWithHomeroomCourse(courseCount = 2)
+
+ val homeroomCourse = data.courses.values.first { it.homeroomCourse }
+ val courseWithSyllabus = homeroomCourse.copy(syllabusBody = "")
+ data.courses[homeroomCourse.id] = courseWithSyllabus
+
+ val nonHomeroomCourses = data.courses.values.filter { !it.homeroomCourse }
+ nonHomeroomCourses.forEach {
+ data.addLTITool("Google Drive", "http://google.com", it, 1234L)
+ data.addLTITool("Media Gallery", "http://instructure.com", it, 12345L)
+ }
+
+ goToResources(data)
+
+ resourcesPage.assertImportantLinksNotDisplayed()
+
+ resourcesPage.assertStudentApplicationsHeaderDisplayed()
+ resourcesPage.assertLtiToolDisplayed("Google Drive")
+ resourcesPage.assertLtiToolDisplayed("Media Gallery")
+
+ val teacher = data.teachers[0]
+ resourcesPage.assertStaffInfoHeaderDisplayed()
+ resourcesPage.assertStaffDisplayed(teacher.shortName!!)
+ }
+
+ @Test
+ @TestMetaData(Priority.P0, FeatureCategory.K5_DASHBOARD, TestCategory.INTERACTION)
+ fun testOnlyLtiToolsShowIfNoHomeroomCourse() {
+ val data = createMockDataWithHomeroomCourse(courseCount = 2, homeroomCourseCount = 0)
+
+ val nonHomeroomCourses = data.courses.values.filter { !it.homeroomCourse }
+ nonHomeroomCourses.forEach {
+ data.addLTITool("Google Drive", "http://google.com", it, 1234L)
+ data.addLTITool("Media Gallery", "http://instructure.com", it, 12345L)
+ }
+
+ goToResources(data)
+
+ resourcesPage.assertImportantLinksNotDisplayed()
+
+ resourcesPage.assertStudentApplicationsHeaderDisplayed()
+ resourcesPage.assertLtiToolDisplayed("Google Drive")
+ resourcesPage.assertLtiToolDisplayed("Media Gallery")
+
+ resourcesPage.assertStaffInfoNotDisplayed()
+ }
+
+ @Test
+ @TestMetaData(Priority.P2, FeatureCategory.K5_DASHBOARD, TestCategory.INTERACTION)
+ fun testEmptyState() {
+ val data = createMockDataWithHomeroomCourse(courseCount = 2, homeroomCourseCount = 0)
+
+ goToResources(data)
+
+ resourcesPage.assertImportantLinksNotDisplayed()
+ resourcesPage.assertStudentApplicationsNotDisplayed()
+ resourcesPage.assertStaffInfoNotDisplayed()
+ resourcesPage.assertEmptyViewDisplayed()
+ }
+
+ @Test
+ @TestMetaData(Priority.P1, FeatureCategory.K5_DASHBOARD, TestCategory.INTERACTION)
+ fun testRefresh() {
+ val data = createMockDataWithHomeroomCourse(courseCount = 2, homeroomCourseCount = 0)
+
+ goToResources(data)
+
+ resourcesPage.assertEmptyViewDisplayed()
+
+ val homeroomCourse = data.addCourseWithEnrollment(data.students[0], Enrollment.EnrollmentType.Student, isHomeroom = true)
+ data.addEnrollment(data.teachers[0], homeroomCourse, Enrollment.EnrollmentType.Teacher)
+
+ val courseWithSyllabus = homeroomCourse.copy(syllabusBody = "Important links content")
+ data.courses[homeroomCourse.id] = courseWithSyllabus
+
+ val nonHomeroomCourses = data.courses.values.filter { !it.homeroomCourse }
+ nonHomeroomCourses.forEach {
+ data.addLTITool("Google Drive", "http://google.com", it, 1234L)
+ data.addLTITool("Media Gallery", "http://instructure.com", it, 12345L)
+ }
+
+ resourcesPage.refresh()
+
+ resourcesPage.assertPageObjects()
+ resourcesPage.assertImportantLinksDisplayed(courseWithSyllabus.syllabusBody!!)
+
+ resourcesPage.assertStudentApplicationsHeaderDisplayed()
+ resourcesPage.assertLtiToolDisplayed("Google Drive")
+ resourcesPage.assertLtiToolDisplayed("Media Gallery")
+
+ val teacher = data.teachers[0]
+ resourcesPage.assertStaffInfoHeaderDisplayed()
+ resourcesPage.assertStaffDisplayed(teacher.shortName!!)
+ }
+
+ @Test
+ @TestMetaData(Priority.P1, FeatureCategory.K5_DASHBOARD, TestCategory.INTERACTION)
+ fun testOpenLtiToolShowsCourseSelector() {
+ val data = createMockDataWithHomeroomCourse(courseCount = 2)
+
+ val homeroomCourse = data.courses.values.first { it.homeroomCourse }
+ val courseWithSyllabus = homeroomCourse.copy(syllabusBody = "Important links content")
+ data.courses[homeroomCourse.id] = courseWithSyllabus
+
+ val nonHomeroomCourses = data.courses.values.filter { !it.homeroomCourse }
+ nonHomeroomCourses.forEach {
+ data.addLTITool("Google Drive", "http://google.com", it, 1234L)
+ data.addLTITool("Media Gallery", "http://instructure.com", it, 12345L)
+ }
+
+ goToResources(data)
+
+ resourcesPage.openLtiApp("Google Drive")
+ nonHomeroomCourses.forEach {
+ resourcesPage.assertCourseShown(it.name)
+ }
+ }
+
+ @Test
+ @TestMetaData(Priority.P1, FeatureCategory.K5_DASHBOARD, TestCategory.INTERACTION)
+ fun testOpenComposeMessageScreen() {
+ val data = createMockDataWithHomeroomCourse(courseCount = 2)
+
+ val homeroomCourse = data.courses.values.first { it.homeroomCourse }
+ val courseWithSyllabus = homeroomCourse.copy(syllabusBody = "Important links content")
+ data.courses[homeroomCourse.id] = courseWithSyllabus
+
+ val nonHomeroomCourses = data.courses.values.filter { !it.homeroomCourse }
+ nonHomeroomCourses.forEach {
+ data.addLTITool("Google Drive", "http://google.com", it, 1234L)
+ data.addLTITool("Media Gallery", "http://instructure.com", it, 12345L)
+ }
+
+ goToResources(data)
+ resourcesPage.openComposeMessage(data.teachers[0].shortName!!)
+
+ newMessagePage.assertToolbarTitleNewMessage()
+ newMessagePage.assertCourseSelectorNotShown()
+ newMessagePage.assertRecipientsNotShown()
+ newMessagePage.assertSendIndividualMessagesNotShown()
+ newMessagePage.assertSubjectViewShown()
+ newMessagePage.assertMessageViewShown()
+ }
+
+ private fun createMockDataWithHomeroomCourse(
+ courseCount: Int = 0,
+ pastCourseCount: Int = 0,
+ favoriteCourseCount: Int = 0,
+ announcementCount: Int = 0,
+ homeroomCourseCount: Int = 1): MockCanvas {
+
+ // We have to add this delay to be sure that the remote config is already fetched before we want to override remote config values.
+ Thread.sleep(3000)
+ RemoteConfigPrefs.putString(RemoteConfigParam.K5_DESIGN.rc_name, "true")
+
+ return MockCanvas.init(
+ studentCount = 1,
+ teacherCount = 1,
+ courseCount = courseCount,
+ pastCourseCount = pastCourseCount,
+ favoriteCourseCount = favoriteCourseCount,
+ accountNotificationCount = announcementCount,
+ homeroomCourseCount = homeroomCourseCount)
+ }
+
+ private fun goToResources(data: MockCanvas) {
+ val student = data.students[0]
+ val token = data.tokenFor(student)!!
+ tokenLoginElementary(data.domain, token, student)
+ elementaryDashboardPage.waitForRender()
+ elementaryDashboardPage.selectResourcesTab()
+ }
+}
\ No newline at end of file
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ScheduleInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ScheduleInteractionTest.kt
new file mode 100644
index 0000000000..ac411ea948
--- /dev/null
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ScheduleInteractionTest.kt
@@ -0,0 +1,294 @@
+/*
+ * Copyright (C) 2021 - 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 com.instructure.canvas.espresso.mockCanvas.MockCanvas
+import com.instructure.canvas.espresso.mockCanvas.addAssignment
+import com.instructure.canvas.espresso.mockCanvas.addTodo
+import com.instructure.canvas.espresso.mockCanvas.init
+import com.instructure.canvasapi2.models.Assignment
+import com.instructure.canvasapi2.utils.RemoteConfigParam
+import com.instructure.canvasapi2.utils.RemoteConfigPrefs
+import com.instructure.canvasapi2.utils.toApiString
+import com.instructure.espresso.page.getStringFromResource
+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.date.DateTimeProvider
+import com.instructure.student.R
+import com.instructure.student.ui.utils.FakeDateTimeProvider
+import com.instructure.student.ui.utils.StudentTest
+import com.instructure.student.ui.utils.tokenLoginElementary
+import dagger.hilt.android.testing.HiltAndroidTest
+import org.junit.Before
+import org.junit.Test
+import java.util.*
+import javax.inject.Inject
+
+@HiltAndroidTest
+class ScheduleInteractionTest : StudentTest() {
+
+ @Inject
+ lateinit var dateTimeProvider: DateTimeProvider
+
+ override fun displaysPageObjects() = Unit
+
+ @Before
+ fun setUp() {
+ if (!this::dateTimeProvider.isInitialized) {
+ hiltRule.inject()
+ }
+ }
+
+ @Test
+ @TestMetaData(Priority.P0, FeatureCategory.K5_DASHBOARD, TestCategory.INTERACTION)
+ fun testShowCorrectHeaderItems() {
+ setDate(2021, Calendar.AUGUST, 11)
+ val data = createMockData(courseCount = 1)
+ goToSchedule(data)
+
+ schedulePage.assertPageObjects()
+ schedulePage.assertDayHeaderShown("August 08", "Sunday", 0)
+ schedulePage.assertDayHeaderShown("August 09", "Monday", 2)
+ schedulePage.assertNoScheduleItemDisplayed()
+
+ schedulePage.assertDayHeaderShown("August 10", schedulePage.getStringFromResource(R.string.yesterday), 4)
+ schedulePage.assertDayHeaderShown("August 11", schedulePage.getStringFromResource(R.string.today), 6)
+ schedulePage.assertNoScheduleItemDisplayed()
+
+ schedulePage.assertDayHeaderShown("August 12", schedulePage.getStringFromResource(R.string.tomorrow), 8)
+ schedulePage.assertDayHeaderShown("August 13", "Friday", 10)
+ schedulePage.assertNoScheduleItemDisplayed()
+
+ schedulePage.assertDayHeaderShown("August 14", "Saturday", 12)
+ schedulePage.assertNoScheduleItemDisplayed()
+ }
+
+ @Test
+ @TestMetaData(Priority.P0, FeatureCategory.K5_DASHBOARD, TestCategory.INTERACTION)
+ fun testShowScheduledAssignments() {
+ setDate(2021, Calendar.AUGUST, 11)
+ val data = createMockData(courseCount = 1)
+
+ val courses = data.courses.values.filter { !it.homeroomCourse }
+ courses[0].name = "Course 1"
+
+ val currentDate = dateTimeProvider.getCalendar().time.toApiString()
+ val assignment1 = data.addAssignment(courses[0].id, submissionType = Assignment.SubmissionType.ONLINE_TEXT_ENTRY, dueAt = currentDate, name = "Assignment 1")
+
+ goToSchedule(data)
+ schedulePage.scrollToPosition(10)
+ schedulePage.assertCourseHeaderDisplayed(courses[0].name)
+ schedulePage.assertScheduleItemDisplayed(assignment1.name!!)
+ }
+
+ @Test
+ @TestMetaData(Priority.P0, FeatureCategory.K5_DASHBOARD, TestCategory.INTERACTION)
+ fun testShowMissingAssignments() {
+ setDate(2021, Calendar.AUGUST, 11)
+ val data = createMockData(courseCount = 1)
+
+ val courses = data.courses.values.filter { !it.homeroomCourse }
+
+ val currentDate = dateTimeProvider.getCalendar().time.toApiString()
+ val assignment1 = data.addAssignment(courses[0].id, submissionType = Assignment.SubmissionType.ONLINE_TEXT_ENTRY, dueAt = currentDate)
+
+ goToSchedule(data)
+ schedulePage.scrollToPosition(12)
+ schedulePage.assertMissingItemDisplayed(assignment1.name!!, courses[0].name, "10 pts")
+ }
+
+ @Test
+ @TestMetaData(Priority.P0, FeatureCategory.K5_DASHBOARD, TestCategory.INTERACTION)
+ fun testShowToDoEvents() {
+ setDate(2021, Calendar.AUGUST, 11)
+ val data = createMockData(courseCount = 1)
+
+ val todo = data.addTodo("To Do event", data.students[0].id, date = dateTimeProvider.getCalendar().time)
+ val todo2 = data.addTodo("Calendar event", data.students[0].id, date = dateTimeProvider.getCalendar().time)
+
+ goToSchedule(data)
+ schedulePage.scrollToPosition(8)
+ schedulePage.assertCourseHeaderDisplayed(schedulePage.getStringFromResource(R.string.schedule_todo_title))
+ schedulePage.assertScheduleItemDisplayed(todo.plannable.title)
+ schedulePage.assertScheduleItemDisplayed(todo2.plannable.title)
+ }
+
+ @Test
+ @TestMetaData(Priority.P1, FeatureCategory.K5_DASHBOARD, TestCategory.INTERACTION)
+ fun testRefresh() {
+ setDate(2021, Calendar.AUGUST, 11)
+ val data = createMockData(courseCount = 1)
+
+ val courses = data.courses.values.filter { !it.homeroomCourse }
+
+ goToSchedule(data)
+
+ // Check that we don't have any elements initially
+ schedulePage.assertNoScheduleItemDisplayed()
+ schedulePage.scrollToPosition(8)
+ schedulePage.assertNoScheduleItemDisplayed()
+
+ val currentDate = dateTimeProvider.getCalendar().time.toApiString()
+ val assignment1 = data.addAssignment(courses[0].id, submissionType = Assignment.SubmissionType.ONLINE_TEXT_ENTRY, dueAt = currentDate)
+ val assignment2 = data.addAssignment(courses[0].id, submissionType = Assignment.SubmissionType.ONLINE_TEXT_ENTRY, dueAt = currentDate)
+
+ schedulePage.scrollToPosition(0)
+ schedulePage.refresh()
+
+ // Check that refresh was successful
+ schedulePage.scrollToPosition(7)
+ schedulePage.assertCourseHeaderDisplayed(courses[0].name)
+ schedulePage.assertScheduleItemDisplayed(assignment1.name!!)
+ schedulePage.assertScheduleItemDisplayed(assignment2.name!!)
+ }
+
+ @Test
+ @TestMetaData(Priority.P1, FeatureCategory.K5_DASHBOARD, TestCategory.INTERACTION)
+ fun testGoBack2Weeks() {
+ setDate(2021, Calendar.AUGUST, 11)
+ val data = createMockData(courseCount = 1)
+
+ goToSchedule(data)
+
+ schedulePage.assertDayHeaderShown("August 08", "Sunday", 0)
+ schedulePage.assertDayHeaderShown("August 09", "Monday", 2)
+
+ schedulePage.previousWeekButtonClick()
+ schedulePage.swipeRight()
+
+ schedulePage.assertDayHeaderShown("July 25", "Sunday", 0, recyclerViewMatcherText = "July 25")
+ schedulePage.assertDayHeaderShown("July 26", "Monday", 2, recyclerViewMatcherText = "July 25")
+ schedulePage.assertDayHeaderShown("July 27", "Tuesday", 4, recyclerViewMatcherText = "July 26")
+ schedulePage.assertDayHeaderShown("July 28", "Wednesday", 6, recyclerViewMatcherText = "July 27")
+ schedulePage.assertDayHeaderShown("July 29", "Thursday", 8, recyclerViewMatcherText = "July 28")
+ schedulePage.assertDayHeaderShown("July 30", "Friday", 10, recyclerViewMatcherText = "July 29")
+ schedulePage.assertDayHeaderShown("July 31", "Saturday", 12, recyclerViewMatcherText = "July 30")
+ }
+
+ @Test
+ @TestMetaData(Priority.P1, FeatureCategory.K5_DASHBOARD, TestCategory.INTERACTION)
+ fun testGoForward2Weeks() {
+ setDate(2021, Calendar.AUGUST, 11)
+ val data = createMockData(courseCount = 1)
+
+ goToSchedule(data)
+
+ schedulePage.assertDayHeaderShown("August 08", "Sunday", 0)
+ schedulePage.assertDayHeaderShown("August 09", "Monday", 2)
+
+ schedulePage.nextWeekButtonClick()
+ schedulePage.swipeLeft()
+
+ schedulePage.assertDayHeaderShown("August 22", "Sunday", 0, recyclerViewMatcherText = "August 22")
+ schedulePage.assertDayHeaderShown("August 23", "Monday", 2, recyclerViewMatcherText = "August 22")
+ schedulePage.assertDayHeaderShown("August 24", "Tuesday", 4, recyclerViewMatcherText = "August 23")
+ schedulePage.assertDayHeaderShown("August 25", "Wednesday", 6, recyclerViewMatcherText = "August 24")
+ schedulePage.assertDayHeaderShown("August 26", "Thursday", 8, recyclerViewMatcherText = "August 25")
+ schedulePage.assertDayHeaderShown("August 27", "Friday", 10, recyclerViewMatcherText = "August 26")
+ schedulePage.assertDayHeaderShown("August 28", "Saturday", 12, recyclerViewMatcherText = "August 27")
+ }
+
+ @Test
+ @TestMetaData(Priority.P1, FeatureCategory.K5_DASHBOARD, TestCategory.INTERACTION)
+ fun testOpenAssignment() {
+ setDate(2021, Calendar.AUGUST, 11)
+ val data = createMockData(courseCount = 1)
+
+ val courses = data.courses.values.filter { !it.homeroomCourse }
+ courses[0].name = "Course 1"
+
+ val currentDate = dateTimeProvider.getCalendar().time.toApiString()
+ val assignment = data.addAssignment(courses[0].id, submissionType = Assignment.SubmissionType.ONLINE_TEXT_ENTRY, dueAt = currentDate, name = "Assignment 1")
+
+ goToSchedule(data)
+ schedulePage.scrollToPosition(9)
+ schedulePage.clickScheduleItem(assignment.name!!)
+
+ assignmentDetailsPage.assertPageObjects()
+ assignmentDetailsPage.verifyAssignmentDetails(assignment)
+ }
+
+ @Test
+ @TestMetaData(Priority.P1, FeatureCategory.K5_DASHBOARD, TestCategory.INTERACTION)
+ fun testOpenCourse() {
+ setDate(2021, Calendar.AUGUST, 11)
+ val data = createMockData(courseCount = 1)
+
+ val courses = data.courses.values.filter { !it.homeroomCourse }
+
+ val currentDate = dateTimeProvider.getCalendar().time.toApiString()
+ data.addAssignment(courses[0].id, submissionType = Assignment.SubmissionType.ONLINE_TEXT_ENTRY, dueAt = currentDate)
+
+ goToSchedule(data)
+ schedulePage.scrollToPosition(8)
+ schedulePage.clickCourseHeader(courses[0].name)
+
+ courseBrowserPage.assertPageObjects()
+ courseBrowserPage.assertTitleCorrect(courses[0])
+ }
+
+ @Test
+ @TestMetaData(Priority.P2, FeatureCategory.K5_DASHBOARD, TestCategory.INTERACTION)
+ fun testMarkAsDone() {
+ setDate(2021, Calendar.AUGUST, 11)
+ val data = createMockData(courseCount = 1)
+
+ data.addTodo("To Do event", data.students[0].id, date = dateTimeProvider.getCalendar().time)
+
+ goToSchedule(data)
+ schedulePage.scrollToPosition(8)
+
+ schedulePage.assertMarkedAsDoneNotShown()
+
+ schedulePage.clickDoneCheckbox()
+ schedulePage.assertMarkedAsDoneShown()
+ }
+
+ private fun createMockData(
+ courseCount: Int = 0,
+ withGradingPeriods: Boolean = false,
+ homeroomCourseCount: Int = 0): MockCanvas {
+
+ // We have to add this delay to be sure that the remote config is already fetched before we want to override remote config values.
+ Thread.sleep(3000)
+ RemoteConfigPrefs.putString(RemoteConfigParam.K5_DESIGN.rc_name, "true")
+
+ return MockCanvas.init(
+ studentCount = 1,
+ courseCount = courseCount,
+ withGradingPeriods = withGradingPeriods,
+ homeroomCourseCount = homeroomCourseCount)
+ }
+
+ private fun goToSchedule(data: MockCanvas) {
+ val student = data.students[0]
+ val token = data.tokenFor(student)!!
+ tokenLoginElementary(data.domain, token, student)
+ elementaryDashboardPage.waitForRender()
+ elementaryDashboardPage.selectScheduleTab()
+ }
+
+ private fun setDate(year: Int, month: Int, dayOfMonth: Int) {
+ val cal = Calendar.getInstance()
+ cal.set(Calendar.YEAR, year)
+ cal.set(Calendar.MONTH, month)
+ cal.set(Calendar.DAY_OF_MONTH, dayOfMonth)
+ (dateTimeProvider as FakeDateTimeProvider).fakeTimeInMillis = cal.timeInMillis
+ }
+}
\ No newline at end of file
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/SettingsInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/SettingsInteractionTest.kt
index b9eeb6c41c..9fd5f912e7 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/SettingsInteractionTest.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/SettingsInteractionTest.kt
@@ -38,10 +38,12 @@ import com.instructure.panda_annotations.TestMetaData
import com.instructure.student.ui.pages.WebViewTextCheck
import com.instructure.student.ui.utils.StudentTest
import com.instructure.student.ui.utils.tokenLogin
+import dagger.hilt.android.testing.HiltAndroidTest
import org.hamcrest.CoreMatchers
import org.junit.Before
import org.junit.Test
+@HiltAndroidTest
class SettingsInteractionTest : StudentTest() {
override fun displaysPageObjects() = Unit // Not used for interaction tests
@@ -57,146 +59,6 @@ class SettingsInteractionTest : StudentTest() {
}
- // Should open a dialog and send a question for the selected course
- // (Checks to see that we can fill out the question and the SEND button exists.)
- @Test
- @TestMetaData(Priority.P0, FeatureCategory.SETTINGS, TestCategory.INTERACTION, false)
- fun testHelp_askQuestion() {
-
- setUpAndSignIn()
-
- dashboardPage.launchSettingsPage()
- settingsPage.launchHelpPage()
- helpPage.verifyAskAQuestion(course, "Here's a question")
- }
-
- // Should open the Canvas guides in a WebView
- @Test
- @TestMetaData(Priority.P0, FeatureCategory.SETTINGS, TestCategory.INTERACTION, false)
- fun testHelp_searchCanvasGuides() {
- setUpAndSignIn()
-
- dashboardPage.launchSettingsPage()
- settingsPage.launchHelpPage()
- helpPage.launchGuides()
- canvasWebViewPage.runTextChecks(
- // Potentially brittle -- the web content could be changed by another team
- WebViewTextCheck(Locator.CLASS_NAME, "lia-panel-heading-bar-title", "Guides by Product", 25)
- )
- }
-
- // Should send an error report
- // (Checks to see that we can fill out an error report and that the SEND button is displayed.)
- @Test
- @TestMetaData(Priority.P0, FeatureCategory.SETTINGS, TestCategory.INTERACTION, false)
- fun testHelp_reportAProblem() {
-
- setUpAndSignIn()
-
- dashboardPage.launchSettingsPage()
- settingsPage.launchHelpPage()
- helpPage.verifyReportAProblem("Problem", "It's a problem!")
- }
-
- // Should send a pre-filled email intent. Should be addressed to mobilesupport@instructure.com.
- //
- // There is a LOT of aspirational code here, in that we would like to be able to handle
- // an intent for a specific email app if one is present. However, our app always launches
- // an email app chooser, even if there is only one option.
- //
- // So this is a watered-down test that just checks whether an email app chooser gets displayed.
- @Test
- @TestMetaData(Priority.P0, FeatureCategory.SETTINGS, TestCategory.INTERACTION, false)
- fun testHelp_submitFeatureIdea() {
- setUpAndSignIn()
-
- dashboardPage.launchSettingsPage()
- settingsPage.launchHelpPage()
-
- // Figure out which email apps we have installed on the device
- var pkgMgr = activity.packageManager
- var intent = Intent(Intent.ACTION_SEND)
- intent.type = "message/rfc822"
- val activities = pkgMgr.queryIntentActivities(intent, 0)
- val matchedChooserActivities = activities.count()
- for (activity in activities) {
- Log.d("submitFeatureIdea","Resolved activity = $activity")
- }
-
- Intents.init()
- try {
- // Try to formulate what an email app chooser intent would look like, and how we might resolve it
- val chooserIntentMatcher = IntentMatchers.hasAction(Intent.ACTION_CHOOSER)
- val expectedChooserIntent = Intent(Intent.ACTION_SEND)
- expectedChooserIntent.type = "message/rfc822"
- expectedChooserIntent.`package` = "com.google.android.gm"
-
- // Formulate what an actual email intent (NOT a chooser intent) would look like
- val emailIntentMatcher = CoreMatchers.allOf(
- IntentMatchers.hasAction(Intent.ACTION_SEND),
- IntentMatchers.hasType("message/rfc822"),
- CoreMatchers.anyOf(
- IntentMatchers.hasExtra(Intent.EXTRA_EMAIL, arrayOf("support@instructure.com")),
- IntentMatchers.hasExtra(Intent.EXTRA_EMAIL, arrayOf("mobilesupport@instructure.com"))
- )
- )
-
- // Set up our intent catchers
- Intents.intending(chooserIntentMatcher).respondWith(Instrumentation.ActivityResult(0, expectedChooserIntent))
- Intents.intending(emailIntentMatcher).respondWith(Instrumentation.ActivityResult(0, null))
-
- // Press the "Submit Feature" button
- helpPage.submitFeature()
-
-// // Depending on how many activities matched our email intent...
-// if(matchedChooserActivities > 1) {
-// // If multiple, just check that the chooser appeared.
-// Intents.intended(chooserIntentMatcher)
-// }
-// else if(matchedChooserActivities == 1){
-// // If single, check that our email intent was dispatched
-// Intents.intended(emailIntentMatcher)
-// }
-// else {
-// // If none, there is nothing much to do here
-// Log.d("submitFeatureIdea","Not matched activities for SEND on device!")
-// }
-
- // :-( Our production code creates a chooser every time, even if there is only one email app option...
- Intents.intended(chooserIntentMatcher)
- }
- finally {
- Intents.release()
- }
- }
-
- // Should send an intent to open the listing for Student App in the Play Store
- @Test
- @TestMetaData(Priority.P0, FeatureCategory.SETTINGS, TestCategory.INTERACTION, false)
- fun testHelp_shareYourLove() {
- setUpAndSignIn()
-
- dashboardPage.launchSettingsPage()
- settingsPage.launchHelpPage()
- Intents.init()
- try {
- val expectedIntent = CoreMatchers.allOf(
- IntentMatchers.hasAction(Intent.ACTION_VIEW),
- CoreMatchers.anyOf(
- // Could be either of these, depending on whether the play store app is installed
- IntentMatchers.hasData("market://details?id=com.instructure.candroid"),
- IntentMatchers.hasData("https://play.google.com/store/apps/details?id=com.instructure.candroid")
- )
- )
- Intents.intending(expectedIntent).respondWith(Instrumentation.ActivityResult(0, null))
- helpPage.shareYourLove()
- Intents.intended(expectedIntent)
- }
- finally {
- Intents.release()
- }
- }
-
// Should launch an intent to go to our canvas-android github page
@Test
@TestMetaData(Priority.P0, FeatureCategory.SETTINGS, TestCategory.INTERACTION, false)
@@ -253,6 +115,7 @@ class SettingsInteractionTest : StudentTest() {
fun testPairObserver_refreshCode() {
setUpAndSignIn()
+ ApiPrefs.canGeneratePairingCode = true
dashboardPage.launchSettingsPage()
settingsPage.launchPairObserverPage()
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/SubmissionDetailsInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/SubmissionDetailsInteractionTest.kt
index cb7a57d533..157388af16 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/SubmissionDetailsInteractionTest.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/SubmissionDetailsInteractionTest.kt
@@ -45,9 +45,11 @@ import com.instructure.panda_annotations.TestMetaData
import com.instructure.student.ui.pages.WebViewTextCheck
import com.instructure.student.ui.utils.StudentTest
import com.instructure.student.ui.utils.tokenLogin
+import dagger.hilt.android.testing.HiltAndroidTest
import org.junit.Test
import java.util.*
+@HiltAndroidTest
class SubmissionDetailsInteractionTest : StudentTest() {
override fun displaysPageObjects() = Unit // Not used for interaction tests
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/SyllabusInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/SyllabusInteractionTest.kt
index 712718dbc8..ae2f297c9f 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/SyllabusInteractionTest.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/SyllabusInteractionTest.kt
@@ -32,8 +32,10 @@ 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.tokenLogin
+import dagger.hilt.android.testing.HiltAndroidTest
import org.junit.Test
+@HiltAndroidTest
class SyllabusInteractionTest : StudentTest() {
override fun displaysPageObjects() = Unit // Not used for interaction tests
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/TodoInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/TodoInteractionTest.kt
index 3cf3959ccd..41be02348d 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/TodoInteractionTest.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/TodoInteractionTest.kt
@@ -32,8 +32,10 @@ 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.tokenLogin
+import dagger.hilt.android.testing.HiltAndroidTest
import org.junit.Test
+@HiltAndroidTest
class TodoInteractionTest : StudentTest() {
override fun displaysPageObjects() = Unit // Not used for interaction tests
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 a04e589aed..1b87659436 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
@@ -36,13 +36,14 @@ 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.tokenLogin
+import dagger.hilt.android.testing.HiltAndroidTest
import org.hamcrest.core.AllOf.allOf
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import java.io.File
-
+@HiltAndroidTest
class UserFilesInteractionTest : StudentTest() {
override fun displaysPageObjects() = Unit // Not used for interaction tests
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/AllCoursesPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/AllCoursesPage.kt
deleted file mode 100644
index af3d6d82a4..0000000000
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/AllCoursesPage.kt
+++ /dev/null
@@ -1,75 +0,0 @@
-/*
- * Copyright (C) 2019 - 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 android.view.View
-import androidx.test.espresso.Espresso
-import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
-import androidx.test.espresso.matcher.ViewMatchers.withText
-import com.instructure.canvasapi2.models.Course
-import com.instructure.dataseeding.model.CourseApiModel
-import com.instructure.espresso.OnViewWithId
-import com.instructure.espresso.WaitForViewWithId
-import com.instructure.espresso.assertDisplayed
-import com.instructure.espresso.assertNotDisplayed
-import com.instructure.espresso.page.BasePage
-import com.instructure.espresso.page.onView
-import com.instructure.espresso.page.plus
-import com.instructure.espresso.page.withAncestor
-import com.instructure.espresso.page.withId
-import com.instructure.espresso.page.withParent
-import com.instructure.student.R
-import org.hamcrest.Matcher
-import org.hamcrest.Matchers.allOf
-
-class AllCoursesPage : BasePage(R.id.allCoursesFragmentContainer) {
- private val toolbar by OnViewWithId(R.id.toolbar)
- private val emptyView by OnViewWithId(R.id.emptyView, autoAssert = false)
- private val listView by WaitForViewWithId(R.id.listView, autoAssert = false)
-
- fun assertDisplaysCourse(course: CourseApiModel) {
- // Odd to specify isDisplayed() when I'm about to assert that it is displayed,
- // but it serves to differentiate the "all courses" version of the course from
- // the "favorites" version of the course. We'll select whichever is currently showing.
- val matcher = allOf(withText(course.name), withId(R.id.titleTextView), isDisplayed())
- scrollAndAssertDisplayed(matcher)
- }
-
- fun assertDisplaysCourse(course: Course) {
- //val matcher = allOf(withText(course.originalName!!), withId(R.id.titleTextView), isDisplayed())
- val matcher = allOf(withText(course.originalName!!), withId(R.id.titleTextView), withAncestor(R.id.allCoursesFragmentContainer))
- scrollAndAssertDisplayed(matcher)
- }
-
-
- fun assertDisplaysAllCourses() {
- emptyView.assertNotDisplayed()
- onView(withParent(R.id.toolbar) + withText(R.string.allCourses)).assertDisplayed()
- listView.assertDisplayed()
- }
-
- private fun scrollAndAssertDisplayed(matcher: Matcher) {
-
- // The recycler view scrolling is buggy. Hold off on scroll for now. :-(
-// // Scroll RecyclerView item into view, if necessary
-// onView(CoreMatchers.allOf(withId(R.id.listView), withAncestor(R.id.all_courses_fragment_container))) // There may be other listViews
-// .perform(RecyclerViewActions.scrollTo(ViewMatchers.hasDescendant(matcher)))
-
- // Now make sure that it is displayed
- Espresso.onView(matcher).assertDisplayed() // Probably unnecessary
- }
-}
\ No newline at end of file
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/AnnouncementListPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/AnnouncementListPage.kt
new file mode 100644
index 0000000000..831b2f9fca
--- /dev/null
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/AnnouncementListPage.kt
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2021 - 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.matcher.ViewMatchers
+import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
+import com.instructure.espresso.assertDisplayed
+import com.instructure.espresso.matchers.WaitForViewMatcher
+import com.instructure.espresso.page.*
+import com.instructure.student.R
+
+class AnnouncementListPage : BasePage(R.id.discussionListPage) {
+
+ fun assertToolbarTitle() {
+ WaitForViewMatcher.waitForView(withParent(R.id.discussionListToolbar) + withText(R.string.announcements)).assertDisplayed()
+ }
+
+ fun assertAnnouncementTitleVisible(title: String) {
+ onView(withText(title) + isDisplayed()).assertDisplayed()
+ }
+}
\ No newline at end of file
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/AssignmentListPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/AssignmentListPage.kt
index 17b8731f8c..10461bb1be 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/AssignmentListPage.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/AssignmentListPage.kt
@@ -17,27 +17,19 @@
package com.instructure.student.ui.pages
import android.view.View
-import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.assertion.ViewAssertions.doesNotExist
-import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
-import androidx.test.espresso.matcher.ViewMatchers.withChild
-import androidx.test.espresso.matcher.ViewMatchers.withId
-import androidx.test.espresso.matcher.ViewMatchers.withParent
-import androidx.test.espresso.matcher.ViewMatchers.withText
-import com.instructure.canvasapi2.models.Assignment
+import androidx.test.espresso.assertion.ViewAssertions.matches
+import androidx.test.espresso.matcher.ViewMatchers.*
import com.instructure.canvas.espresso.scrollRecyclerView
import com.instructure.canvas.espresso.waitForMatcherWithRefreshes
+import com.instructure.canvasapi2.models.Assignment
import com.instructure.dataseeding.model.AssignmentApiModel
import com.instructure.dataseeding.model.QuizApiModel
-import com.instructure.espresso.OnViewWithId
-import com.instructure.espresso.WaitForViewWithId
-import com.instructure.espresso.WaitForViewWithText
-import com.instructure.espresso.assertDisplayed
-import com.instructure.espresso.click
+import com.instructure.espresso.*
import com.instructure.espresso.page.BasePage
+import com.instructure.espresso.page.onView
+import com.instructure.espresso.page.waitForView
import com.instructure.espresso.page.waitForViewWithText
-import com.instructure.espresso.scrollTo
-import com.instructure.espresso.swipeDown
import com.instructure.student.R
import org.hamcrest.Matcher
import org.hamcrest.Matchers.allOf
@@ -46,13 +38,13 @@ import org.hamcrest.Matchers.containsString
class AssignmentListPage : BasePage(pageResId = R.id.assignmentListPage) {
private val assignmentListToolbar by OnViewWithId(R.id.toolbar)
+ private val gradingPeriodHeader by WaitForViewWithId(R.id.termSpinnerLayout)
+ private val sortByButton by OnViewWithId(R.id.sortByButton)
+ private val sortByTextView by OnViewWithId(R.id.sortByTextView)
// Only displayed when assignment list is empty
private val emptyView by WaitForViewWithId(R.id.emptyView, autoAssert = false)
- // Only displayed when there are grading periods
- private val gradingPeriodHeader by WaitForViewWithId(R.id.termSpinnerLayout, autoAssert = false)
-
// Only displayed when there are no assignments
private val emptyText by WaitForViewWithText(R.string.noItemsToDisplayShort, autoAssert = false)
@@ -83,7 +75,7 @@ class AssignmentListPage : BasePage(pageResId = R.id.assignmentListPage) {
private fun assertHasAssignmentCommon(assignmentName: String, assignmentDueAt: String?, expectedGrade: String? = null) {
waitForMatcherWithRefreshes(withText(assignmentName))
- waitForViewWithText(assignmentName).assertDisplayed()
+ waitForView(allOf(withText(assignmentName), isDescendantOfA(withId(R.id.assignmentListPage)))).assertDisplayed()
// Check that either the assignment due date is present, or "No Due Date" is displayed
if(assignmentDueAt != null) {
@@ -140,4 +132,21 @@ class AssignmentListPage : BasePage(pageResId = R.id.assignmentListPage) {
fun assertHasGradingPeriods() {
gradingPeriodHeader.assertDisplayed()
}
+
+ fun assertSortByButtonShowsSortByTime() {
+ sortByTextView.check(matches(withText(R.string.sortByTime)))
+ }
+
+ fun assertSortByButtonShowsSortByType() {
+ sortByTextView.check(matches(withText(R.string.sortByType)))
+ }
+
+ fun assertFindsUndatedAssignmentLabel() {
+ onView(withText(R.string.undatedAssignments)).assertVisible()
+ }
+
+ fun selectSortByType() {
+ sortByButton.click()
+ onView(withText(R.string.sortByDialogTypeOption)).click()
+ }
}
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 92547d360b..f0c70431c3 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
@@ -16,17 +16,15 @@
*/
package com.instructure.student.ui.pages
+import androidx.annotation.StringRes
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
-import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.web.assertion.WebViewAssertions.webMatches
import androidx.test.espresso.web.sugar.Web.onWebView
-import androidx.test.espresso.web.webdriver.DriverAtoms.findElement
-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.DriverAtoms.*
import androidx.test.espresso.web.webdriver.Locator
import com.instructure.canvas.espresso.withElementRepeat
-import com.instructure.espresso.page.BasePage
+import com.instructure.espresso.assertVisible
+import com.instructure.espresso.page.*
import com.instructure.student.R
import org.hamcrest.Matchers.allOf
import org.hamcrest.Matchers.containsString
@@ -35,16 +33,20 @@ import org.hamcrest.Matchers.containsString
* An abstraction for operations on a full-screen (or mostly-full-screen) webpage.
*/
open class CanvasWebViewPage : BasePage(R.id.canvasWebView) {
- fun runTextChecks(vararg checks : WebViewTextCheck) {
- for(check in checks) {
- if(check.repeatSecs != null) {
+
+ fun verifyTitle(@StringRes title: Int) {
+ onView(withAncestor(R.id.toolbar) + withText(title)).assertVisible()
+ }
+
+ fun runTextChecks(vararg checks: WebViewTextCheck) {
+ for (check in checks) {
+ if (check.repeatSecs != null) {
onWebView(allOf(withId(R.id.canvasWebView), isDisplayed()))
- .withElementRepeat(findElement(check.locatorType, check.locatorValue), check.repeatSecs)
- .check(webMatches(getText(), containsString(check.textValue)))
- }
- else {
+ .withElementRepeat(findElement(check.locatorType, check.locatorValue), check.repeatSecs)
+ .check(webMatches(getText(), containsString(check.textValue)))
+ } else {
onWebView(allOf(withId(R.id.canvasWebView), isDisplayed()))
- .withElement(findElement(check.locatorType, check.locatorValue))
+ .withElement(findElement(check.locatorType, check.locatorValue))
.check(webMatches(getText(), containsString(check.textValue)))
}
}
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/DashboardPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/DashboardPage.kt
index 2aec455e3c..4f54bfb56a 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/DashboardPage.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/DashboardPage.kt
@@ -18,11 +18,8 @@
package com.instructure.student.ui.pages
-import android.os.SystemClock.sleep
import android.view.View
-import android.widget.TextView
import androidx.appcompat.widget.SwitchCompat
-import androidx.recyclerview.widget.RecyclerView
import androidx.test.espresso.Espresso
import androidx.test.espresso.PerformException
import androidx.test.espresso.UiController
@@ -30,7 +27,6 @@ import androidx.test.espresso.ViewAction
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.assertion.ViewAssertions.doesNotExist
import androidx.test.espresso.assertion.ViewAssertions.matches
-import androidx.test.espresso.contrib.RecyclerViewActions
import androidx.test.espresso.matcher.ViewMatchers
import androidx.test.espresso.matcher.ViewMatchers.isAssignableFrom
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
@@ -51,7 +47,6 @@ import com.instructure.espresso.OnViewWithContentDescription
import com.instructure.espresso.OnViewWithId
import com.instructure.espresso.WaitForViewWithId
import com.instructure.espresso.assertDisplayed
-import com.instructure.espresso.assertGone
import com.instructure.espresso.assertNotDisplayed
import com.instructure.espresso.click
import com.instructure.espresso.page.BasePage
@@ -67,10 +62,8 @@ import com.instructure.espresso.scrollTo
import com.instructure.espresso.swipeDown
import com.instructure.espresso.waitForCheck
import com.instructure.student.R
-import org.hamcrest.BaseMatcher
import org.hamcrest.CoreMatchers.allOf
import org.hamcrest.CoreMatchers.containsString
-import org.hamcrest.Description
import org.hamcrest.Matcher
class DashboardPage : BasePage(R.id.dashboardPage) {
@@ -78,8 +71,7 @@ class DashboardPage : BasePage(R.id.dashboardPage) {
private val toolbar by OnViewWithId(R.id.toolbar)
private val emptyView by OnViewWithId(R.id.emptyCoursesView, autoAssert = false)
private val listView by WaitForViewWithId(R.id.listView, autoAssert = false)
- private val selectFavorites by WaitForViewWithId(R.id.selectFavorites)
- private val seeAllCoursesButton by WaitForViewWithId(R.id.seeAllTextView)
+ private val selectFavorites by WaitForViewWithId(R.id.editDashboardTextView)
private val hamburgerButton by OnViewWithContentDescription(R.string.navigation_drawer_open)
// Sometimes when we navigate back to the dashboard page, there can be several hamburger buttons
@@ -91,7 +83,7 @@ class DashboardPage : BasePage(R.id.dashboardPage) {
onView(withParent(R.id.toolbar) + withText(R.string.dashboard)).assertDisplayed()
listView.assertDisplayed()
onViewWithText("Courses").assertDisplayed()
- onViewWithText("See All").assertDisplayed()
+ onViewWithText("Edit Dashboard").assertDisplayed()
}
fun assertDisplaysCourse(course: CourseApiModel) {
@@ -141,14 +133,6 @@ class DashboardPage : BasePage(R.id.dashboardPage) {
onViewWithId(R.id.addCoursesButton).assertDisplayed()
}
- fun assertSeeAllDisplayed() {
- seeAllCoursesButton.assertDisplayed()
- }
-
- fun clickSeeAll() {
- seeAllCoursesButton.click()
- }
-
fun signOut() {
onView(hamburgerButtonMatcher).click()
onViewWithId(R.id.navigationDrawerItem_logout).scrollTo().click()
@@ -164,6 +148,11 @@ class DashboardPage : BasePage(R.id.dashboardPage) {
onViewWithId(R.id.navigationDrawerItem_changeUser).scrollTo().click()
}
+ fun goToHelp() {
+ onView(hamburgerButtonMatcher).click()
+ onViewWithId(R.id.navigationDrawerItem_help).scrollTo().click()
+ }
+
fun gotoGlobalFiles() {
onView(hamburgerButtonMatcher).click()
onViewWithId(R.id.navigationDrawerItem_files).click()
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/DiscussionDetailsPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/DiscussionDetailsPage.kt
index cf41c948ac..6c63b0d547 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/DiscussionDetailsPage.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/DiscussionDetailsPage.kt
@@ -20,6 +20,7 @@ import android.os.SystemClock.sleep
import androidx.test.espresso.Espresso
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions.swipeDown
+import androidx.test.espresso.action.ViewActions.swipeUp
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.isDisplayingAtLeast
import androidx.test.espresso.matcher.ViewMatchers.withId
@@ -43,6 +44,7 @@ import com.instructure.espresso.assertHasText
import com.instructure.espresso.assertNotDisplayed
import com.instructure.espresso.click
import com.instructure.espresso.page.BasePage
+import com.instructure.espresso.page.withText
import com.instructure.espresso.scrollTo
import com.instructure.student.R
import com.instructure.student.ui.utils.TypeInRCETextEditor
@@ -77,6 +79,7 @@ class DiscussionDetailsPage : BasePage(R.id.discussionDetailsPage) {
}
fun refresh() {
+ scrollToTop()
onView(allOf(withId(R.id.swipeRefreshLayout), isDisplayingAtLeast(10)))
.perform(withCustomConstraints(swipeDown(), isDisplayingAtLeast(10)))
}
@@ -310,6 +313,11 @@ class DiscussionDetailsPage : BasePage(R.id.discussionDetailsPage) {
return false
}
}
+
+ fun scrollToTop() {
+ onView(allOf(withId(R.id.swipeRefreshLayout), isDisplayingAtLeast(10)))
+ .perform(withCustomConstraints(swipeDown(), isDisplayingAtLeast(10)))
+ }
}
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/EditDashboardPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/EditDashboardPage.kt
new file mode 100644
index 0000000000..64b0c43d78
--- /dev/null
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/EditDashboardPage.kt
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2021 - 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.recyclerview.widget.RecyclerView
+import androidx.test.espresso.Espresso
+import androidx.test.espresso.Espresso.onData
+import androidx.test.espresso.Espresso.onView
+import androidx.test.espresso.action.ViewActions.click
+import androidx.test.espresso.assertion.ViewAssertions
+import androidx.test.espresso.contrib.RecyclerViewActions
+import androidx.test.espresso.matcher.ViewMatchers
+import androidx.test.espresso.matcher.ViewMatchers.*
+import androidx.test.espresso.matcher.ViewMatchers.withId
+import com.instructure.canvasapi2.models.Course
+import com.instructure.canvasapi2.models.Group
+import com.instructure.espresso.assertDisplayed
+import com.instructure.espresso.click
+import com.instructure.espresso.page.*
+import com.instructure.student.R
+import org.hamcrest.Matchers.allOf
+import org.hamcrest.Matchers.containsString
+
+class EditDashboardPage : BasePage(R.id.editDashboardPage) {
+
+ fun assertCourseDisplayed(course: Course) {
+ val itemMatcher = allOf(withText(containsString(course.name)), withId(R.id.title))
+ onView(itemMatcher).assertDisplayed()
+ }
+
+ fun assertCourseNotFavorited(course: Course) {
+ val childMatcher = withContentDescription("Add to dashboard")
+ val itemMatcher = allOf(
+ withContentDescription(containsString(", not favorite")),
+ withContentDescription(containsString(course.name)),
+ hasDescendant(childMatcher))
+ onView(itemMatcher).assertDisplayed()
+ }
+
+ fun unfavoriteCourse(course: Course) {
+ val childMatcher = withContentDescription("Remove from dashboard")
+ val itemMatcher = allOf(
+ withContentDescription(containsString(", favorite")),
+ withContentDescription(containsString(course.name)),
+ hasDescendant(childMatcher))
+
+ onView(withParent(itemMatcher) + childMatcher).click()
+ }
+
+ fun favoriteCourse(course: Course) {
+ val childMatcher = withContentDescription("Add to dashboard")
+ val itemMatcher = allOf(
+ withContentDescription(containsString(", not favorite")),
+ withContentDescription(containsString(course.name)),
+ hasDescendant(childMatcher))
+
+ onView(withParent(itemMatcher) + childMatcher).click()
+ }
+
+ fun assertCourseFavorited(course: Course) {
+ val childMatcher = withContentDescription("Remove from dashboard")
+ val itemMatcher = allOf(
+ withContentDescription(containsString(", favorite")),
+ withContentDescription(containsString(course.name)),
+ hasDescendant(childMatcher))
+ onView(itemMatcher).assertDisplayed()
+ }
+
+ fun selectAllCourses() {
+ val childMatcher = withContentDescription("Add all to dashboard")
+ val itemMatcher = allOf(hasDescendant(withText("All courses")), hasDescendant(childMatcher))
+
+ onView(withParent(itemMatcher) + childMatcher).click()
+ }
+
+ fun unselectAllCourses() {
+ val childMatcher = withContentDescription("Remove all from dashboard")
+ val itemMatcher = allOf(hasDescendant(withText("All courses")), hasDescendant(childMatcher))
+
+ onView(withParent(itemMatcher) + childMatcher).click()
+ }
+}
\ No newline at end of file
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/EditFavoritesPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/EditFavoritesPage.kt
deleted file mode 100644
index 71e3b1a28c..0000000000
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/EditFavoritesPage.kt
+++ /dev/null
@@ -1,74 +0,0 @@
-/*
- * Copyright (C) 2019 - 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.recyclerview.widget.RecyclerView
-import androidx.test.espresso.Espresso.onView
-import androidx.test.espresso.action.ViewActions.click
-import androidx.test.espresso.contrib.RecyclerViewActions
-import androidx.test.espresso.matcher.ViewMatchers.hasDescendant
-import androidx.test.espresso.matcher.ViewMatchers.hasSibling
-import androidx.test.espresso.matcher.ViewMatchers.withContentDescription
-import androidx.test.espresso.matcher.ViewMatchers.withId
-import androidx.test.espresso.matcher.ViewMatchers.withText
-import com.instructure.canvasapi2.models.Course
-import com.instructure.espresso.assertDisplayed
-import com.instructure.espresso.page.BasePage
-import com.instructure.espresso.page.withAncestor
-import com.instructure.student.R
-import org.hamcrest.Matchers.allOf
-import org.hamcrest.Matchers.containsString
-
-class EditFavoritesPage : BasePage(R.id.editFavoritesPage) {
-
- fun assertCourseDisplayed(course: Course) {
- val itemMatcher = allOf(withText(containsString(course.originalName)), withId(R.id.title))
- onView(allOf(withId(R.id.listView), withAncestor(R.id.editFavoritesPage)))
- .perform(RecyclerViewActions.scrollTo(hasDescendant(itemMatcher)))
- onView(itemMatcher).assertDisplayed()
- }
-
- fun assertCourseFavorited(course: Course) {
- val itemMatcher = allOf(
- withContentDescription(containsString(", favorite")),
- withText(containsString(course.originalName)),
- withId(R.id.title))
- onView(allOf(withId(R.id.listView), withAncestor(R.id.editFavoritesPage)))
- .perform(RecyclerViewActions.scrollTo(hasDescendant(itemMatcher)))
- onView(itemMatcher).assertDisplayed()
- }
-
- fun assertCourseNotFavorited(course: Course) {
- val itemMatcher = allOf(
- withContentDescription(containsString(", not favorite")),
- withText(containsString(course.originalName)),
- withId(R.id.title))
- onView(allOf(withId(R.id.listView), withAncestor(R.id.editFavoritesPage)))
- .perform(RecyclerViewActions.scrollTo(hasDescendant(itemMatcher)))
- onView(itemMatcher).assertDisplayed()
- }
-
- fun toggleCourse(course: Course) {
- val itemMatcher = allOf(withId(R.id.star), hasSibling(allOf(withText(containsString(course.originalName)), withId(R.id.title))))
- onView(allOf(withId(R.id.listView), withAncestor(R.id.editFavoritesPage)))
- .perform(RecyclerViewActions.scrollTo(hasDescendant(itemMatcher)))
- .perform(RecyclerViewActions.actionOnItem(hasDescendant(itemMatcher), click()))
- }
-
-
-}
-
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/ElementaryDashboardPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/ElementaryDashboardPage.kt
new file mode 100644
index 0000000000..6b86ee2dd4
--- /dev/null
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/ElementaryDashboardPage.kt
@@ -0,0 +1,110 @@
+/*
+ * Copyright (C) 2021 - 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.assertion.ViewAssertions
+import androidx.test.espresso.assertion.ViewAssertions.matches
+import androidx.test.espresso.matcher.ViewMatchers
+import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
+import androidx.test.espresso.matcher.ViewMatchers.withContentDescription
+import com.instructure.espresso.*
+import com.instructure.espresso.page.*
+import com.instructure.student.R
+import kotlinx.android.synthetic.main.fragment_elementary_dashboard.view.*
+import org.hamcrest.CoreMatchers
+
+class ElementaryDashboardPage : BasePage(R.id.elementaryDashboardPage) {
+
+ private val toolbar by OnViewWithId(R.id.toolbar)
+ private val tabLayout by OnViewWithId(R.id.dashboardTabLayout)
+ private val pager by OnViewWithId(R.id.dashboardPager)
+
+ private val hamburgerButtonMatcher = CoreMatchers.allOf(withContentDescription(R.string.navigation_drawer_open), isDisplayed())
+
+ fun assertToolbarTitle() {
+ onView(withParent(R.id.toolbar) + withText(R.string.dashboard) + isDisplayed()).assertDisplayed()
+ }
+
+ fun clickInboxTab() {
+ onView(withId(R.id.bottomNavigationInbox)).click()
+ }
+
+ fun selectHomeroomTab() {
+ onView(withAncestor(R.id.dashboardTabLayout) + withText(R.string.dashboardTabHomeroom))
+ .scrollTo()
+ .click()
+ }
+
+ fun assertHomeroomTabVisibleAndSelected() {
+ onView(withAncestor(R.id.dashboardTabLayout) + withText(R.string.dashboardTabHomeroom) + isDisplayed()).assertDisplayed()
+ onView(withAncestor(R.id.dashboardTabLayout) + withText(R.string.dashboardTabHomeroom) + isDisplayed()).assertSelected()
+ }
+
+ fun selectScheduleTab() {
+ onView(withAncestor(R.id.dashboardTabLayout) + withText(R.string.dashboardTabSchedule))
+ .scrollTo()
+ .click()
+ }
+
+ fun assertScheduleTabVisibleAndSelected() {
+ onView(withAncestor(R.id.dashboardTabLayout) + withText(R.string.dashboardTabSchedule) + isDisplayed()).assertDisplayed()
+ onView(withAncestor(R.id.dashboardTabLayout) + withText(R.string.dashboardTabSchedule) + isDisplayed()).assertSelected()
+ }
+
+ fun selectGradesTab() {
+ onView(withAncestor(R.id.dashboardTabLayout) + withText(R.string.dashboardTabGrades))
+ .scrollTo()
+ .click()
+ }
+
+ fun assertGradesTabVisibleAndSelected() {
+ onView(withAncestor(R.id.dashboardTabLayout) + withText(R.string.dashboardTabGrades) + isDisplayed()).assertDisplayed()
+ onView(withAncestor(R.id.dashboardTabLayout) + withText(R.string.dashboardTabGrades) + isDisplayed()).assertSelected()
+ }
+
+ fun selectResourcesTab() {
+ onView(withAncestor(R.id.dashboardTabLayout) + withText(R.string.dashboardTabResources))
+ .scrollTo()
+ .click()
+ }
+
+ fun assertResourcesTabVisibleAndSelected() {
+ onView(withAncestor(R.id.dashboardTabLayout) + withText(R.string.dashboardTabResources) + isDisplayed()).assertDisplayed()
+ onView(withAncestor(R.id.dashboardTabLayout) + withText(R.string.dashboardTabResources) + isDisplayed()).assertSelected()
+ }
+
+ fun waitForRender() {
+ onView(hamburgerButtonMatcher).waitForCheck(matches(isDisplayed()))
+ }
+
+ fun openDrawer() {
+ onView(hamburgerButtonMatcher).click()
+ }
+
+ fun assertElementaryMenuItemsShownInDrawer() {
+ onView(withText(R.string.files)).assertDisplayed()
+ onView(withText(R.string.settings)).assertDisplayed()
+ onView(withText(R.string.help)).assertDisplayed()
+ onView(withText(R.string.changeUser)).assertDisplayed()
+ onView(withText(R.string.logout)).assertDisplayed()
+ }
+
+ fun assertNotElementaryMenuItemsDontShowInDrawer() {
+ onView(withText(R.string.showGrades)).assertNotDisplayed()
+ onView(withText(R.string.colorOverlay)).assertNotDisplayed()
+ }
+}
\ No newline at end of file
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/GradesPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/GradesPage.kt
new file mode 100644
index 0000000000..f13e38b928
--- /dev/null
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/GradesPage.kt
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2021 - 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.*
+import com.instructure.espresso.page.*
+import com.instructure.student.R
+
+class GradesPage : BasePage(R.id.gradesPage) {
+
+ private val swipeRefreshLayout by OnViewWithId(R.id.gradesRefreshLayout)
+ private val gradesRecyclerView by OnViewWithId(R.id.gradesRecyclerView)
+ private val emptyView by OnViewWithId(R.id.gradesEmptyView, autoAssert = false)
+
+ fun assertCourseShownWithGrades(courseName: String, grade: String) {
+ val courseNameMatcher = withId(R.id.gradesCourseNameText) + withText(courseName)
+ val gradeMatcher = withId(R.id.scoreText) + withText(grade)
+
+ onView(withId(R.id.gradeRow) + withDescendant(courseNameMatcher) + withDescendant(gradeMatcher))
+ .scrollTo()
+ .assertDisplayed()
+ }
+
+ fun refresh() {
+ swipeRefreshLayout.swipeDown()
+ }
+
+ fun assertEmptyViewVisible() {
+ emptyView.assertDisplayed()
+ }
+
+ fun assertRecyclerViewNotVisible() {
+ gradesRecyclerView.assertNotDisplayed()
+ }
+
+ fun clickGradeRow(courseName: String) {
+ onView(withId(R.id.gradesCourseNameText) + withText(courseName))
+ .scrollTo()
+ .click()
+ }
+
+ fun clickGradingPeriodSelector() {
+ onView(withId(R.id.gradingPeriodSelector))
+ .click()
+ }
+
+ fun selectGradingPeriod(gradingPeriodName: String) {
+ onView(withText(gradingPeriodName))
+ .click()
+ }
+
+ fun assertSelectedGradingPeriod(gradingPeriodName: String) {
+ onView(withId(R.id.gradingPeriodSelector) + withText(gradingPeriodName))
+ .assertDisplayed()
+ }
+}
\ No newline at end of file
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/HomeroomPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/HomeroomPage.kt
new file mode 100644
index 0000000000..437f780447
--- /dev/null
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/HomeroomPage.kt
@@ -0,0 +1,123 @@
+/*
+ * Copyright (C) 2021 - 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.assertion.ViewAssertions
+import androidx.test.espresso.matcher.ViewMatchers
+import androidx.test.espresso.web.assertion.WebViewAssertions
+import androidx.test.espresso.web.sugar.Web
+import androidx.test.espresso.web.webdriver.DriverAtoms
+import androidx.test.espresso.web.webdriver.Locator
+import com.instructure.espresso.*
+import com.instructure.espresso.page.*
+import com.instructure.student.R
+import org.hamcrest.Matchers
+
+class HomeroomPage : BasePage(R.id.homeroomPage) {
+
+ private val swipeRefreshLayout by OnViewWithId(R.id.homeroomSwipeRefreshLayout)
+ private val welcomeText by OnViewWithId(R.id.welcomeText)
+ private val announcementsContainer by OnViewWithId(R.id.announcementsContainer)
+ private val mySubjectsTitle by OnViewWithId(R.id.mySubjectsTitle)
+ private val coursesRecyclerView by OnViewWithId(R.id.coursesRecyclerView)
+ private val noSubjectsText by OnViewWithId(R.id.noSubjectsText, autoAssert = false)
+
+ fun assertWelcomeText(studentName: String) {
+ welcomeText.assertHasText(getStringFromResource(R.string.homeroomWelcomeMessage, studentName))
+ }
+
+ fun assertAnnouncementDisplayed(courseName: String, title: String, content: String) {
+ onView(withAncestor(R.id.announcementsContainer) + withText(courseName)).assertDisplayed()
+ onView(withAncestor(R.id.announcementsContainer) + withText(title)).assertDisplayed()
+
+ Web.onWebView()
+ .withElement(DriverAtoms.findElement(Locator.TAG_NAME, "html"))
+ .check(WebViewAssertions.webMatches(DriverAtoms.getText(), Matchers.comparesEqualTo(content)))
+
+ onView(withAncestor(R.id.announcementsContainer) + withText(R.string.viewPreviousAnnouncements))
+ .scrollTo()
+ .assertDisplayed()
+ }
+
+ fun assertAnnouncementNotDisplayed() {
+ announcementsContainer.check(ViewAssertions.matches(ViewMatchers.hasChildCount(0)))
+ }
+
+ fun assertCourseItemsCount(coursesCount: Int) {
+ coursesRecyclerView.check(RecyclerViewItemCountAssertion(coursesCount))
+ }
+
+ fun assertCourseDisplayed(courseName: String, todoText: String, announcementText: String) {
+ val titleMatcher = withId(R.id.courseNameText) + withText(courseName)
+ val todoTextMatcher = withId(R.id.todoText) + withText(todoText)
+ val announcementMatcher = withId(R.id.announcementText) + withText(announcementText)
+
+ onView(withId(R.id.cardView) + withDescendant(titleMatcher) + withDescendant(todoTextMatcher) + withDescendant(announcementMatcher))
+ .scrollTo()
+ .assertDisplayed()
+ }
+
+ fun assertNoSubjectsTextDisplayed() {
+ noSubjectsText
+ .scrollTo()
+ .assertDisplayed()
+ .assertHasText(R.string.homeroomNoSubjects)
+ }
+
+ fun assertHomeroomContentNotDisplayed() {
+ onViewWithId(R.id.homeroomContent).assertNotDisplayed()
+ }
+
+ fun assertEmptyViewDisplayed() {
+ onViewWithId(R.id.emptyView).assertDisplayed()
+ onViewWithText(R.string.homeroomEmptyTitle).assertDisplayed()
+ onViewWithText(R.string.homeroomEmptyMessage).assertDisplayed()
+ }
+
+ fun refresh() {
+ swipeRefreshLayout.swipeDown()
+ }
+
+ fun openHomeroomAnnouncements() {
+ onViewWithId(R.id.viewPreviousAnnouncements)
+ .click()
+ }
+
+ fun openCourseAnnouncement(announcementText: String) {
+ swipeRefreshLayout.swipeUp()
+ onView(withId(R.id.announcementText) + withText(announcementText))
+ .click()
+ }
+
+ fun openCourse(courseName: String) {
+ onView(withId(R.id.courseNameText) + withText(courseName))
+ .scrollTo()
+ .click()
+ }
+
+ fun assertToDoText(todoText: String) {
+ onView(withId(R.id.todoText) + withText(todoText))
+ .scrollTo()
+ .assertDisplayed()
+ }
+
+ fun openAssignments(todoText: String) {
+ swipeRefreshLayout.swipeUp()
+ onView(withId(R.id.todoText) + withText(todoText))
+ .click()
+ }
+}
\ No newline at end of file
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/InboxPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/InboxPage.kt
index 5cfa4edf67..c4579b654a 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/InboxPage.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/InboxPage.kt
@@ -107,7 +107,7 @@ class InboxPage : BasePage(R.id.inboxPage) {
}
fun goToDashboard() {
- onView(withId(R.id.bottomNavigationCourses)).click()
+ onView(withId(R.id.bottomNavigationHome)).click()
}
fun assertConversationStarred(conversation: Conversation) {
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/NewMessagePage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/NewMessagePage.kt
index 6819469c76..7d146add29 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/NewMessagePage.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/NewMessagePage.kt
@@ -22,23 +22,19 @@ import androidx.test.espresso.Espresso
import androidx.test.espresso.NoMatchingViewException
import androidx.test.espresso.ViewAssertion
import androidx.test.espresso.action.ViewActions.click
-import androidx.test.espresso.action.ViewActions.typeText
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers
import androidx.test.espresso.matcher.ViewMatchers.*
-import com.instructure.canvas.espresso.withCustomConstraints
import com.instructure.canvasapi2.models.Course
import com.instructure.canvasapi2.models.User
import com.instructure.dataseeding.model.CanvasUserApiModel
import com.instructure.dataseeding.model.CourseApiModel
import com.instructure.dataseeding.model.GroupApiModel
import com.instructure.espresso.*
-import com.instructure.espresso.page.BasePage
-import com.instructure.espresso.page.onView
-import com.instructure.espresso.page.onViewWithId
+import com.instructure.espresso.page.*
import com.instructure.student.R
import junit.framework.Assert.assertTrue
-import org.hamcrest.Matchers.*
+import org.hamcrest.Matchers.allOf
class NewMessagePage : BasePage() {
@@ -172,6 +168,30 @@ class NewMessagePage : BasePage() {
setMessage(message)
Espresso.closeSoftKeyboard()
}
+
+ fun assertToolbarTitleNewMessage() {
+ onView(withId(R.id.toolbar) + withDescendant(withText(R.string.newMessage))).assertDisplayed()
+ }
+
+ fun assertCourseSelectorNotShown() {
+ coursesSpinner.assertNotDisplayed()
+ }
+
+ fun assertRecipientsNotShown() {
+ onViewWithId(R.id.recipientWrapper).assertNotDisplayed()
+ }
+
+ fun assertSendIndividualMessagesNotShown() {
+ sendIndividualMessageSwitch.assertNotDisplayed()
+ }
+
+ fun assertSubjectViewShown() {
+ onViewWithId(R.id.editSubject).assertDisplayed()
+ }
+
+ fun assertMessageViewShown() {
+ onViewWithId(R.id.message)
+ }
}
/** Custom ViewAssertion to make sure that a TextBox is empty */
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/ResourcesPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/ResourcesPage.kt
new file mode 100644
index 0000000000..302f348b35
--- /dev/null
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/ResourcesPage.kt
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2021 - 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.assertion.ViewAssertions
+import androidx.test.espresso.matcher.ViewMatchers
+import androidx.test.espresso.web.assertion.WebViewAssertions
+import androidx.test.espresso.web.sugar.Web
+import androidx.test.espresso.web.webdriver.DriverAtoms
+import androidx.test.espresso.web.webdriver.Locator
+import com.instructure.espresso.*
+import com.instructure.espresso.page.*
+import com.instructure.student.R
+import org.hamcrest.Matchers
+
+class ResourcesPage : BasePage(R.id.resourcesPage) {
+
+ private val swipeRefreshLayout by OnViewWithId(R.id.resourcesSwipeRefreshLayout)
+ private val importantLinksTitle by OnViewWithId(R.id.importantLinksTitle, autoAssert = false)
+ private val importantLinksContainer by OnViewWithId(R.id.importantLinksContainer)
+ private val coursesRecyclerView by OnViewWithId(R.id.actionItemsRecyclerView)
+
+ fun assertImportantLinksDisplayed(content: String) {
+ importantLinksTitle.assertDisplayed()
+ Web.onWebView()
+ .withElement(DriverAtoms.findElement(Locator.TAG_NAME, "html"))
+ .check(WebViewAssertions.webMatches(DriverAtoms.getText(), Matchers.comparesEqualTo(content)))
+ }
+
+ fun assertCourseNameDisplayed(courseName: String) {
+ onView(withId(R.id.importantLinksCourseName) + withText(courseName)).assertDisplayed()
+ }
+
+ fun assertStudentApplicationsHeaderDisplayed() {
+ onView(withText(R.string.studentApplications)).assertDisplayed()
+ }
+
+ fun assertLtiToolDisplayed(name: String) {
+ onView(withId(R.id.ltiAppCardView) + withDescendant(withText(name))).assertDisplayed()
+ }
+
+ fun assertStaffInfoHeaderDisplayed() {
+ onView(withText(R.string.staffContactInfo)).assertDisplayed()
+ }
+
+ fun assertStaffDisplayed(name: String) {
+ onView(withId(R.id.contactInfoLayout) + withDescendant(withText(name))).assertDisplayed()
+ }
+
+ fun assertImportantLinksNotDisplayed() {
+ importantLinksTitle.assertNotDisplayed()
+ importantLinksContainer.check(ViewAssertions.matches(ViewMatchers.hasChildCount(0)))
+ }
+
+ fun assertStaffInfoNotDisplayed() {
+ onView(withText(R.string.staffContactInfo)).check(ViewAssertions.doesNotExist())
+ onView(withId(R.id.contactInfoLayout)).check(ViewAssertions.doesNotExist())
+ }
+
+ fun assertStudentApplicationsNotDisplayed() {
+ onView(withText(R.string.studentApplications)).check(ViewAssertions.doesNotExist())
+ onView(withId(R.id.ltiAppCardView)).check(ViewAssertions.doesNotExist())
+ }
+
+ fun assertEmptyViewDisplayed() {
+ onViewWithId(R.id.resourcesEmptyView).assertDisplayed()
+ onViewWithText(R.string.resourcesEmptyMessage).assertDisplayed()
+ }
+
+ fun refresh() {
+ swipeRefreshLayout.swipeDown()
+ }
+
+ fun openLtiApp(name: String) {
+ onView(withId(R.id.ltiAppCardView) + withDescendant(withText(name))).click()
+ }
+
+ fun assertCourseShown(courseName: String) {
+ onView(withText(courseName))
+ }
+
+ fun openComposeMessage(teacherName: String) {
+ onView(withId(R.id.contactInfoLayout) + withDescendant(withText(teacherName))).click()
+ }
+}
\ No newline at end of file
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/SchedulePage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/SchedulePage.kt
new file mode 100644
index 0000000000..f56c4b93e4
--- /dev/null
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/SchedulePage.kt
@@ -0,0 +1,112 @@
+/*
+ * Copyright (C) 2021 - 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.assertion.ViewAssertions
+import androidx.test.espresso.contrib.RecyclerViewActions
+import com.instructure.espresso.*
+import com.instructure.espresso.page.*
+import com.instructure.pandautils.binding.BindableViewHolder
+import com.instructure.student.R
+
+class SchedulePage : BasePage(R.id.schedulePage) {
+
+ private val pager by OnViewWithId(R.id.schedulePager)
+ private val previousWeekButton by OnViewWithId(R.id.previousWeekButton)
+ private val nextWeekButton by OnViewWithId(R.id.nextWeekButton)
+ private val recyclerView by OnViewWithId(R.id.scheduleRecyclerView)
+ private val swipeRefreshLayout by OnViewWithId(R.id.scheduleSwipeRefreshLayout)
+
+ fun assertDayHeaderShown(dateText: String, dayText: String, position: Int, recyclerViewMatcherText: String? = null) {
+ val dateTextMatcher = withId(R.id.dateText) + withText(dateText)
+ val dayTextMatcher = withId(R.id.dayText) + withText(dayText)
+
+ val todayHeaderMatcher = withId(R.id.scheduleHeaderLayout) + withDescendant(dateTextMatcher) + withDescendant(dayTextMatcher)
+ if (recyclerViewMatcherText != null) {
+ val recyclerViewInteraction = onView(withId(R.id.scheduleRecyclerView) + withDescendant(withText(recyclerViewMatcherText)))
+ recyclerViewInteraction.perform(RecyclerViewActions.scrollToPosition(position))
+ } else {
+ recyclerView.perform(RecyclerViewActions.scrollToPosition(position))
+ }
+ waitForView(todayHeaderMatcher).assertDisplayed()
+ }
+
+ fun assertNoScheduleItemDisplayed() {
+ onView(withId(R.id.scheduleCourseItemLayout)).check(ViewAssertions.doesNotExist())
+ }
+
+ fun scrollToPosition(position: Int) {
+ recyclerView.perform(RecyclerViewActions.scrollToPosition(position))
+ }
+
+ fun assertCourseHeaderDisplayed(courseName: String) {
+ onView(withId(R.id.scheduleCourseHeaderText) + withText(courseName)).assertDisplayed()
+ }
+
+ fun assertScheduleItemDisplayed(scheduleItemName: String) {
+ onView(withAncestor(R.id.plannerItems) + withText(scheduleItemName)).assertDisplayed()
+ }
+
+ fun assertMissingItemDisplayed(itemName: String, courseName: String, pointsPossible: String) {
+ val titleMatcher = withId(R.id.title) + withText(itemName)
+ val courseNameMatcher = withId(R.id.courseName) + withText(courseName)
+ val pointsPossibleMatcher = withId(R.id.points) + withText(pointsPossible)
+
+ onView(withId(R.id.missingItemLayout) + withDescendant(titleMatcher) + withDescendant(courseNameMatcher) + withDescendant(pointsPossibleMatcher))
+ .assertDisplayed()
+ }
+
+ fun refresh() {
+ swipeRefreshLayout.swipeDown()
+ }
+
+ fun previousWeekButtonClick() {
+ previousWeekButton.click()
+ }
+
+ fun swipeRight() {
+ pager.swipeRight()
+ }
+
+ fun nextWeekButtonClick() {
+ nextWeekButton.click()
+ }
+
+ fun swipeLeft() {
+ pager.swipeLeft()
+ }
+
+ fun clickCourseHeader(courseName: String) {
+ onView(withId(R.id.scheduleCourseHeaderText) + withText(courseName)).click()
+ }
+
+ fun clickScheduleItem(name: String) {
+ onView(withAncestor(R.id.plannerItems) + withText(name)).click()
+ }
+
+ fun clickDoneCheckbox() {
+ onView(withId(R.id.checkbox)).click()
+ }
+
+ fun assertMarkedAsDoneShown() {
+ onViewWithText(R.string.schedule_marked_as_done).assertDisplayed()
+ }
+
+ fun assertMarkedAsDoneNotShown() {
+ onViewWithText(R.string.schedule_marked_as_done).assertNotDisplayed()
+ }
+}
\ No newline at end of file
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/SettingsPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/SettingsPage.kt
index 17528d14b3..9cbb41a9de 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/SettingsPage.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/SettingsPage.kt
@@ -32,7 +32,6 @@ class SettingsPage : BasePage(R.id.settingsFragment) {
private val pairObserverLabel by OnViewWithId(R.id.pairObserver,autoAssert=false)
private val aboutLabel by OnViewWithId(R.id.about)
private val legalLabel by OnViewWithId(R.id.legal)
- private val helpLabel by OnViewWithId(R.id.help)
private val remoteConfigLabel by OnViewWithId(R.id.remoteConfigParams)
fun launchAboutPage() {
@@ -43,10 +42,6 @@ class SettingsPage : BasePage(R.id.settingsFragment) {
legalLabel.scrollTo().click()
}
- fun launchHelpPage() {
- helpLabel.scrollTo().click()
- }
-
fun launchRemoteConfigParams() {
remoteConfigLabel.scrollTo().click()
}
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 90bf1796f3..8ec5a3764b 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
@@ -34,6 +34,7 @@ import com.instructure.student.espresso.StudentRenderTest
import com.instructure.student.mobius.assignmentDetails.AssignmentDetailsModel
import com.instructure.student.mobius.assignmentDetails.ui.AssignmentDetailsFragment
import com.spotify.mobius.runners.WorkRunner
+import dagger.hilt.android.testing.HiltAndroidTest
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@@ -41,6 +42,7 @@ import java.util.Calendar
import java.util.Date
import java.util.GregorianCalendar
+@HiltAndroidTest
@RunWith(AndroidJUnit4::class)
class AssignmentDetailsRenderTest : StudentRenderTest() {
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/ConferenceDetailsRenderTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/ConferenceDetailsRenderTest.kt
index 4006806a18..482fce4c16 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/ConferenceDetailsRenderTest.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/ConferenceDetailsRenderTest.kt
@@ -26,10 +26,12 @@ import com.instructure.student.mobius.conferences.conference_details.ui.Conferen
import com.instructure.student.mobius.conferences.conference_details.ui.ConferenceDetailsViewState
import com.instructure.student.mobius.conferences.conference_details.ui.ConferenceRecordingViewState
import com.spotify.mobius.runners.WorkRunner
+import dagger.hilt.android.testing.HiltAndroidTest
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
+@HiltAndroidTest
@RunWith(AndroidJUnit4::class)
class ConferenceDetailsRenderTest : StudentRenderTest() {
private val canvasContext: CanvasContext = Course(id = 123L, name = "Test Course")
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/ConferenceListRenderTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/ConferenceListRenderTest.kt
index 22d9f3efb2..9531404264 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/ConferenceListRenderTest.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/ConferenceListRenderTest.kt
@@ -26,9 +26,11 @@ import com.instructure.student.mobius.conferences.conference_list.ui.ConferenceL
import com.instructure.student.mobius.conferences.conference_list.ui.ConferenceListItemViewState
import com.instructure.student.mobius.conferences.conference_list.ui.ConferenceListViewState
import com.spotify.mobius.runners.WorkRunner
+import dagger.hilt.android.testing.HiltAndroidTest
import org.junit.Test
import org.junit.runner.RunWith
+@HiltAndroidTest
@RunWith(AndroidJUnit4::class)
class ConferenceListRenderTest : StudentRenderTest() {
private val canvasContext: CanvasContext = Course(id = 123L, name = "Test Course")
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/DiscussionSubmissionViewRenderTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/DiscussionSubmissionViewRenderTest.kt
index c07490f9a3..d936842623 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/DiscussionSubmissionViewRenderTest.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/DiscussionSubmissionViewRenderTest.kt
@@ -22,8 +22,10 @@ import com.instructure.panda_annotations.TestMetaData
import com.instructure.student.espresso.StudentRenderTest
import com.instructure.student.mobius.assignmentDetails.submissionDetails.content.DiscussionSubmissionViewFragment
import com.instructure.student.ui.pages.renderPages.DiscussionSubmissionViewRenderPage
+import dagger.hilt.android.testing.HiltAndroidTest
import org.junit.Test
+@HiltAndroidTest
class DiscussionSubmissionViewRenderTest : StudentRenderTest() {
private val page = DiscussionSubmissionViewRenderPage()
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/MediaSubmissionViewRenderTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/MediaSubmissionViewRenderTest.kt
index cd2f6dffb6..39d46f1a95 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/MediaSubmissionViewRenderTest.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/MediaSubmissionViewRenderTest.kt
@@ -31,9 +31,11 @@ import com.instructure.student.espresso.StudentRenderTest
import com.instructure.student.mobius.assignmentDetails.submissionDetails.SubmissionDetailsContentType
import com.instructure.student.mobius.assignmentDetails.submissionDetails.content.MediaSubmissionViewFragment
import com.instructure.student.ui.pages.renderPages.MediaSubmissionViewRenderPage
+import dagger.hilt.android.testing.HiltAndroidTest
import org.junit.Test
import org.junit.runner.RunWith
+@HiltAndroidTest
@RunWith(AndroidJUnit4::class)
class MediaSubmissionViewRenderTest : StudentRenderTest() {
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/PairObserverRenderTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/PairObserverRenderTest.kt
index 4776c5f617..9ee8af5e6e 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/PairObserverRenderTest.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/PairObserverRenderTest.kt
@@ -27,10 +27,12 @@ import com.instructure.student.mobius.settings.pairobserver.ui.PairObserverFragm
import com.instructure.student.mobius.syllabus.SyllabusModel
import com.instructure.student.mobius.syllabus.ui.SyllabusFragment
import com.spotify.mobius.runners.WorkRunner
+import dagger.hilt.android.testing.HiltAndroidTest
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
+@HiltAndroidTest
@RunWith(AndroidJUnit4::class)
class PairObserverRenderTest : StudentRenderTest() {
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/PickerSubmissionUploadRenderTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/PickerSubmissionUploadRenderTest.kt
index 3013ed911b..95a91eea8e 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/PickerSubmissionUploadRenderTest.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/PickerSubmissionUploadRenderTest.kt
@@ -38,9 +38,11 @@ import com.instructure.student.mobius.assignmentDetails.submission.picker.ui.Pic
import com.instructure.student.mobius.assignmentDetails.submission.picker.ui.PickerVisibilities
import com.instructure.student.ui.pages.renderPages.PickerSubmissionUploadRenderPage
import com.spotify.mobius.runners.WorkRunner
+import dagger.hilt.android.testing.HiltAndroidTest
import org.junit.Test
import org.junit.runner.RunWith
+@HiltAndroidTest
@RunWith(AndroidJUnit4::class)
class PickerSubmissionUploadRenderTest : StudentRenderTest() {
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/QuizSubmissionViewRenderTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/QuizSubmissionViewRenderTest.kt
index 5d6d365cac..fb99ae36b4 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/QuizSubmissionViewRenderTest.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/QuizSubmissionViewRenderTest.kt
@@ -22,8 +22,10 @@ import com.instructure.student.R
import com.instructure.student.espresso.StudentRenderTest
import com.instructure.student.mobius.assignmentDetails.submissionDetails.content.QuizSubmissionViewFragment
import com.instructure.student.ui.pages.renderPages.QuizSubmissionViewRenderPage
+import dagger.hilt.android.testing.HiltAndroidTest
import org.junit.Test
+@HiltAndroidTest
class QuizSubmissionViewRenderTest : StudentRenderTest() {
private val url = "https://www.google.com"
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/SubmissionCommentsRenderTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/SubmissionCommentsRenderTest.kt
index 972ea3e498..64551e8539 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/SubmissionCommentsRenderTest.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/SubmissionCommentsRenderTest.kt
@@ -38,12 +38,14 @@ import com.instructure.student.mobius.assignmentDetails.submissionDetails.drawer
import com.instructure.student.mobius.assignmentDetails.submissionDetails.ui.SubmissionDetailsTabData
import com.instructure.student.ui.pages.renderPages.SubmissionCommentsRenderPage
import com.spotify.mobius.runners.WorkRunner
+import dagger.hilt.android.testing.HiltAndroidTest
import junit.framework.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import java.util.*
+@HiltAndroidTest
@RunWith(AndroidJUnit4::class)
class SubmissionCommentsRenderTest: StudentRenderTest() {
private lateinit var user: User
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/SubmissionDetailsEmptyContentRenderTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/SubmissionDetailsEmptyContentRenderTest.kt
index 23ebf77966..40d39e788b 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/SubmissionDetailsEmptyContentRenderTest.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/SubmissionDetailsEmptyContentRenderTest.kt
@@ -25,6 +25,7 @@ import com.instructure.student.espresso.StudentRenderTest
import com.instructure.student.mobius.assignmentDetails.submissionDetails.content.emptySubmission.SubmissionDetailsEmptyContentModel
import com.instructure.student.mobius.assignmentDetails.submissionDetails.content.emptySubmission.ui.SubmissionDetailsEmptyContentFragment
import com.spotify.mobius.runners.WorkRunner
+import dagger.hilt.android.testing.HiltAndroidTest
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@@ -32,6 +33,7 @@ import org.threeten.bp.Month
import org.threeten.bp.OffsetDateTime
import org.threeten.bp.format.DateTimeFormatter
+@HiltAndroidTest
@RunWith(AndroidJUnit4::class)
class SubmissionDetailsEmptyContentRenderTest : StudentRenderTest() {
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/SubmissionDetailsRenderTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/SubmissionDetailsRenderTest.kt
index 14d955be15..9300d7e30f 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/SubmissionDetailsRenderTest.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/SubmissionDetailsRenderTest.kt
@@ -31,10 +31,12 @@ import com.instructure.student.espresso.StudentRenderTest
import com.instructure.student.mobius.assignmentDetails.submissionDetails.SubmissionDetailsModel
import com.instructure.student.mobius.assignmentDetails.submissionDetails.ui.SubmissionDetailsFragment
import com.spotify.mobius.runners.WorkRunner
+import dagger.hilt.android.testing.HiltAndroidTest
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
+@HiltAndroidTest
@RunWith(AndroidJUnit4::class)
class SubmissionDetailsRenderTest : StudentRenderTest() {
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/SubmissionFilesRenderTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/SubmissionFilesRenderTest.kt
index 3b1585b593..25a5c8ac80 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/SubmissionFilesRenderTest.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/SubmissionFilesRenderTest.kt
@@ -30,9 +30,11 @@ import com.instructure.student.mobius.assignmentDetails.submissionDetails.drawer
import com.instructure.student.mobius.assignmentDetails.submissionDetails.ui.SubmissionDetailsTabData
import com.instructure.student.ui.pages.renderPages.SubmissionFilesRenderPage
import com.spotify.mobius.runners.WorkRunner
+import dagger.hilt.android.testing.HiltAndroidTest
import org.junit.Test
import org.junit.runner.RunWith
+@HiltAndroidTest
@RunWith(AndroidJUnit4::class)
class SubmissionFilesRenderTest : StudentRenderTest() {
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/SubmissionRubricRenderTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/SubmissionRubricRenderTest.kt
index e9dc0f535e..2713887740 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/SubmissionRubricRenderTest.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/SubmissionRubricRenderTest.kt
@@ -39,10 +39,12 @@ import com.instructure.student.mobius.assignmentDetails.ui.gradeCell.GradeCellVi
import com.instructure.student.ui.pages.renderPages.SubmissionRubricRenderPage
import com.instructure.student.ui.utils.assertFontSizeSP
import com.spotify.mobius.runners.WorkRunner
+import dagger.hilt.android.testing.HiltAndroidTest
import org.hamcrest.CoreMatchers.not
import org.junit.Test
import org.junit.runner.RunWith
+@HiltAndroidTest
@RunWith(AndroidJUnit4::class)
class SubmissionRubricRenderTest : StudentRenderTest() {
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 3cf9f58d23..6d354779c1 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
@@ -24,10 +24,12 @@ import com.instructure.student.espresso.StudentRenderTest
import com.instructure.student.mobius.syllabus.SyllabusModel
import com.instructure.student.mobius.syllabus.ui.SyllabusFragment
import com.spotify.mobius.runners.WorkRunner
+import dagger.hilt.android.testing.HiltAndroidTest
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
+@HiltAndroidTest
@RunWith(AndroidJUnit4::class)
class SyllabusRenderTest : StudentRenderTest() {
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/TextSubmissionUploadRenderTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/TextSubmissionUploadRenderTest.kt
index 87ba0c05e2..afe3a91efa 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/TextSubmissionUploadRenderTest.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/TextSubmissionUploadRenderTest.kt
@@ -24,9 +24,11 @@ import com.instructure.espresso.waitForCheck
import com.instructure.student.espresso.StudentRenderTest
import com.instructure.student.mobius.assignmentDetails.submission.text.ui.TextSubmissionUploadFragment
import com.instructure.student.ui.pages.renderPages.TextSubmissionUploadRenderPage
+import dagger.hilt.android.testing.HiltAndroidTest
import org.hamcrest.CoreMatchers.not
import org.junit.Test
+@HiltAndroidTest
class TextSubmissionUploadRenderTest : StudentRenderTest() {
private val page = TextSubmissionUploadRenderPage()
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/TextSubmissionViewRenderTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/TextSubmissionViewRenderTest.kt
index e850f2d2b1..b74816a351 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/TextSubmissionViewRenderTest.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/TextSubmissionViewRenderTest.kt
@@ -19,8 +19,10 @@ import android.os.Build
import com.instructure.student.espresso.StudentRenderTest
import com.instructure.student.mobius.assignmentDetails.submissionDetails.content.TextSubmissionViewFragment
import com.instructure.student.ui.pages.renderPages.TextSubmissionViewRenderPage
+import dagger.hilt.android.testing.HiltAndroidTest
import org.junit.Test
+@HiltAndroidTest
class TextSubmissionViewRenderTest : StudentRenderTest() {
private val page = TextSubmissionViewRenderPage()
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/UploadStatusSubmissionRenderTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/UploadStatusSubmissionRenderTest.kt
index e8ed5d5d20..e5712097e7 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/UploadStatusSubmissionRenderTest.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/UploadStatusSubmissionRenderTest.kt
@@ -26,10 +26,12 @@ import com.instructure.student.espresso.StudentRenderTest
import com.instructure.student.mobius.assignmentDetails.submission.file.UploadStatusSubmissionModel
import com.instructure.student.mobius.assignmentDetails.submission.file.ui.UploadStatusSubmissionFragment
import com.spotify.mobius.runners.WorkRunner
+import dagger.hilt.android.testing.HiltAndroidTest
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
+@HiltAndroidTest
@RunWith(AndroidJUnit4::class)
class UploadStatusSubmissionRenderTest : StudentRenderTest() {
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/UrlSubmissionUploadRenderTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/UrlSubmissionUploadRenderTest.kt
index cfe792898f..4b1890beff 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/UrlSubmissionUploadRenderTest.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/UrlSubmissionUploadRenderTest.kt
@@ -25,9 +25,11 @@ import com.instructure.espresso.replaceText
import com.instructure.espresso.waitForCheck
import com.instructure.student.espresso.StudentRenderTest
import com.instructure.student.mobius.assignmentDetails.submission.url.ui.UrlSubmissionUploadFragment
+import dagger.hilt.android.testing.HiltAndroidTest
import org.hamcrest.Matchers.not
import org.junit.Test
+@HiltAndroidTest
class UrlSubmissionUploadRenderTest : StudentRenderTest() {
private val testUrl = "https://www.instructure.com"
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/UrlSubmissionViewRenderTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/UrlSubmissionViewRenderTest.kt
index 67ef6d3035..4cc7c834ea 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/UrlSubmissionViewRenderTest.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/UrlSubmissionViewRenderTest.kt
@@ -19,8 +19,10 @@ import com.instructure.espresso.assertCompletelyDisplayed
import com.instructure.espresso.assertHasText
import com.instructure.student.espresso.StudentRenderTest
import com.instructure.student.mobius.assignmentDetails.submissionDetails.content.UrlSubmissionViewFragment
+import dagger.hilt.android.testing.HiltAndroidTest
import org.junit.Test
+@HiltAndroidTest
class UrlSubmissionViewRenderTest : StudentRenderTest() {
private val testUrl = "https://www.instructure.com"
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/views/GradeCellRenderTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/views/GradeCellRenderTest.kt
index c7f676f957..432f6a7c6c 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/views/GradeCellRenderTest.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/views/GradeCellRenderTest.kt
@@ -23,9 +23,11 @@ import com.instructure.student.R
import com.instructure.student.espresso.StudentRenderTest
import com.instructure.student.mobius.assignmentDetails.ui.gradeCell.GradeCellView
import com.instructure.student.mobius.assignmentDetails.ui.gradeCell.GradeCellViewState
+import dagger.hilt.android.testing.HiltAndroidTest
import org.junit.Test
import org.junit.runner.RunWith
+@HiltAndroidTest
@RunWith(AndroidJUnit4::class)
class GradeCellRenderTest : StudentRenderTest() {
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 ba23e60e0f..6181b9b93f 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
@@ -17,7 +17,6 @@
package com.instructure.student.ui.utils
import android.app.Activity
-import android.content.Context
import android.os.Environment
import androidx.test.espresso.Espresso
import android.view.View
@@ -26,22 +25,33 @@ import androidx.test.espresso.ViewAction
import androidx.test.espresso.matcher.ViewMatchers
import com.instructure.canvas.espresso.CanvasTest
import com.instructure.espresso.InstructureActivityTestRule
+import com.instructure.espresso.ScreenshotTestRule
import com.instructure.espresso.swipeRight
import com.instructure.student.BuildConfig
import com.instructure.student.R
import com.instructure.student.activity.LoginActivity
import com.instructure.student.ui.pages.*
+import dagger.hilt.android.testing.HiltAndroidRule
+import dagger.hilt.android.testing.HiltAndroidTest
+import dagger.hilt.processor.internal.aggregateddeps.AggregatedDeps
import instructure.rceditor.RCETextEditor
import org.hamcrest.Matcher
import org.junit.Before
+import org.junit.Rule
+import org.junit.rules.RuleChain
+import org.junit.rules.TestRule
import java.io.File
abstract class StudentTest : CanvasTest() {
override val activityRule: InstructureActivityTestRule =
StudentActivityTestRule(LoginActivity::class.java)
+
lateinit var originalActivity : Activity
+ @get:Rule(order = 0)
+ var hiltRule = HiltAndroidRule(this)
+
// Sometimes activityRule.activity can get nulled out over time, probably as we
// navigate away from the original login screen. Capture the activity here so
// that we can reference it safely later.
@@ -55,49 +65,54 @@ abstract class StudentTest : CanvasTest() {
/**
* Required for auto complete of page objects within tests
*/
+ val annotationCommentListPage = AnnotationCommentListPage()
+ val announcementListPage = AnnouncementListPage()
+ val assignmentDetailsPage = AssignmentDetailsPage()
val assignmentListPage = AssignmentListPage()
- val dashboardPage = DashboardPage()
- val allCoursesPage = AllCoursesPage()
- val editFavoritesPage = EditFavoritesPage()
+ val bookmarkPage = BookmarkPage()
+ val calendarEventPage = CalendarEventPage()
val calendarPage = CalendarPage()
- val todoPage = TodoPage()
- val inboxPage = InboxPage()
+ val canvasWebViewPage = CanvasWebViewPage()
+ val courseBrowserPage = CourseBrowserPage()
+ val courseGradesPage = CourseGradesPage()
+ val dashboardPage = DashboardPage()
+ val discussionDetailsPage = DiscussionDetailsPage()
+ val discussionListPage = DiscussionListPage()
+ val editDashboardPage = EditDashboardPage()
+ val fileListPage = FileListPage()
+ val fileUploadPage = FileUploadPage()
+ val helpPage = HelpPage()
val inboxConversationPage = InboxConversationPage()
- val newMessagePage = NewMessagePage()
- val settingsPage = SettingsPage()
- val pairObserverPage = PairObserverPage()
+ val inboxPage = InboxPage()
val legalPage = LegalPage()
- val helpPage = HelpPage()
val loginFindSchoolPage = LoginFindSchoolPage()
val loginLandingPage = LoginLandingPage()
val loginSignInPage = LoginSignInPage()
- val qrLoginPage = QRLoginPage()
- val courseBrowserPage = CourseBrowserPage()
- val assignmentDetailsPage = AssignmentDetailsPage()
- val submissionDetailsPage = SubmissionDetailsPage()
- val peopleListPage = PeopleListPage()
- val personDetailsPage = PersonDetailsPage()
+ val moduleProgressionPage = ModuleProgressionPage()
val modulesPage = ModulesPage()
- val syllabusPage = SyllabusPage()
- val fileListPage = FileListPage()
- val discussionListPage = DiscussionListPage()
- val discussionDetailsPage = DiscussionDetailsPage()
+ val newMessagePage = NewMessagePage()
+ val notificationPage = NotificationPage()
val pageListPage = PageListPage()
- val quizListPage = QuizListPage()
- val urlSubmissionUploadPage = UrlSubmissionUploadPage()
- val courseGradesPage = CourseGradesPage()
- val moduleProgressionPage = ModuleProgressionPage()
- val canvasWebViewPage = CanvasWebViewPage()
- val fileUploadPage = FileUploadPage()
- val annotationCommentListPage = AnnotationCommentListPage()
+ val pairObserverPage = PairObserverPage()
+ val pandaAvatarPage = PandaAvatarPage()
+ val peopleListPage = PeopleListPage()
+ val personDetailsPage = PersonDetailsPage()
val pickerSubmissionUploadPage = PickerSubmissionUploadPage()
- val remoteConfigSettingsPage = RemoteConfigSettingsPage()
val profileSettingsPage = ProfileSettingsPage()
- val calendarEventPage = CalendarEventPage()
+ val qrLoginPage = QRLoginPage()
+ val quizListPage = QuizListPage()
val quizTakingPage = QuizTakingPage()
- val pandaAvatarPage = PandaAvatarPage()
- val notificationPage = NotificationPage()
- val bookmarkPage = BookmarkPage()
+ val remoteConfigSettingsPage = RemoteConfigSettingsPage()
+ val settingsPage = SettingsPage()
+ val submissionDetailsPage = SubmissionDetailsPage()
+ val syllabusPage = SyllabusPage()
+ val todoPage = TodoPage()
+ val urlSubmissionUploadPage = UrlSubmissionUploadPage()
+ val elementaryDashboardPage = ElementaryDashboardPage()
+ val homeroomPage = HomeroomPage()
+ val schedulePage = SchedulePage()
+ val gradesPage = GradesPage()
+ val resourcesPage = ResourcesPage()
// 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/androidTest/java/com/instructure/student/ui/utils/StudentTestExtensions.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/utils/StudentTestExtensions.kt
index 1030ee7b53..7a9107b612 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/utils/StudentTestExtensions.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/utils/StudentTestExtensions.kt
@@ -131,6 +131,21 @@ fun StudentTest.tokenLogin(domain: String, token: String, user: User) {
dashboardPage.assertPageObjects()
}
+fun StudentTest.tokenLoginElementary(domain: String, token: String, user: User) {
+ activityRule.runOnUiThread {
+ (originalActivity as LoginActivity).loginWithToken(
+ token,
+ domain,
+ user,
+ canvasForElementary = true
+ )
+ }
+ // Sometimes, especially on slow FTL emulators, it can take a bit for the dashboard to show
+ // up after a token login. Add some tolerance for that.
+ waitForMatcherWithSleeps(withId(R.id.elementaryDashboardPage), 20000).check(matches(isDisplayed()))
+ elementaryDashboardPage.assertPageObjects()
+}
+
fun StudentTest.routeTo(route: String) {
val url = "canvas-student://${CanvasRestAdapter.canvasDomain}/$route"
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/utils/TestDateTimeModule.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/utils/TestDateTimeModule.kt
new file mode 100644
index 0000000000..7e18e00d08
--- /dev/null
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/utils/TestDateTimeModule.kt
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2021 - 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.utils
+
+import com.instructure.pandautils.di.DateTimeModule
+import com.instructure.pandautils.utils.date.DateTimeProvider
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.components.SingletonComponent
+import dagger.hilt.testing.TestInstallIn
+import java.util.*
+import javax.inject.Singleton
+
+@Module
+@TestInstallIn(components = [SingletonComponent::class], replaces = [DateTimeModule::class])
+class TestDateTimeModule {
+
+ @Provides
+ @Singleton
+ fun provideDateTimeProvider(): DateTimeProvider {
+ return FakeDateTimeProvider()
+ }
+}
+
+class FakeDateTimeProvider : DateTimeProvider {
+
+ var fakeTimeInMillis: Long = Calendar.getInstance().timeInMillis
+
+ override fun getCalendar(): Calendar {
+ return Calendar.getInstance().apply {
+ timeInMillis = fakeTimeInMillis
+ }
+ }
+}
\ No newline at end of file
diff --git a/apps/student/src/main/AndroidManifest.xml b/apps/student/src/main/AndroidManifest.xml
index dcacead26f..9c2f96dc18 100644
--- a/apps/student/src/main/AndroidManifest.xml
+++ b/apps/student/src/main/AndroidManifest.xml
@@ -63,7 +63,6 @@
android:hardwareAccelerated="true"
android:supportsRtl="true"
android:largeHeap="true"
- android:usesCleartextTraffic="true"
android:requestLegacyExternalStorage="true"
android:networkSecurityConfig="@xml/network_security_config"
tools:replace="android:supportsRtl"
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 295f80b83c..44d68499e5 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
@@ -20,7 +20,6 @@ package com.instructure.student.activity
import android.content.ActivityNotFoundException
import android.content.Intent
import android.os.Bundle
-import android.os.Handler
import android.widget.Toast
import androidx.loader.app.LoaderManager
import androidx.loader.content.Loader
@@ -28,10 +27,11 @@ import com.instructure.canvasapi2.StatusCallback
import com.instructure.canvasapi2.managers.FileFolderManager
import com.instructure.canvasapi2.models.CanvasContext
import com.instructure.canvasapi2.models.FileFolder
-import com.instructure.canvasapi2.utils.*
+import com.instructure.canvasapi2.utils.ApiType
+import com.instructure.canvasapi2.utils.LinkHeaders
+import com.instructure.canvasapi2.utils.Logger
import com.instructure.interactions.FullScreenInteractions
import com.instructure.interactions.router.Route
-import com.instructure.loginapi.login.tasks.LogoutTask
import com.instructure.pandautils.loaders.OpenMediaAsyncTaskLoader
import com.instructure.pandautils.models.PushNotification
import com.instructure.pandautils.receivers.PushExternalReceiver
@@ -41,9 +41,7 @@ import com.instructure.pandautils.utils.toast
import com.instructure.student.R
import com.instructure.student.fragment.InternalWebviewFragment
import com.instructure.student.router.RouteMatcher
-import com.instructure.student.tasks.StudentLogoutTask
import com.instructure.student.util.FileUtils
-import com.jakewharton.processphoenix.ProcessPhoenix
import kotlinx.coroutines.Job
//Intended to handle all routing to fragments from links both internal and external
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 41a934c58a..d39e0c2b4b 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
@@ -75,6 +75,10 @@ abstract class CallbackActivity : ParentActivity(), InboxFragment.OnUnreadCountI
}
}
+ val termsOfService = awaitApi { UserManager.getTermsOfService(it, true) }
+ ApiPrefs.canGeneratePairingCode = termsOfService.selfRegistrationType == SelfRegistration.ALL
+ || termsOfService.selfRegistrationType == SelfRegistration.OBSERVER
+
// Grab colors
if (ColorKeeper.hasPreviouslySynced) {
UserManager.getColors(userColorsCallback, true)
diff --git a/apps/student/src/main/java/com/instructure/student/activity/InterwebsToApplication.kt b/apps/student/src/main/java/com/instructure/student/activity/InterwebsToApplication.kt
index b78ad83680..dce5de2644 100644
--- a/apps/student/src/main/java/com/instructure/student/activity/InterwebsToApplication.kt
+++ b/apps/student/src/main/java/com/instructure/student/activity/InterwebsToApplication.kt
@@ -27,6 +27,7 @@ import android.view.View
import android.view.Window
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
+import com.instructure.canvasapi2.managers.FeaturesManager
import com.instructure.canvasapi2.models.AccountDomain
import com.instructure.canvasapi2.utils.*
import com.instructure.canvasapi2.utils.weave.catch
@@ -34,18 +35,29 @@ import com.instructure.canvasapi2.utils.weave.tryWeave
import com.instructure.loginapi.login.tasks.LogoutTask
import com.instructure.loginapi.login.util.QRLogin.performSSOLogin
import com.instructure.loginapi.login.util.QRLogin.verifySSOLoginUri
+import com.instructure.pandautils.typeface.TypefaceBehavior
import com.instructure.pandautils.utils.Const
+import com.instructure.pandautils.utils.FeatureFlagProvider
import com.instructure.pandautils.utils.Utils.generateUserAgent
import com.instructure.student.R
import com.instructure.student.router.RouteMatcher
import com.instructure.student.tasks.StudentLogoutTask
import com.instructure.student.util.LoggingUtility
+import dagger.hilt.android.AndroidEntryPoint
import kotlinx.android.synthetic.main.loading_canvas_view.*
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
+import javax.inject.Inject
+@AndroidEntryPoint
class InterwebsToApplication : AppCompatActivity() {
+ @Inject
+ lateinit var featureFlagProvider: FeatureFlagProvider
+
+ @Inject
+ lateinit var typefaceBehavior: TypefaceBehavior
+
private var loadingJob: Job? = null
public override fun onCreate(savedInstanceState: Bundle?) {
@@ -86,7 +98,7 @@ class InterwebsToApplication : AppCompatActivity() {
// This is an App Link from a QR code, let's try to login the user and launch navigationActivity
try {
if(signedIn) { // If the user is already signed in, use the QR Switch
- StudentLogoutTask(type = LogoutTask.Type.QR_CODE_SWITCH, uri = data).execute()
+ StudentLogoutTask(type = LogoutTask.Type.QR_CODE_SWITCH, uri = data, canvasForElementaryFeatureFlag = featureFlagProvider.getCanvasForElementaryFlag(), typefaceBehavior = typefaceBehavior).execute()
finish()
return@tryWeave
}
@@ -97,6 +109,7 @@ class InterwebsToApplication : AppCompatActivity() {
val tokenResponse = performSSOLogin(data, this@InterwebsToApplication)
+ val canvasForElementary = featureFlagProvider.getCanvasForElementaryFlag()
// Add delay for animation and launch Navigation Activity
delay(700)
@@ -113,6 +126,7 @@ class InterwebsToApplication : AppCompatActivity() {
}
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
+ intent.putExtra("canvas_for_elementary", canvasForElementary)
startActivity(intent)
finish()
return@tryWeave
@@ -145,9 +159,11 @@ class InterwebsToApplication : AppCompatActivity() {
}
if (signedIn && !domain.contains(host)) {
+ val canvasForElementary = featureFlagProvider.getCanvasForElementaryFlag()
delay(700)
val intent = Intent(this@InterwebsToApplication, NavigationActivity.startActivityClass)
intent.putExtra(Const.MESSAGE, getString(R.string.differentDomainFromLink))
+ intent.putExtra("canvas_for_elementary", canvasForElementary)
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK)
startActivity(intent)
finish()
diff --git a/apps/student/src/main/java/com/instructure/student/activity/LoginActivity.kt b/apps/student/src/main/java/com/instructure/student/activity/LoginActivity.kt
index a69f2b6bb9..5b4771fed3 100644
--- a/apps/student/src/main/java/com/instructure/student/activity/LoginActivity.kt
+++ b/apps/student/src/main/java/com/instructure/student/activity/LoginActivity.kt
@@ -26,18 +26,21 @@ import com.instructure.canvasapi2.models.User
import com.instructure.canvasapi2.utils.ApiPrefs
import com.instructure.interactions.router.Route
import com.instructure.loginapi.login.activities.BaseLoginInitActivity
+import com.instructure.loginapi.login.tasks.LogoutTask
import com.instructure.loginapi.login.util.QRLogin
-import com.instructure.pandautils.services.PushNotificationRegistrationService
+import com.instructure.pandautils.services.PushNotificationRegistrationWorker
import com.instructure.pandautils.utils.Const
import com.instructure.pandautils.utils.Utils
import com.instructure.student.BuildConfig
import com.instructure.student.R
+import com.instructure.student.tasks.StudentLogoutTask
+import dagger.hilt.android.AndroidEntryPoint
-
+@AndroidEntryPoint
class LoginActivity : BaseLoginInitActivity() {
override fun launchApplicationMainActivityIntent(): Intent {
- PushNotificationRegistrationService.scheduleJob(this, ApiPrefs.isMasquerading)
+ PushNotificationRegistrationWorker.scheduleJob(this, ApiPrefs.isMasquerading)
CookieManager.getInstance().flush()
@@ -65,6 +68,10 @@ class LoginActivity : BaseLoginInitActivity() {
override val isTesting: Boolean = BuildConfig.IS_TESTING
+ override fun logout() {
+ StudentLogoutTask(LogoutTask.Type.LOGOUT).execute()
+ }
+
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -79,13 +86,16 @@ class LoginActivity : BaseLoginInitActivity() {
* ONLY USE FOR UI TESTING
* Skips the traditional login process by directly setting the domain, token, and user info.
*/
- fun loginWithToken(token: String, domain: String, user: User) {
+ fun loginWithToken(token: String, domain: String, user: User, canvasForElementary: Boolean = false) {
ApiPrefs.accessToken = token
ApiPrefs.domain = domain
ApiPrefs.user = user
ApiPrefs.userAgent = Utils.generateUserAgent(this, userAgent())
finish()
- val intent = Intent(this, NavigationActivity.startActivityClass).apply { intent?.extras?.let { putExtras(it) } }
+ val intent = Intent(this, NavigationActivity.startActivityClass).apply {
+ intent?.extras?.let { putExtras(it) }
+ putExtra("canvas_for_elementary", canvasForElementary)
+ }
startActivity(intent)
}
diff --git a/apps/student/src/main/java/com/instructure/student/activity/LoginLandingPageActivity.kt b/apps/student/src/main/java/com/instructure/student/activity/LoginLandingPageActivity.kt
index 37bf8d9012..23048877c9 100644
--- a/apps/student/src/main/java/com/instructure/student/activity/LoginLandingPageActivity.kt
+++ b/apps/student/src/main/java/com/instructure/student/activity/LoginLandingPageActivity.kt
@@ -26,12 +26,14 @@ import com.instructure.canvasapi2.models.AccountDomain
import com.instructure.canvasapi2.utils.ApiPrefs
import com.instructure.loginapi.login.activities.BaseLoginLandingPageActivity
import com.instructure.loginapi.login.snicker.SnickerDoodle
-import com.instructure.pandautils.services.PushNotificationRegistrationService
+import com.instructure.pandautils.services.PushNotificationRegistrationWorker
+import dagger.hilt.android.AndroidEntryPoint
+@AndroidEntryPoint
class LoginLandingPageActivity : BaseLoginLandingPageActivity() {
override fun launchApplicationMainActivityIntent(): Intent {
- PushNotificationRegistrationService.scheduleJob(this, ApiPrefs.isMasquerading)
+ PushNotificationRegistrationWorker.scheduleJob(this, ApiPrefs.isMasquerading)
CookieManager.getInstance().flush()
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 be6bf223f4..b5264e3f96 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
@@ -24,10 +24,12 @@ import android.content.pm.PackageManager
import android.content.res.Configuration
import android.graphics.Color
import android.graphics.Typeface
-import android.net.Uri
import android.os.Bundle
import android.os.Handler
-import android.view.*
+import android.view.LayoutInflater
+import android.view.MenuItem
+import android.view.View
+import android.view.ViewGroup
import android.widget.CompoundButton
import android.widget.ImageView
import android.widget.TextView
@@ -61,11 +63,15 @@ import com.instructure.interactions.router.Route
import com.instructure.interactions.router.RouteContext
import com.instructure.interactions.router.RouteType
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.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.student.R
import com.instructure.student.dialog.BookmarkCreationDialog
@@ -75,12 +81,17 @@ import com.instructure.student.fragment.*
import com.instructure.student.mobius.assignmentDetails.submission.picker.PickerSubmissionUploadEffectHandler
import com.instructure.student.mobius.assignmentDetails.submissionDetails.content.emptySubmission.ui.SubmissionDetailsEmptyContentFragment
import com.instructure.student.mobius.assignmentDetails.ui.AssignmentDetailsFragment
+import com.instructure.student.navigation.AccountMenuItem
+import com.instructure.student.navigation.NavigationBehavior
+import com.instructure.student.navigation.NavigationMenuItem
+import com.instructure.student.navigation.OptionsMenuItem
import com.instructure.student.router.RouteMatcher
import com.instructure.student.router.RouteResolver
import com.instructure.student.tasks.StudentLogoutTask
import com.instructure.student.util.Analytics
import com.instructure.student.util.AppShortcutManager
import com.instructure.student.util.StudentPrefs
+import dagger.hilt.android.AndroidEntryPoint
import kotlinx.android.synthetic.main.activity_navigation.*
import kotlinx.android.synthetic.main.loading_canvas_view.*
import kotlinx.android.synthetic.main.navigation_drawer.*
@@ -88,10 +99,28 @@ import kotlinx.coroutines.*
import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode
+import java.util.*
+import javax.inject.Inject
+private const val BOTTOM_NAV_SCREEN = "bottomNavScreen"
+
+@AndroidEntryPoint
@Suppress("DELEGATED_MEMBER_HIDES_SUPERTYPE_OVERRIDE")
class NavigationActivity : BaseRouterActivity(), Navigation, MasqueradingDialog.OnMasqueradingSet,
- FullScreenInteractions, ActivityCompat.OnRequestPermissionsResultCallback by PermissionReceiver() {
+ FullScreenInteractions, ActivityCompat.OnRequestPermissionsResultCallback by PermissionReceiver(),
+ ErrorReportDialog.ErrorReportDialogResultListener {
+
+ @Inject
+ lateinit var navigationBehavior: NavigationBehavior
+
+ @Inject
+ lateinit var appShortcutManager: AppShortcutManager
+
+ @Inject
+ lateinit var typefaceBehavior: TypefaceBehavior
+
+ @Inject
+ lateinit var updateManager: UpdateManager
private var routeJob: WeaveJob? = null
private var debounceJob: Job? = null
@@ -99,14 +128,7 @@ class NavigationActivity : BaseRouterActivity(), Navigation, MasqueradingDialog.
private var mDrawerToggle: ActionBarDrawerToggle? = null
private var colorOverlayJob: Job? = null
- /** 'Root' fragments that should include the bottom nav bar */
- private val bottomNavBarFragments = listOf(
- DashboardFragment::class.java,
- CalendarFragment::class.java,
- ToDoListFragment::class.java,
- NotificationListFragment::class.java,
- InboxFragment::class.java
- )
+ private val bottomNavScreensStack: Deque = ArrayDeque()
override fun contentResId(): Int = R.layout.activity_navigation
@@ -118,6 +140,9 @@ class NavigationActivity : BaseRouterActivity(), Navigation, MasqueradingDialog.
closeNavigationDrawer()
delay(250)
when (v.id) {
+ R.id.navigationDrawerItem_help -> {
+ HelpDialogFragment.show(this@NavigationActivity)
+ }
R.id.navigationDrawerItem_files -> {
ApiPrefs.user?.let { handleRoute(FileListFragment.makeRoute(it)) }
}
@@ -141,13 +166,13 @@ class NavigationActivity : BaseRouterActivity(), Navigation, MasqueradingDialog.
}, route)
}
R.id.navigationDrawerItem_changeUser -> {
- StudentLogoutTask(if (ApiPrefs.isStudentView) LogoutTask.Type.LOGOUT else LogoutTask.Type.SWITCH_USERS).execute()
+ StudentLogoutTask(if (ApiPrefs.isStudentView) LogoutTask.Type.LOGOUT else LogoutTask.Type.SWITCH_USERS, typefaceBehavior = typefaceBehavior).execute()
}
R.id.navigationDrawerItem_logout -> {
AlertDialog.Builder(this@NavigationActivity)
.setTitle(R.string.logout_warning)
.setPositiveButton(android.R.string.yes) { _, _ ->
- StudentLogoutTask(LogoutTask.Type.LOGOUT).execute()
+ StudentLogoutTask(LogoutTask.Type.LOGOUT, typefaceBehavior = typefaceBehavior).execute()
}
.setNegativeButton(android.R.string.no, null)
.create()
@@ -173,7 +198,7 @@ class NavigationActivity : BaseRouterActivity(), Navigation, MasqueradingDialog.
/* Update nav bar visibility to show for specific 'root' fragments. Also show the nav bar when there is
only one fragment on the backstack, which commonly occurs with non-root fragments when routing
from external sources. */
- val visible = it::class.java in bottomNavBarFragments || supportFragmentManager.backStackEntryCount <= 1
+ val visible = isBottomNavFragment(it) || supportFragmentManager.backStackEntryCount <= 1
bottomBar.setVisible(visible)
bottomBarDivider.setVisible(visible)
}
@@ -184,6 +209,10 @@ class NavigationActivity : BaseRouterActivity(), Navigation, MasqueradingDialog.
applyCurrentFragmentTheme()
}
+ private fun checkAppUpdates() {
+ updateManager.checkForInAppUpdate(this)
+ }
+
private fun applyCurrentFragmentTheme() {
Handler().post {
(currentFragment as? FragmentInteractions)?.let {
@@ -200,6 +229,8 @@ class NavigationActivity : BaseRouterActivity(), Navigation, MasqueradingDialog.
MasqueradeHelper.startMasquerading(masqueradingUserId, ApiPrefs.domain, NavigationActivity::class.java)
}
+ bottomBar.inflateMenu(navigationBehavior.bottomBarMenu)
+
supportFragmentManager.addOnBackStackChangedListener(onBackStackChangedListener)
if (savedInstanceState == null) {
@@ -208,7 +239,27 @@ class NavigationActivity : BaseRouterActivity(), Navigation, MasqueradingDialog.
}
}
- AppShortcutManager.make(this)
+ appShortcutManager.make(this)
+
+ setupNavDrawerItems()
+
+ checkAppUpdates()
+ }
+
+ private fun setupNavDrawerItems() {
+ navigationDrawerItem_files.setVisible(navigationBehavior.visibleNavigationMenuItems.contains(NavigationMenuItem.FILES))
+ navigationDrawerItem_bookmarks.setVisible(navigationBehavior.visibleNavigationMenuItems.contains(NavigationMenuItem.BOOKMARKS))
+ navigationDrawerSettings.setVisible(navigationBehavior.visibleNavigationMenuItems.contains(NavigationMenuItem.SETTINGS))
+ navigationMenuItemsDivider.setVisible(navigationBehavior.visibleNavigationMenuItems.isNotEmpty())
+
+ optionsMenuTitle.setVisible(navigationBehavior.visibleOptionsMenuItems.isNotEmpty())
+ navigationDrawerItem_showGrades.setVisible(navigationBehavior.visibleOptionsMenuItems.contains(OptionsMenuItem.SHOW_GRADES))
+ navigationDrawerItem_colorOverlay.setVisible(navigationBehavior.visibleOptionsMenuItems.contains(OptionsMenuItem.COLOR_OVERLAY))
+ optionsMenuItemsDivider.setVisible(navigationBehavior.visibleOptionsMenuItems.isNotEmpty())
+
+ navigationDrawerItem_help.setVisible(navigationBehavior.visibleAccountMenuItems.contains(AccountMenuItem.HELP))
+ navigationDrawerItem_changeUser.setVisible(navigationBehavior.visibleAccountMenuItems.contains(AccountMenuItem.CHANGE_USER))
+ navigationDrawerItem_logout.setVisible(navigationBehavior.visibleAccountMenuItems.contains(AccountMenuItem.LOGOUT))
}
override fun initialCoreDataLoadingComplete() {
@@ -268,9 +319,9 @@ class NavigationActivity : BaseRouterActivity(), Navigation, MasqueradingDialog.
}
override fun loadLandingPage(clearBackStack: Boolean) {
- if (clearBackStack) clearBackStack(DashboardFragment::class.java)
- val dashboardRoute = DashboardFragment.makeRoute(ApiPrefs.user)
- addFragment(DashboardFragment.newInstance(dashboardRoute), dashboardRoute)
+ if (clearBackStack) clearBackStack(navigationBehavior.homeFragmentClass)
+ selectBottomNavFragment(navigationBehavior.homeFragmentClass)
+ bottomNavScreensStack.clear()
if (intent.extras?.containsKey(AppShortcutManager.APP_SHORTCUT_PLACEMENT) == true) {
// Launch to the app shortcut placement
@@ -284,26 +335,15 @@ class NavigationActivity : BaseRouterActivity(), Navigation, MasqueradingDialog.
val route = BookmarksFragment.makeRoute(ApiPrefs.user)
addFragment(BookmarksFragment.newInstance(route) { RouteMatcher.routeUrl(this, it.url!!) }, route)
}
- AppShortcutManager.APP_SHORTCUT_CALENDAR -> {
- val route = CalendarFragment.makeRoute()
- addFragment(CalendarFragment.newInstance(route), route)
- }
- AppShortcutManager.APP_SHORTCUT_TODO -> {
- val route = ToDoListFragment.makeRoute(ApiPrefs.user!!)
- addFragment(ToDoListFragment.newInstance(route), route)
- }
- AppShortcutManager.APP_SHORTCUT_NOTIFICATIONS -> {
- val route = NotificationListFragment.makeRoute(ApiPrefs.user!!)
- addFragment(NotificationListFragment.newInstance(route), route)
- }
+ AppShortcutManager.APP_SHORTCUT_CALENDAR -> selectBottomNavFragment(CalendarFragment::class.java)
+ AppShortcutManager.APP_SHORTCUT_TODO -> selectBottomNavFragment(ToDoListFragment::class.java)
+ AppShortcutManager.APP_SHORTCUT_NOTIFICATIONS -> selectBottomNavFragment(NotificationListFragment::class.java)
AppShortcutManager.APP_SHORTCUT_INBOX -> {
if (ApiPrefs.isStudentView) {
// Inbox not available in Student View
- val route = NothingToSeeHereFragment.makeRoute()
- addFragment(NothingToSeeHereFragment.newInstance(), route)
+ selectBottomNavFragment(NothingToSeeHereFragment::class.java)
} else {
- val route = InboxFragment.makeRoute()
- addFragment(InboxFragment.newInstance(route), route)
+ selectBottomNavFragment(InboxFragment::class.java)
}
}
}
@@ -374,15 +414,13 @@ class NavigationActivity : BaseRouterActivity(), Navigation, MasqueradingDialog.
}
override fun attachNavigationDrawer(fragment: F, toolbar: Toolbar) where F : Fragment, F : FragmentInteractions {
- ColorUtils.colorIt(ThemePrefs.primaryColor, navigationDrawerInstitutionImage.background)
- navigationDrawerInstitutionImage.loadUri(Uri.parse(ThemePrefs.logoUrl), R.mipmap.ic_launcher_foreground)
-
//Navigation items
navigationDrawerItem_files.setOnClickListener(mNavigationDrawerItemClickListener)
navigationDrawerItem_gauge.setOnClickListener(mNavigationDrawerItemClickListener)
navigationDrawerItem_studio.setOnClickListener(mNavigationDrawerItemClickListener)
navigationDrawerItem_bookmarks.setOnClickListener(mNavigationDrawerItemClickListener)
navigationDrawerItem_changeUser.setOnClickListener(mNavigationDrawerItemClickListener)
+ navigationDrawerItem_help.setOnClickListener(mNavigationDrawerItemClickListener)
navigationDrawerItem_logout.setOnClickListener(mNavigationDrawerItemClickListener)
navigationDrawerSettings.setOnClickListener(mNavigationDrawerItemClickListener)
navigationDrawerItem_startMasquerading.setOnClickListener(mNavigationDrawerItemClickListener)
@@ -408,10 +446,14 @@ class NavigationActivity : BaseRouterActivity(), Navigation, MasqueradingDialog.
Logger.e("Error getting version: " + e)
}
- toolbar.setNavigationIcon(R.drawable.ic_hamburger)
- toolbar.navigationContentDescription = getString(R.string.navigation_drawer_open)
- toolbar.setNavigationOnClickListener {
- openNavigationDrawer()
+ if (isBottomNavFragment(fragment)) {
+ toolbar.setNavigationIcon(R.drawable.ic_hamburger)
+ toolbar.navigationContentDescription = getString(R.string.navigation_drawer_open)
+ toolbar.setNavigationOnClickListener {
+ openNavigationDrawer()
+ }
+ } else {
+ toolbar.setupAsBackButton(fragment)
}
drawerLayout.setDrawerShadow(R.drawable.drawer_shadow, GravityCompat.START)
@@ -489,30 +531,28 @@ class NavigationActivity : BaseRouterActivity(), Navigation, MasqueradingDialog.
toast(R.string.fileQuotaExceeded)
}
+ override fun overrideFont() {
+ super.overrideFont()
+ if (navigationBehavior.shouldOverrideFont) {
+ typefaceBehavior.overrideFont()
+ }
+ }
+
//endregion
//region Bottom Bar Navigation
private val bottomBarItemSelectedListener = BottomNavigationView.OnNavigationItemSelectedListener { item: MenuItem ->
when (item.itemId) {
- R.id.bottomNavigationCourses -> handleRoute(Route(DashboardFragment::class.java, ApiPrefs.user))
- R.id.bottomNavigationCalendar -> handleRoute(CalendarFragment.makeRoute())
- R.id.bottomNavigationToDo -> {
- val route = ToDoListFragment.makeRoute(ApiPrefs.user!!)
- addFragment(ToDoListFragment.newInstance(route), route)
- }
- R.id.bottomNavigationNotifications ->{
- val route = NotificationListFragment.makeRoute(ApiPrefs.user!!)
- addFragment(NotificationListFragment.newInstance(route), route)
- }
+ R.id.bottomNavigationHome -> selectBottomNavFragment(navigationBehavior.homeFragmentClass)
+ R.id.bottomNavigationCalendar -> selectBottomNavFragment(CalendarFragment::class.java)
+ R.id.bottomNavigationToDo -> selectBottomNavFragment(ToDoListFragment::class.java)
+ R.id.bottomNavigationNotifications -> selectBottomNavFragment(NotificationListFragment::class.java)
R.id.bottomNavigationInbox -> {
if (ApiPrefs.isStudentView) {
- // Inbox not available in Student View
- val route = NothingToSeeHereFragment.makeRoute()
- addFragment(NothingToSeeHereFragment.newInstance(), route)
+ selectBottomNavFragment(NothingToSeeHereFragment::class.java)
} else {
- val route = InboxFragment.makeRoute()
- addFragment(InboxFragment.newInstance(route), route)
+ selectBottomNavFragment(InboxFragment::class.java)
}
}
}
@@ -526,7 +566,7 @@ class NavigationActivity : BaseRouterActivity(), Navigation, MasqueradingDialog.
topFragment?.let {
val currentFragmentClass = it::class.java
when (item.itemId) {
- R.id.bottomNavigationCourses -> abortReselect = currentFragmentClass.isAssignableFrom(DashboardFragment::class.java)
+ R.id.bottomNavigationHome -> abortReselect = currentFragmentClass.isAssignableFrom(navigationBehavior.homeFragmentClass)
R.id.bottomNavigationCalendar -> abortReselect = currentFragmentClass.isAssignableFrom(CalendarFragment::class.java)
R.id.bottomNavigationToDo -> abortReselect = currentFragmentClass.isAssignableFrom(ToDoListFragment::class.java)
R.id.bottomNavigationNotifications -> abortReselect = currentFragmentClass.isAssignableFrom(NotificationListFragment::class.java)
@@ -536,24 +576,15 @@ class NavigationActivity : BaseRouterActivity(), Navigation, MasqueradingDialog.
if(!abortReselect) {
when (item.itemId) {
- R.id.bottomNavigationCourses -> handleRoute(Route(DashboardFragment::class.java, ApiPrefs.user))
- R.id.bottomNavigationCalendar -> handleRoute(CalendarFragment.makeRoute())
- R.id.bottomNavigationToDo -> {
- val route = ToDoListFragment.makeRoute(ApiPrefs.user!!)
- addFragment(ToDoListFragment.newInstance(route), route)
- }
- R.id.bottomNavigationNotifications -> {
- val route = NotificationListFragment.makeRoute(ApiPrefs.user!!)
- addFragment(NotificationListFragment.newInstance(route), route)
- }
+ R.id.bottomNavigationHome -> selectBottomNavFragment(navigationBehavior.homeFragmentClass)
+ R.id.bottomNavigationCalendar -> selectBottomNavFragment(CalendarFragment::class.java)
+ R.id.bottomNavigationToDo -> selectBottomNavFragment(ToDoListFragment::class.java)
+ R.id.bottomNavigationNotifications -> selectBottomNavFragment(NotificationListFragment::class.java)
R.id.bottomNavigationInbox -> {
if (ApiPrefs.isStudentView) {
- // Inbox not available in Student View
- val route = NothingToSeeHereFragment.makeRoute()
- addFragment(NothingToSeeHereFragment.newInstance(), route)
+ selectBottomNavFragment(NothingToSeeHereFragment::class.java)
} else {
- val route = InboxFragment.makeRoute()
- addFragment(InboxFragment.newInstance(route), route)
+ selectBottomNavFragment(InboxFragment::class.java)
}
}
}
@@ -602,7 +633,7 @@ class NavigationActivity : BaseRouterActivity(), Navigation, MasqueradingDialog.
is ToDoListFragment -> setBottomBarItemSelected(R.id.bottomNavigationToDo)
//Notifications
is NotificationListFragment-> {
- setBottomBarItemSelected(if(fragment.isCourseOrGroup()) R.id.bottomNavigationCourses
+ setBottomBarItemSelected(if(fragment.isCourseOrGroup()) R.id.bottomNavigationHome
else R.id.bottomNavigationNotifications)
}
//Inbox
@@ -611,7 +642,7 @@ class NavigationActivity : BaseRouterActivity(), Navigation, MasqueradingDialog.
is InboxComposeMessageFragment,
is InboxRecipientsFragment -> setBottomBarItemSelected(R.id.bottomNavigationInbox)
//courses
- else -> setBottomBarItemSelected(R.id.bottomNavigationCourses)
+ else -> setBottomBarItemSelected(R.id.bottomNavigationHome)
}
}
@@ -629,19 +660,6 @@ class NavigationActivity : BaseRouterActivity(), Navigation, MasqueradingDialog.
}
addBookmark()
return true
- } else if (item.itemId == android.R.id.home) {
- //if we hit the x while we're on a detail fragment, we always want to close the top fragment
- //and not have it trigger an actual "back press"
- val topFragment = topFragment
- if (supportFragmentManager.backStackEntryCount > 0) {
- if (topFragment != null) {
- supportFragmentManager.beginTransaction().remove(topFragment).commit()
- }
- super.onBackPressed()
- } else if (topFragment == null) {
- super.onBackPressed()
- }
- return true
}
return super.onOptionsItemSelected(item)
@@ -737,31 +755,72 @@ class NavigationActivity : BaseRouterActivity(), Navigation, MasqueradingDialog.
}
private fun addFragment(fragment: Fragment?, route: Route) {
+ if (RouteType.DIALOG == route.routeType && fragment is DialogFragment && isTablet) {
+ val ft = supportFragmentManager.beginTransaction()
+ ft.addToBackStack(fragment::class.java.name)
+ fragment.show(ft, fragment::class.java.name)
+ } else {
+ if (fragment != null && fragment::class.java.name in getBottomNavFragmentNames() && isBottomNavFragment(currentFragment)) {
+ selectBottomNavFragment(fragment::class.java)
+ } else {
+ addFullScreenFragment(fragment)
+ }
+ }
+ }
+
+ private fun selectBottomNavFragment(fragmentClass: Class) {
+ val selectedFragment = supportFragmentManager.findFragmentByTag(fragmentClass.name)
+
+ if (selectedFragment == null) {
+ val fragment = createBottomNavFragment(fragmentClass.name)
+ val newArguments = if (fragment?.arguments != null) fragment.requireArguments() else Bundle()
+ newArguments.putBoolean(BOTTOM_NAV_SCREEN, true)
+ fragment?.arguments = newArguments
+ addFullScreenFragment(fragment)
+ } else {
+ showHiddenFragment(selectedFragment)
+ }
+
+ bottomNavScreensStack.remove(fragmentClass.name)
+ bottomNavScreensStack.push(fragmentClass.name)
+ }
+
+ private fun addFullScreenFragment(fragment: Fragment?) {
if (fragment == null) {
- Logger.e("NavigationActivity:addFragment() - Could not route null Fragment.")
+ Logger.e("NavigationActivity:addFullScreenFragment() - Could not route null Fragment.")
return
}
val ft = supportFragmentManager.beginTransaction()
+ ft.setCustomAnimations(R.anim.fade_in_quick, R.anim.fade_out_quick)
+ currentFragment?.let { ft.hide(it) }
+ ft.add(R.id.fullscreen, fragment, fragment::class.java.name)
+ ft.addToBackStack(fragment::class.java.name)
+ ft.commitAllowingStateLoss()
+ }
- if (RouteType.DIALOG == route.routeType && fragment is DialogFragment && isTablet) {
- ft.addToBackStack(fragment::class.java.name)
- fragment.show(ft, fragment::class.java.name)
- } else {
- ft.setCustomAnimations(R.anim.fade_in_quick, R.anim.fade_out_quick)
- currentFragment?.let { ft.hide(it) }
- ft.add(R.id.fullscreen, fragment, fragment::class.java.name)
- ft.addToBackStack(fragment::class.java.name)
- ft.commitAllowingStateLoss()
+ private fun showHiddenFragment(fragment: Fragment) {
+ val ft = supportFragmentManager.beginTransaction()
+ ft.setCustomAnimations(R.anim.fade_in_quick, R.anim.fade_out_quick)
+ val bottomBarFragments = getBottomBarFragments(fragment::class.java.name)
+ bottomBarFragments.forEach {
+ ft.hide(it)
}
+ ft.show(fragment)
+ ft.commitAllowingStateLoss()
}
+ private fun getBottomBarFragments(selectedFragmentName: String): List {
+ return getBottomNavFragmentNames()
+ .filter { it != selectedFragmentName }
+ .mapNotNull { supportFragmentManager.findFragmentByTag(it) }
+ }
//endregion
//region Back Stack
override fun onBackPressed() {
- if(isDrawerOpen) {
+ if (isDrawerOpen) {
closeNavigationDrawer()
return
}
@@ -775,19 +834,48 @@ class NavigationActivity : BaseRouterActivity(), Navigation, MasqueradingDialog.
val topFragment = topFragment
if (topFragment is ParentFragment) {
if (!topFragment.handleBackPressed()) {
- super.onBackPressed()
+ if (isBottomNavFragment(topFragment)) {
+ handleBottomNavBackStack()
+ } else {
+ super.onBackPressed()
+ }
}
} else {
super.onBackPressed()
}
}
+ private fun handleBottomNavBackStack() {
+ if (bottomNavScreensStack.size == 0) {
+ finish()
+ } else if (bottomNavScreensStack.size == 1) {
+ bottomNavScreensStack.pop()
+ val previousFragment = supportFragmentManager.findFragmentByTag(navigationBehavior.homeFragmentClass.name)
+ if (previousFragment != null) {
+ showHiddenFragment(previousFragment)
+ applyCurrentFragmentTheme()
+ }
+ } else {
+ bottomNavScreensStack.pop()
+ val previousFragmentName = bottomNavScreensStack.peek()
+ val previousFragment = supportFragmentManager.findFragmentByTag(previousFragmentName)
+ if (previousFragment != null) {
+ showHiddenFragment(previousFragment)
+ applyCurrentFragmentTheme()
+ }
+ }
+ }
+
override val topFragment: Fragment?
get() {
val stackSize = supportFragmentManager.backStackEntryCount
if (stackSize > 0) {
- val fragmentTag = supportFragmentManager.getBackStackEntryAt(stackSize - 1).name
- return supportFragmentManager.findFragmentByTag(fragmentTag)
+ val backStackEntryName = supportFragmentManager.getBackStackEntryAt(stackSize - 1).name
+ return if (backStackEntryName in getBottomNavFragmentNames()) {
+ currentFragment
+ } else {
+ supportFragmentManager.findFragmentByTag(backStackEntryName)
+ }
}
return null
}
@@ -802,7 +890,20 @@ class NavigationActivity : BaseRouterActivity(), Navigation, MasqueradingDialog.
return null
}
- override val currentFragment: Fragment? get() = supportFragmentManager.findFragmentById(R.id.fullscreen)
+ override val currentFragment: Fragment?
+ get() {
+ val fragment = supportFragmentManager.findFragmentById(R.id.fullscreen)
+ return if (fragment != null && isBottomNavFragment(fragment)) {
+ val currentFragmentName = bottomNavScreensStack.peek() ?: navigationBehavior.homeFragmentClass.name
+ supportFragmentManager.findFragmentByTag(currentFragmentName)
+ } else {
+ fragment
+ }
+ }
+
+ private fun isBottomNavFragment(fragment: Fragment?) = fragment?.arguments?.getBoolean(BOTTOM_NAV_SCREEN) == true
+
+ private fun getBottomNavFragmentNames() = navigationBehavior.bottomNavBarFragments.map { it.name }
private fun clearBackStack(cls: Class<*>?) {
val fragment = topFragment
@@ -892,21 +993,13 @@ class NavigationActivity : BaseRouterActivity(), Navigation, MasqueradingDialog.
gauge.tag = gaugeLaunchDefinition
}
- override fun updateCalendarStartDay() {
- //Restarts the CalendarListViewFragment to update the changed start day of the week
- val fragment = supportFragmentManager.findFragmentByTag(CalendarFragment::class.java.name) as? ParentFragment
- if (fragment != null) {
- supportFragmentManager.beginTransaction().remove(fragment).commit()
- }
- val route = CalendarFragment.makeRoute()
- addFragment(CalendarFragment.newInstance(route), route)
- }
-
override fun addBookmark() {
val dialog = BookmarkCreationDialog.newInstance(this, topFragment, peekingFragment)
dialog?.show(supportFragmentManager, BookmarkCreationDialog::class.java.simpleName)
}
+ override fun canBookmark(): Boolean = navigationBehavior.visibleNavigationMenuItems.contains(NavigationMenuItem.BOOKMARKS)
+
override fun updateUnreadCount(unreadCount: String) {
// get the view
val bottomBarNavView = bottomBar?.getChildAt(0)
@@ -984,30 +1077,51 @@ class NavigationActivity : BaseRouterActivity(), Navigation, MasqueradingDialog.
}
}
- companion object {
- fun createIntent(context: Context): Intent {
- return Intent(context, NavigationActivity::class.java)
- }
+ override fun onTicketPost() {
+ // The message is a little longer than normal, so show it for LENGTH_LONG instead of LENGTH_SHORT
+ Toast.makeText(this, R.string.errorReportThankyou, Toast.LENGTH_LONG).show()
+ }
- fun createIntent(context: Context, route: Route): Intent {
- return Intent(context, NavigationActivity::class.java).apply { putExtra(Route.ROUTE, route) }
- }
+ override fun onTicketError() {
+ toast(R.string.errorOccurred)
+ }
- fun createIntent(context: Context, extras: Bundle): Intent {
- val intent = Intent(context, NavigationActivity::class.java)
- intent.putExtra(Const.EXTRAS, extras)
- return intent
+ private fun createBottomNavFragment(name: String?): ParentFragment? {
+ return when (name) {
+ navigationBehavior.homeFragmentClass.name -> {
+ val route = navigationBehavior.createHomeFragmentRoute(ApiPrefs.user)
+ navigationBehavior.createHomeFragment(route)
+ }
+ CalendarFragment::class.java.name -> {
+ val route = CalendarFragment.makeRoute()
+ CalendarFragment.newInstance(route)
+ }
+ ToDoListFragment::class.java.name -> {
+ val route = ToDoListFragment.makeRoute(ApiPrefs.user!!)
+ ToDoListFragment.newInstance(route)
+ }
+ NotificationListFragment::class.java.name -> {
+ val route = NotificationListFragment.makeRoute(ApiPrefs.user!!)
+ NotificationListFragment.newInstance(route)
+ }
+ InboxFragment::class.java.name -> {
+ val route = InboxFragment.makeRoute()
+ InboxFragment.newInstance(route)
+ }
+ NothingToSeeHereFragment::class.java.name -> NothingToSeeHereFragment.newInstance()
+ else -> null
}
+ }
- fun createIntent(context: Context, message: String, messageType: Int): Intent {
- val intent = createIntent(context)
- intent.putExtra(Const.MESSAGE, message)
- intent.putExtra(Const.MESSAGE_TYPE, messageType)
- return intent
+ companion object {
+ fun createIntent(context: Context, route: Route): Intent {
+ return Intent(context, NavigationActivity::class.java).apply { putExtra(Route.ROUTE, route) }
}
- fun createIntent(context: Context, masqueradingUserId: Long): Intent = createIntent(context).apply {
- putExtra(Const.QR_CODE_MASQUERADE_ID, masqueradingUserId)
+ fun createIntent(context: Context, masqueradingUserId: Long): Intent {
+ return Intent(context, NavigationActivity::class.java).apply {
+ putExtra(Const.QR_CODE_MASQUERADE_ID, masqueradingUserId)
+ }
}
val startActivityClass: Class
diff --git a/apps/student/src/main/java/com/instructure/student/activity/SettingsActivity.kt b/apps/student/src/main/java/com/instructure/student/activity/SettingsActivity.kt
index 5f46669749..adb582dbd0 100644
--- a/apps/student/src/main/java/com/instructure/student/activity/SettingsActivity.kt
+++ b/apps/student/src/main/java/com/instructure/student/activity/SettingsActivity.kt
@@ -25,8 +25,9 @@ import android.widget.Toast
import com.instructure.loginapi.login.dialog.ErrorReportDialog
import com.instructure.pandautils.utils.toast
import com.instructure.student.R
+import dagger.hilt.android.AndroidEntryPoint
-class SettingsActivity : AppCompatActivity(), ErrorReportDialog.ErrorReportDialogResultListener{
+class SettingsActivity : AppCompatActivity(){
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -43,15 +44,6 @@ class SettingsActivity : AppCompatActivity(), ErrorReportDialog.ErrorReportDialo
ft.commitAllowingStateLoss()
}
- override fun onTicketPost() {
- // The message is a little longer than normal, so show it for LENGTH_LONG instead of LENGTH_SHORT
- Toast.makeText(this@SettingsActivity, R.string.errorReportThankyou, Toast.LENGTH_LONG).show()
- }
-
- override fun onTicketError() {
- toast(R.string.errorOccurred)
- }
-
companion object {
fun createIntent(context: Context): Intent {
return Intent(context, SettingsActivity::class.java)
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
index bd4cfb5ea6..10c7871031 100644
--- a/apps/student/src/main/java/com/instructure/student/activity/ShareFileUploadActivity.kt
+++ b/apps/student/src/main/java/com/instructure/student/activity/ShareFileUploadActivity.kt
@@ -39,6 +39,7 @@ 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
diff --git a/apps/student/src/main/java/com/instructure/student/activity/SignInActivity.kt b/apps/student/src/main/java/com/instructure/student/activity/SignInActivity.kt
index 8c5ded42b2..51a93b3af9 100644
--- a/apps/student/src/main/java/com/instructure/student/activity/SignInActivity.kt
+++ b/apps/student/src/main/java/com/instructure/student/activity/SignInActivity.kt
@@ -26,12 +26,14 @@ import com.instructure.student.widget.WidgetUpdater
import com.instructure.canvasapi2.models.AccountDomain
import com.instructure.canvasapi2.utils.ApiPrefs
import com.instructure.loginapi.login.activities.BaseLoginSignInActivity
-import com.instructure.pandautils.services.PushNotificationRegistrationService
+import com.instructure.pandautils.services.PushNotificationRegistrationWorker
+import dagger.hilt.android.AndroidEntryPoint
+@AndroidEntryPoint
class SignInActivity : BaseLoginSignInActivity() {
override fun launchApplicationMainActivityIntent(): Intent {
- PushNotificationRegistrationService.scheduleJob(this, ApiPrefs.isMasquerading)
+ PushNotificationRegistrationWorker.scheduleJob(this, ApiPrefs.isMasquerading)
CookieManager.getInstance().flush()
diff --git a/apps/student/src/main/java/com/instructure/student/adapter/AllCoursesRecyclerAdapter.kt b/apps/student/src/main/java/com/instructure/student/adapter/AllCoursesRecyclerAdapter.kt
deleted file mode 100644
index e8dd7b4325..0000000000
--- a/apps/student/src/main/java/com/instructure/student/adapter/AllCoursesRecyclerAdapter.kt
+++ /dev/null
@@ -1,85 +0,0 @@
-/*
- * Copyright (C) 2017 - present Instructure, Inc.
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, version 3 of the License.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
- *
- */
-
-package com.instructure.student.adapter
-
-import android.app.Activity
-import android.view.View
-import android.widget.Toast
-import com.instructure.student.R
-import com.instructure.student.holders.CourseViewHolder
-import com.instructure.student.interfaces.CourseAdapterToFragmentCallback
-import com.instructure.canvasapi2.managers.CourseManager
-import com.instructure.canvasapi2.models.Course
-import com.instructure.canvasapi2.utils.APIHelper
-import com.instructure.canvasapi2.utils.isInvited
-import com.instructure.canvasapi2.utils.weave.WeaveJob
-import com.instructure.canvasapi2.utils.weave.awaitApi
-import com.instructure.canvasapi2.utils.weave.catch
-import com.instructure.canvasapi2.utils.weave.tryWeave
-
-
-class AllCoursesRecyclerAdapter(
- context: Activity,
- private val mAdapterToFragmentCallback: CourseAdapterToFragmentCallback
-) : BaseListRecyclerAdapter(context, Course::class.java) {
-
- init {
- loadData()
- }
-
- private var mApiCall: WeaveJob? = null
-
- override fun contextReady() = Unit
- override fun setupCallbacks() = Unit
- override fun itemLayoutResId(viewType: Int) = CourseViewHolder.HOLDER_RES_ID
- override fun createViewHolder(v: View, viewType: Int) = CourseViewHolder(v)
-
- override fun bindHolder(model: Course, holder: CourseViewHolder, position: Int) {
- holder.bind(model, mAdapterToFragmentCallback)
- }
-
- override fun loadData() {
- mApiCall?.cancel()
- mApiCall = tryWeave {
- val courses = awaitApi> { CourseManager.getCourses(isRefresh, it) }
- .filter { !it.accessRestrictedByDate && !it.isInvited() }
- addAll(courses)
- notifyDataSetChanged()
- isAllPagesLoaded = true
- if (itemCount == 0) adapterToRecyclerViewCallback.setIsEmpty(true)
- mAdapterToFragmentCallback.onRefreshFinished()
- } catch {
- if (!APIHelper.hasNetworkConnection()) {
- adapterToRecyclerViewCallback.setDisplayNoConnection(true)
- } else {
- adapterToRecyclerViewCallback.setIsEmpty(true)
- Toast.makeText(context, R.string.errorOccurred, Toast.LENGTH_SHORT).show()
- }
- mAdapterToFragmentCallback.onRefreshFinished()
- }
- }
-
- override fun cancel() {
- mApiCall?.cancel()
- }
-
- override fun refresh() {
- mApiCall?.cancel()
- super.refresh()
- }
-}
diff --git a/apps/student/src/main/java/com/instructure/student/adapter/DashboardRecyclerAdapter.kt b/apps/student/src/main/java/com/instructure/student/adapter/DashboardRecyclerAdapter.kt
index 70e662359b..ecb1220cd5 100644
--- a/apps/student/src/main/java/com/instructure/student/adapter/DashboardRecyclerAdapter.kt
+++ b/apps/student/src/main/java/com/instructure/student/adapter/DashboardRecyclerAdapter.kt
@@ -161,7 +161,7 @@ class DashboardRecyclerAdapter(
// Get enrollment invites
val invites = awaitApi> {
- EnrollmentManager.getSelfEnrollments(null, listOf(EnrollmentAPI.STATE_INVITED), isRefresh, it)
+ EnrollmentManager.getSelfEnrollments(null, listOf(EnrollmentAPI.STATE_INVITED, EnrollmentAPI.STATE_CURRENT_AND_FUTURE), isRefresh, it)
}
// Map not null is needed because the dashboard api can return unpublished courses
@@ -205,10 +205,7 @@ class DashboardRecyclerAdapter(
addOrUpdateAllItems(ItemType.ANNOUNCEMENT_HEADER, announcements)
// Add course invites
- val validInvites = invites.filter {
- mCourseMap[it.courseId]?.let { course ->
- course.isValidTerm() && !course.accessRestrictedByDate && isEnrollmentBetweenCourseDatesOrNotRestricted(course) } ?: false
- }
+ val validInvites = invites.filter { it.enrollmentState == EnrollmentAPI.STATE_INVITED && hasValidCourseForEnrollment(it) }
addOrUpdateAllItems(ItemType.INVITATION_HEADER, validInvites)
@@ -222,13 +219,20 @@ class DashboardRecyclerAdapter(
}
}
- private fun isEnrollmentBetweenCourseDatesOrNotRestricted(course: Course): Boolean {
- val now = OffsetDateTime.now()
- val startDate = OffsetDateTime.parse(course.startAt).withOffsetSameInstant(OffsetDateTime.now().offset)
- val endDate = OffsetDateTime.parse(course.endAt).withOffsetSameInstant(OffsetDateTime.now().offset)
+ private fun hasValidCourseForEnrollment(enrollment: Enrollment): Boolean {
+ return mCourseMap[enrollment.courseId]?.let { course ->
+ course.isValidTerm() && !course.accessRestrictedByDate && isEnrollmentBeforeEndDateOrNotRestricted(course)
+ } ?: false
+ }
+
+ private fun isEnrollmentBeforeEndDateOrNotRestricted(course: Course): Boolean {
+ val isBeforeEndDate = course.endAt?.let {
+ val now = OffsetDateTime.now()
+ val endDate = OffsetDateTime.parse(it).withOffsetSameInstant(OffsetDateTime.now().offset)
+ now.isBefore(endDate)
+ } ?: true // Case when the course has no end date
- val isBetweenCourseDates = now.isAfter(startDate) && now.isBefore(endDate)
- return !course.restrictEnrollmentsToCourseDate || isBetweenCourseDates
+ return !course.restrictEnrollmentsToCourseDate || isBeforeEndDate
}
override fun itemLayoutResId(viewType: Int) = when (ItemType.values()[viewType]) {
diff --git a/apps/student/src/main/java/com/instructure/student/adapter/EditFavoritesRecyclerAdapter.kt b/apps/student/src/main/java/com/instructure/student/adapter/EditFavoritesRecyclerAdapter.kt
deleted file mode 100644
index ba433dbb96..0000000000
--- a/apps/student/src/main/java/com/instructure/student/adapter/EditFavoritesRecyclerAdapter.kt
+++ /dev/null
@@ -1,155 +0,0 @@
-/*
- * Copyright (C) 2017 - present Instructure, Inc.
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, version 3 of the License.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
- *
- */
-package com.instructure.student.adapter
-
-import android.app.Activity
-import android.view.View
-import androidx.recyclerview.widget.RecyclerView
-import com.instructure.canvasapi2.managers.CourseManager
-import com.instructure.canvasapi2.managers.GroupManager
-import com.instructure.canvasapi2.models.CanvasComparable
-import com.instructure.canvasapi2.models.Course
-import com.instructure.canvasapi2.models.Group
-import com.instructure.canvasapi2.utils.isInvited
-import com.instructure.canvasapi2.utils.isValidTerm
-import com.instructure.canvasapi2.utils.weave.WeaveJob
-import com.instructure.canvasapi2.utils.weave.awaitApis
-import com.instructure.canvasapi2.utils.weave.catch
-import com.instructure.canvasapi2.utils.weave.tryWeave
-import com.instructure.pandarecycler.util.GroupSortedList
-import com.instructure.student.holders.EditFavoritesCourseHeaderViewHolder
-import com.instructure.student.holders.EditFavoritesCourseViewHolder
-import com.instructure.student.holders.EditFavoritesGroupHeaderViewHolder
-import com.instructure.student.holders.EditFavoritesGroupViewHolder
-import com.instructure.student.interfaces.AdapterToFragmentCallback
-
-class EditFavoritesRecyclerAdapter(
- context: Activity,
- private val mAdapterToFragmentCallback: AdapterToFragmentCallback>
-) : ExpandableRecyclerAdapter, RecyclerView.ViewHolder>(
- context,
- ItemType::class.java,
- CanvasComparable::class.java
-) {
-
- enum class ItemType {
- COURSE_HEADER,
- COURSE,
- GROUP_HEADER,
- GROUP
- }
-
- private var mApiCalls: WeaveJob? = null
-
- init {
- isExpandedByDefault = true
- loadData()
- }
-
- override fun createItemCallback(): GroupSortedList.ItemComparatorCallback> {
- return object : GroupSortedList.ItemComparatorCallback> {
- override fun compare(group: ItemType, o1: CanvasComparable<*>, o2: CanvasComparable<*>) = when {
- o1 is Course && o2 is Course -> o1.compareTo(o2)
- o1 is Group && o2 is Group -> o1.compareTo(o2)
- else -> -1
- }
-
- override fun areContentsTheSame(oldItem: CanvasComparable<*>, newItem: CanvasComparable<*>) = false
-
- override fun areItemsTheSame(item1: CanvasComparable<*>, item2: CanvasComparable<*>) = when {
- item1 is Course && item2 is Course -> item1.contextId.hashCode() == item2.contextId.hashCode()
- item1 is Group && item2 is Group -> item1.contextId.hashCode() == item2.contextId.hashCode()
- else -> false
- }
-
- override fun getUniqueItemId(item: CanvasComparable<*>) = when (item) {
- is Course -> item.contextId.hashCode().toLong()
- is Group -> item.contextId.hashCode().toLong()
- else -> -1L
- }
-
- override fun getChildType(group: ItemType, item: CanvasComparable<*>) = when (item) {
- is Course -> ItemType.COURSE.ordinal
- is Group -> ItemType.GROUP.ordinal
- else -> -1
- }
- }
- }
-
- override fun createGroupCallback(): GroupSortedList.GroupComparatorCallback {
- return object : GroupSortedList.GroupComparatorCallback {
- override fun compare(o1: ItemType, o2: ItemType) = o1.ordinal.compareTo(o2.ordinal)
- override fun areContentsTheSame(oldGroup: ItemType, newGroup: ItemType) = oldGroup == newGroup
- override fun areItemsTheSame(group1: ItemType, group2: ItemType) = group1 == group2
- override fun getUniqueGroupId(group: ItemType) = group.ordinal.toLong()
- override fun getGroupType(group: ItemType) = group.ordinal
- }
- }
-
- override fun itemLayoutResId(viewType: Int): Int = when(ItemType.values()[viewType]) {
- ItemType.COURSE_HEADER -> EditFavoritesCourseHeaderViewHolder.HOLDER_RES_ID
- ItemType.COURSE -> EditFavoritesCourseViewHolder.HOLDER_RES_ID
- ItemType.GROUP_HEADER -> EditFavoritesGroupHeaderViewHolder.HOLDER_RES_ID
- ItemType.GROUP -> EditFavoritesGroupViewHolder.HOLDER_RES_ID
- }
-
- override fun createViewHolder(v: View, viewType: Int) = when(ItemType.values()[viewType]) {
- ItemType.COURSE_HEADER -> EditFavoritesCourseHeaderViewHolder(v)
- ItemType.COURSE -> EditFavoritesCourseViewHolder(v)
- ItemType.GROUP_HEADER -> EditFavoritesGroupHeaderViewHolder(v)
- ItemType.GROUP -> EditFavoritesGroupViewHolder(v)
- }
-
- override fun onBindChildHolder(holder: RecyclerView.ViewHolder, header: ItemType, item: CanvasComparable<*>) {
- when {
- holder is EditFavoritesCourseViewHolder && item is Course -> holder.bind(context,item, mAdapterToFragmentCallback)
- holder is EditFavoritesGroupViewHolder && item is Group -> holder.bind(context, item, mAdapterToFragmentCallback)
- }
- }
-
- override fun onBindHeaderHolder(holder: RecyclerView.ViewHolder, header: ItemType, isExpanded: Boolean) = Unit
-
- override fun loadData() {
- mApiCalls?.cancel()
- mApiCalls = tryWeave {
- val (rawCourses, rawGroups) = awaitApis,List>(
- { CourseManager.getCourses(true, it) },
- { GroupManager.getAllGroups(it,true)})
- val validCourses = rawCourses.filter { !it.accessRestrictedByDate && !it.isInvited() }
- addOrUpdateAllItems(ItemType.COURSE_HEADER,validCourses)
- val courseMap = rawCourses.associateBy { it.id }
- val groups = rawGroups.filter { group -> group.isActive(courseMap[group.courseId]) }
-
- addOrUpdateAllItems(ItemType.GROUP_HEADER,groups)
- notifyDataSetChanged()
- isAllPagesLoaded = true
- if (itemCount == 0) adapterToRecyclerViewCallback.setIsEmpty(true)
- mAdapterToFragmentCallback.onRefreshFinished()
- } catch {
- onNoNetwork()
- }
- }
-
- override fun refresh() {
- mApiCalls?.cancel()
- super.refresh()
- }
-
- override fun cancel() {
- mApiCalls?.cancel()
- }
-}
diff --git a/apps/student/src/main/java/com/instructure/student/adapter/ModuleListRecyclerAdapter.kt b/apps/student/src/main/java/com/instructure/student/adapter/ModuleListRecyclerAdapter.kt
index cba6858fda..66d75e01b0 100644
--- a/apps/student/src/main/java/com/instructure/student/adapter/ModuleListRecyclerAdapter.kt
+++ b/apps/student/src/main/java/com/instructure/student/adapter/ModuleListRecyclerAdapter.kt
@@ -57,9 +57,10 @@ import retrofit2.Response
import java.util.*
open class ModuleListRecyclerAdapter(
- val courseContext: CanvasContext,
+ private val courseContext: CanvasContext,
context: Context,
- val adapterToFragmentCallback: ModuleAdapterToFragmentCallback?
+ private var shouldExhaustPagination: Boolean,
+ private val adapterToFragmentCallback: ModuleAdapterToFragmentCallback?
) : ExpandableRecyclerAdapter(context, ModuleObject::class.java, ModuleItem::class.java) {
private val mModuleItemCallbacks = HashMap()
@@ -67,7 +68,7 @@ open class ModuleListRecyclerAdapter(
private var checkCourseTabsJob: Job? = null
/* For testing purposes only */
- protected constructor(context: Context) : this(CanvasContext.defaultCanvasContext(), context, null) // Callback not needed for testing, cast to null
+ protected constructor(context: Context) : this(CanvasContext.defaultCanvasContext(), context, false,null) // Callback not needed for testing, cast to null
init {
viewHolderHeaderClicked = object : ViewHolderHeaderClicked {
@@ -83,7 +84,7 @@ open class ModuleListRecyclerAdapter(
}
isExpandedByDefault = false
-// isDisplayEmptyCell = true TODO - make this work with scroll to functionality
+ isDisplayEmptyCell = true
if (adapterToFragmentCallback != null) loadData() // Callback is null when testing
}
@@ -136,6 +137,7 @@ open class ModuleListRecyclerAdapter(
}
override fun refresh() {
+ shouldExhaustPagination = false
mModuleItemCallbacks.clear()
checkCourseTabsJob?.cancel()
collapseAll()
@@ -339,8 +341,8 @@ open class ModuleListRecyclerAdapter(
ModuleManager.getFirstPageModuleItems(courseContext, it.id, getModuleItemsCallback(it, true), true)
}
}
- if(!this.moreCallsExist()) {
- // Wait until we are done exhausting pagination
+ if(!shouldExhaustPagination || !this.moreCallsExist()) {
+ // If we should exhaust pagination wait until we are done exhausting pagination
adapterToFragmentCallback?.onRefreshFinished()
}
}
@@ -358,7 +360,11 @@ open class ModuleListRecyclerAdapter(
// We only want to show modules if its a course nav option OR set to as the homepage
if (tabs.find { it.tabId == "modules" } != null || (courseContext as Course).homePage?.apiString == "modules") {
- ModuleManager.getAllModuleObjets(courseContext, mModuleObjectCallback!!, true)
+ if (shouldExhaustPagination) {
+ ModuleManager.getAllModuleObjets(courseContext, mModuleObjectCallback!!, true)
+ } else {
+ ModuleManager.getFirstPageModuleObjects(courseContext, mModuleObjectCallback!!, true)
+ }
} else {
adapterToFragmentCallback?.onRefreshFinished(true)
}
@@ -367,6 +373,10 @@ open class ModuleListRecyclerAdapter(
}
}
+ override fun loadNextPage(nextURL: String) {
+ ModuleManager.getNextPageModuleObjects(nextURL, mModuleObjectCallback!!, true)
+ }
+
// endregion
// region Module binder Helpers
diff --git a/apps/student/src/main/java/com/instructure/student/adapter/TermSpinnerAdapter.kt b/apps/student/src/main/java/com/instructure/student/adapter/TermSpinnerAdapter.kt
index 75cca95596..638036d152 100644
--- a/apps/student/src/main/java/com/instructure/student/adapter/TermSpinnerAdapter.kt
+++ b/apps/student/src/main/java/com/instructure/student/adapter/TermSpinnerAdapter.kt
@@ -31,7 +31,8 @@ import com.instructure.student.R
class TermSpinnerAdapter(
context: Context,
resource: Int,
- private val gradingPeriods: List
+ private val gradingPeriods: List,
+ private val showDropdownArrow: Boolean = true
) : ArrayAdapter(context, resource, gradingPeriods) {
private val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
@@ -54,7 +55,7 @@ class TermSpinnerAdapter(
holder = view.tag as TermSpinnerViewHolder
}
- holder.dropDown.setVisible(!isLoading)
+ holder.dropDown.setVisible(!isLoading && showDropdownArrow)
holder.progressBar.setVisible(isLoading)
holder.periodName.text = gradingPeriods[position].title
diff --git a/apps/student/src/main/java/com/instructure/student/adapter/TodoListRecyclerAdapter.kt b/apps/student/src/main/java/com/instructure/student/adapter/TodoListRecyclerAdapter.kt
index b50b7c972b..f787c56283 100644
--- a/apps/student/src/main/java/com/instructure/student/adapter/TodoListRecyclerAdapter.kt
+++ b/apps/student/src/main/java/com/instructure/student/adapter/TodoListRecyclerAdapter.kt
@@ -168,7 +168,9 @@ open class TodoListRecyclerAdapter : ExpandableRecyclerAdapter.
+ *
+ */
+
+package com.instructure.student.adapter.assignment
+
+import android.content.Context
+import com.instructure.canvasapi2.models.*
+import com.instructure.canvasapi2.utils.*
+import com.instructure.pandarecycler.util.GroupSortedList
+import com.instructure.pandarecycler.util.Types
+import com.instructure.student.R
+import com.instructure.student.interfaces.AdapterToAssignmentsCallback
+import java.util.*
+
+private const val HEADER_POSITION_OVERDUE = 0
+private const val HEADER_POSITION_UPCOMING = 1
+private const val HEADER_POSITION_UNDATED = 2
+private const val HEADER_POSITION_PAST = 3
+
+class AssignmentListByDateRecyclerAdapter(
+ context: Context,
+ canvasContext: CanvasContext,
+ adapterToAssignmentsCallback: AdapterToAssignmentsCallback,
+ isTesting: Boolean = false
+) : AssignmentListRecyclerAdapter(context, canvasContext, adapterToAssignmentsCallback, isTesting) {
+
+ private val overdue = AssignmentGroup(name = context.getString(R.string.overdueAssignments), position = HEADER_POSITION_OVERDUE)
+ private val upcoming = AssignmentGroup(name = context.getString(R.string.upcomingAssignments), position = HEADER_POSITION_UPCOMING)
+ private val undated = AssignmentGroup(name = context.getString(R.string.undatedAssignments), position = HEADER_POSITION_UNDATED)
+ private val past = AssignmentGroup(name = context.getString(R.string.pastAssignments), position = HEADER_POSITION_PAST)
+
+ override fun createItemCallback() = object : GroupSortedList.ItemComparatorCallback {
+ private val sameCheck = compareBy({ it.dueAt }, { it.name })
+ override fun areContentsTheSame(old: Assignment, new: Assignment) = sameCheck.compare(old, new) == 0
+ override fun areItemsTheSame(item1: Assignment, item2: Assignment) = item1.id == item2.id
+ override fun getChildType(group: AssignmentGroup, item: Assignment) = Types.TYPE_ITEM
+ override fun getUniqueItemId(item: Assignment) = item.id
+ override fun compare(group: AssignmentGroup, o1: Assignment, o2: Assignment): Int {
+ return when (group.position) {
+ HEADER_POSITION_UNDATED -> o1.name?.toLowerCase()?.compareTo(o2.name?.toLowerCase() ?: "") ?: 0
+ HEADER_POSITION_PAST -> o2.dueAt?.compareTo(o1.dueAt ?: "") ?: 0 // Sort newest date first (o1 and o2 switched places)
+ else -> o1.dueAt?.compareTo(o2.dueAt ?: "") ?: 0 // Sort oldest date first
+ }
+ }
+ }
+
+ override fun populateData() {
+ val today = Date()
+ for (assignmentGroup in assignmentGroups) {
+ // TODO canHaveOverDueAssignment
+ // web does it like this
+ // # only handles observer observing one student, this needs to change to handle multiple users in the future
+ // canHaveOverdueAssignment = !ENV.current_user_has_been_observer_in_this_course || ENV.observed_student_ids?.length == 1I
+ // endtodo
+ assignmentGroup.assignments
+ .filterWithQuery(searchQuery, Assignment::name)
+ .forEach { assignment ->
+ val dueAt = assignment.dueAt
+ val submission = assignment.submission
+ assignment.submission = submission
+ val isWithoutGradedSubmission = submission == null || submission.isWithoutGradedSubmission
+ val isOverdue = assignment.isAllowedToSubmit && isWithoutGradedSubmission
+ if (dueAt == null) {
+ addOrUpdateItem(undated, assignment)
+ } else {
+ when {
+ today.before(dueAt.toDate()) -> addOrUpdateItem(upcoming, assignment)
+ isOverdue -> addOrUpdateItem(overdue, assignment)
+ else -> addOrUpdateItem(past, assignment)
+ }
+ }
+ }
+ }
+ isAllPagesLoaded = true
+ }
+
+}
diff --git a/apps/student/src/main/java/com/instructure/student/adapter/assignment/AssignmentListByTypeRecyclerAdapter.kt b/apps/student/src/main/java/com/instructure/student/adapter/assignment/AssignmentListByTypeRecyclerAdapter.kt
new file mode 100644
index 0000000000..569115e6c1
--- /dev/null
+++ b/apps/student/src/main/java/com/instructure/student/adapter/assignment/AssignmentListByTypeRecyclerAdapter.kt
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2021 - 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.adapter.assignment
+
+import android.content.Context
+import com.instructure.canvasapi2.models.Assignment
+import com.instructure.canvasapi2.models.AssignmentGroup
+import com.instructure.canvasapi2.models.CanvasContext
+import com.instructure.canvasapi2.utils.filterWithQuery
+import com.instructure.pandarecycler.util.GroupSortedList
+import com.instructure.pandarecycler.util.Types
+import com.instructure.student.interfaces.AdapterToAssignmentsCallback
+
+class AssignmentListByTypeRecyclerAdapter(
+ context: Context,
+ canvasContext: CanvasContext,
+ adapterToAssignmentsCallback: AdapterToAssignmentsCallback,
+ isTesting: Boolean = false
+) : AssignmentListRecyclerAdapter(context, canvasContext, adapterToAssignmentsCallback, isTesting) {
+
+ override fun populateData() {
+ assignmentGroups.forEach { assignmentGroup ->
+ val filteredAssignments = assignmentGroup.assignments.filterWithQuery(searchQuery, Assignment::name)
+ addOrUpdateAllItems(assignmentGroup, filteredAssignments)
+ }
+ isAllPagesLoaded = true
+ }
+
+ override fun createItemCallback() = object : GroupSortedList.ItemComparatorCallback {
+ private val sameCheck = compareBy({ it.dueAt }, { it.name })
+ override fun areContentsTheSame(old: Assignment, new: Assignment) = sameCheck.compare(old, new) == 0
+ override fun areItemsTheSame(item1: Assignment, item2: Assignment) = item1.id == item2.id
+ override fun getChildType(group: AssignmentGroup, item: Assignment) = Types.TYPE_ITEM
+ override fun getUniqueItemId(item: Assignment) = item.id
+ override fun compare(group: AssignmentGroup, o1: Assignment, o2: Assignment) = o1.position - o2.position
+ }
+}
diff --git a/apps/student/src/main/java/com/instructure/student/adapter/AssignmentDateListRecyclerAdapter.kt b/apps/student/src/main/java/com/instructure/student/adapter/assignment/AssignmentListRecyclerAdapter.kt
similarity index 61%
rename from apps/student/src/main/java/com/instructure/student/adapter/AssignmentDateListRecyclerAdapter.kt
rename to apps/student/src/main/java/com/instructure/student/adapter/assignment/AssignmentListRecyclerAdapter.kt
index edce374397..57620483b9 100644
--- a/apps/student/src/main/java/com/instructure/student/adapter/AssignmentDateListRecyclerAdapter.kt
+++ b/apps/student/src/main/java/com/instructure/student/adapter/assignment/AssignmentListRecyclerAdapter.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2016 - present Instructure, Inc.
+ * Copyright (C) 2021 - 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
@@ -14,8 +14,7 @@
* along with this program. If not, see .
*
*/
-
-package com.instructure.student.adapter
+package com.instructure.student.adapter.assignment
import android.content.Context
import android.view.View
@@ -33,6 +32,7 @@ import com.instructure.pandarecycler.util.GroupSortedList
import com.instructure.pandarecycler.util.Types
import com.instructure.pandautils.utils.color
import com.instructure.student.R
+import com.instructure.student.adapter.ExpandableRecyclerAdapter
import com.instructure.student.holders.AssignmentViewHolder
import com.instructure.student.holders.EmptyViewHolder
import com.instructure.student.holders.ExpandableViewHolder
@@ -40,27 +40,22 @@ import com.instructure.student.interfaces.AdapterToAssignmentsCallback
import com.instructure.student.interfaces.GradingPeriodsCallback
import retrofit2.Call
import retrofit2.Response
-import java.util.*
-open class AssignmentDateListRecyclerAdapter(
- context: Context,
- private val canvasContext: CanvasContext,
- private val adapterToAssignmentsCallback: AdapterToAssignmentsCallback,
- isTesting: Boolean = false
+abstract class AssignmentListRecyclerAdapter (
+ context: Context,
+ private val canvasContext: CanvasContext,
+ private val adapterToAssignmentsCallback: AdapterToAssignmentsCallback,
+ isTesting: Boolean = false
) : ExpandableRecyclerAdapter(
- context,
- AssignmentGroup::class.java,
- Assignment::class.java
+ context,
+ AssignmentGroup::class.java,
+ Assignment::class.java
), GradingPeriodsCallback {
- private val overdue: AssignmentGroup
- private val upcoming: AssignmentGroup
- private val undated: AssignmentGroup
- private val past: AssignmentGroup
private var assignmentGroupCallback: StatusCallback>? = null
override var currentGradingPeriod: GradingPeriod? = null
private var apiJob: WeaveJob? = null
- private var assignmentGroups: List = emptyList()
+ protected var assignmentGroups: List = emptyList()
var searchQuery: String = ""
set(value) {
@@ -73,10 +68,6 @@ open class AssignmentDateListRecyclerAdapter(
}
init {
- overdue = AssignmentGroup(name = context.getString(R.string.overdueAssignments), position = HEADER_POSITION_OVERDUE)
- upcoming = AssignmentGroup(name = context.getString(R.string.upcomingAssignments), position = HEADER_POSITION_UPCOMING)
- undated = AssignmentGroup(name = context.getString(R.string.undatedAssignments), position = HEADER_POSITION_UNDATED)
- past = AssignmentGroup(name = context.getString(R.string.pastAssignments), position = HEADER_POSITION_PAST)
isExpandedByDefault = true
isDisplayEmptyCell = true
if (!isTesting) loadData()
@@ -89,15 +80,15 @@ open class AssignmentDateListRecyclerAdapter(
assignmentGroups = response.body()!!
populateData()
adapterToAssignmentsCallback.onRefreshFinished()
- adapterToAssignmentsCallback.setTermSpinnerState(true)
+ adapterToAssignmentsCallback.assignmentLoadingFinished()
}
override fun onFail(call: Call>?, error: Throwable, response: Response<*>?) {
- adapterToAssignmentsCallback.setTermSpinnerState(true)
+ adapterToAssignmentsCallback.assignmentLoadingFinished()
}
override fun onFinished(type: ApiType) {
- this@AssignmentDateListRecyclerAdapter.onCallbackFinished(type)
+ this@AssignmentListRecyclerAdapter.onCallbackFinished(type)
}
}
@@ -127,7 +118,7 @@ open class AssignmentDateListRecyclerAdapter(
//This check is for the "all grading periods" option
if (currentGradingPeriod != null && currentGradingPeriod!!.title != null
- && currentGradingPeriod!!.title == context.getString(R.string.allGradingPeriods)) {
+ && currentGradingPeriod!!.title == context.getString(R.string.assignmentsListAllGradingPeriods)) {
loadAssignment()
return
}
@@ -150,6 +141,8 @@ open class AssignmentDateListRecyclerAdapter(
loadAssignmentsForGradingPeriod(currentGradingPeriod!!.id, true)
return
}
+ } else {
+ adapterToAssignmentsCallback.gradingPeriodsFetched(emptyList())
}
}
//If we made it this far, MGP is disabled so we just go forward with the standard
@@ -163,6 +156,7 @@ open class AssignmentDateListRecyclerAdapter(
}.gradingPeriodList
adapterToAssignmentsCallback.gradingPeriodsFetched(periods)
} catch {
+ adapterToAssignmentsCallback.gradingPeriodsFetched(emptyList())
Logger.w("Unable to fetch grading periods")
it.printStackTrace()
}
@@ -173,9 +167,9 @@ open class AssignmentDateListRecyclerAdapter(
override val isPaginated get() = false
override fun onBindChildHolder(
- holder: RecyclerView.ViewHolder,
- assignmentGroup: AssignmentGroup,
- assignment: Assignment
+ holder: RecyclerView.ViewHolder,
+ assignmentGroup: AssignmentGroup,
+ assignment: Assignment
) {
(holder as AssignmentViewHolder).bind(context, assignment, canvasContext.color, adapterToAssignmentsCallback)
}
@@ -185,16 +179,16 @@ open class AssignmentDateListRecyclerAdapter(
}
override fun onBindHeaderHolder(
- holder: RecyclerView.ViewHolder,
- assignmentGroup: AssignmentGroup,
- isExpanded: Boolean
+ holder: RecyclerView.ViewHolder,
+ assignmentGroup: AssignmentGroup,
+ isExpanded: Boolean
) {
(holder as ExpandableViewHolder).bind(
- context,
- assignmentGroup,
- assignmentGroup.name ?: "",
- isExpanded,
- viewHolderHeaderClicked
+ context,
+ assignmentGroup,
+ assignmentGroup.name ?: "",
+ isExpanded,
+ viewHolderHeaderClicked
)
}
@@ -206,11 +200,11 @@ open class AssignmentDateListRecyclerAdapter(
// Scope assignments if its for a student
val scopeToStudent = (canvasContext as Course).isStudent
AssignmentManager.getAssignmentGroupsWithAssignmentsForGradingPeriod(
- canvasContext.id,
- gradingPeriodID,
- scopeToStudent,
- isRefresh,
- assignmentGroupCallback!!
+ canvasContext.id,
+ gradingPeriodID,
+ scopeToStudent,
+ isRefresh,
+ assignmentGroupCallback!!
)
}
@@ -218,35 +212,7 @@ open class AssignmentDateListRecyclerAdapter(
AssignmentManager.getAssignmentGroupsWithAssignments(canvasContext.id, isRefresh, assignmentGroupCallback!!)
}
- private fun populateData() {
- val today = Date()
- for (assignmentGroup in assignmentGroups) {
- // TODO canHaveOverDueAssignment
- // web does it like this
- // # only handles observer observing one student, this needs to change to handle multiple users in the future
- // canHaveOverdueAssignment = !ENV.current_user_has_been_observer_in_this_course || ENV.observed_student_ids?.length == 1I
- // endtodo
- assignmentGroup.assignments
- .filterWithQuery(searchQuery, Assignment::name)
- .forEach { assignment ->
- val dueAt = assignment.dueAt
- val submission = assignment.submission
- assignment.submission = submission
- val isWithoutGradedSubmission = submission == null || submission.isWithoutGradedSubmission
- val isOverdue = assignment.isAllowedToSubmit && isWithoutGradedSubmission
- if (dueAt == null) {
- addOrUpdateItem(undated, assignment)
- } else {
- when {
- today.before(dueAt.toDate()) -> addOrUpdateItem(upcoming, assignment)
- isOverdue -> addOrUpdateItem(overdue, assignment)
- else -> addOrUpdateItem(past, assignment)
- }
- }
- }
- }
- isAllPagesLoaded = true
- }
+ protected abstract fun populateData()
// region Expandable callbacks
@@ -258,37 +224,10 @@ open class AssignmentDateListRecyclerAdapter(
override fun getUniqueGroupId(group: AssignmentGroup) = group.position.toLong()
}
- override fun createItemCallback() = itemCallback
-
// endregion
override fun cancel() {
super.cancel()
apiJob?.cancel()
}
-
- companion object {
- const val HEADER_POSITION_OVERDUE = 0
- const val HEADER_POSITION_UPCOMING = 1
- const val HEADER_POSITION_UNDATED = 2
- const val HEADER_POSITION_PAST = 3
-
- // Decoupled for testability
- val itemCallback = object : GroupSortedList.ItemComparatorCallback {
- private val sameCheck = compareBy({ it.dueAt }, { it.name })
- override fun areContentsTheSame(old: Assignment, new: Assignment) = sameCheck.compare(old, new) == 0
- override fun areItemsTheSame(item1: Assignment, item2: Assignment) = item1.id == item2.id
- override fun getChildType(group: AssignmentGroup, item: Assignment) = Types.TYPE_ITEM
- override fun getUniqueItemId(item: Assignment) = item.id
- override fun compare(group: AssignmentGroup, o1: Assignment, o2: Assignment): Int {
- val position = group.position
- return when (position) {
- HEADER_POSITION_UNDATED -> o1.name?.toLowerCase()?.compareTo(o2.name?.toLowerCase() ?: "") ?: 0
- HEADER_POSITION_PAST -> o2.dueAt?.compareTo(o1.dueAt ?: "") ?: 0 // Sort newest date first (o1 and o2 switched places)
- else -> o1.dueAt?.compareTo(o2.dueAt ?: "") ?: 0 // Sort oldest date first
- }
- }
- }
- }
-
-}
+}
\ No newline at end of file
diff --git a/apps/student/src/main/java/com/instructure/student/di/FragmentModule.kt b/apps/student/src/main/java/com/instructure/student/di/FragmentModule.kt
new file mode 100644
index 0000000000..e5acd2200a
--- /dev/null
+++ b/apps/student/src/main/java/com/instructure/student/di/FragmentModule.kt
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2021 - 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.di
+
+import androidx.fragment.app.FragmentActivity
+import com.instructure.pandautils.navigation.WebViewRouter
+import com.instructure.student.navigation.StudentWebViewRouter
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.android.components.FragmentComponent
+
+/**
+ * Module for various common Fragment scope dependencies that are used in different Fragments.
+ */
+@Module
+@InstallIn(FragmentComponent::class)
+class FragmentModule {
+
+ @Provides
+ fun provideWebViewRouter(activity: FragmentActivity): WebViewRouter {
+ return StudentWebViewRouter(activity)
+ }
+}
\ No newline at end of file
diff --git a/apps/student/src/main/java/com/instructure/student/di/HelpDialogModule.kt b/apps/student/src/main/java/com/instructure/student/di/HelpDialogModule.kt
new file mode 100644
index 0000000000..1b6a616b5b
--- /dev/null
+++ b/apps/student/src/main/java/com/instructure/student/di/HelpDialogModule.kt
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2021 - 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.di
+
+import androidx.fragment.app.FragmentActivity
+import com.instructure.pandautils.features.help.HelpDialogFragmentBehavior
+import com.instructure.pandautils.features.help.HelpLinkFilter
+import com.instructure.student.mobius.settings.help.StudentHelpDialogFragmentBehavior
+import com.instructure.student.mobius.settings.help.StudentHelpLinkFilter
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.android.components.FragmentComponent
+import dagger.hilt.android.components.ViewModelComponent
+
+@Module
+@InstallIn(ViewModelComponent::class)
+class HelpDialogModule {
+
+ @Provides
+ fun provideHelpLinkFilter(): HelpLinkFilter = StudentHelpLinkFilter()
+}
+
+@Module
+@InstallIn(FragmentComponent::class)
+class HelpDialogFragmentModule {
+
+ @Provides
+ fun provideHelpDialogFragmentBehavior(activity: FragmentActivity): HelpDialogFragmentBehavior {
+ return StudentHelpDialogFragmentBehavior(activity)
+ }
+}
\ No newline at end of file
diff --git a/apps/student/src/main/java/com/instructure/student/di/NavigationActivityModule.kt b/apps/student/src/main/java/com/instructure/student/di/NavigationActivityModule.kt
new file mode 100644
index 0000000000..4ebf736494
--- /dev/null
+++ b/apps/student/src/main/java/com/instructure/student/di/NavigationActivityModule.kt
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2021 - 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.di
+
+import androidx.fragment.app.FragmentActivity
+import com.instructure.student.navigation.DefaultNavigationBehavior
+import com.instructure.student.navigation.ElementaryNavigationBehavior
+import com.instructure.student.navigation.NavigationBehavior
+import com.instructure.student.util.AppShortcutManager
+import com.instructure.student.util.DefaultAppShortcutManager
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.android.components.ActivityComponent
+import javax.inject.Named
+
+private const val CANVAS_FOR_ELEMENTARY = "canvas_for_elementary"
+
+@Module
+@InstallIn(ActivityComponent::class)
+class NavigationActivityModule {
+
+ @Provides
+ @Named(CANVAS_FOR_ELEMENTARY)
+ fun providesCanvasForElementaryFeatureFlag(activity: FragmentActivity): Boolean {
+ val intent = activity.intent
+ return intent?.getBooleanExtra("canvas_for_elementary", false) ?: false
+ }
+
+ @Provides
+ fun providesNavigationBehavior(@Named(CANVAS_FOR_ELEMENTARY) canvasForElementary: Boolean): NavigationBehavior {
+ return if (canvasForElementary) {
+ ElementaryNavigationBehavior()
+ } else {
+ DefaultNavigationBehavior()
+ }
+ }
+
+ @Provides
+ fun provideAppShortcutManager(): AppShortcutManager {
+ return DefaultAppShortcutManager()
+ }
+}
\ No newline at end of file
diff --git a/apps/student/src/main/java/com/instructure/student/di/elementary/GradesModule.kt b/apps/student/src/main/java/com/instructure/student/di/elementary/GradesModule.kt
new file mode 100644
index 0000000000..466bcf9e9f
--- /dev/null
+++ b/apps/student/src/main/java/com/instructure/student/di/elementary/GradesModule.kt
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2021 - 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.di.elementary
+
+import androidx.fragment.app.FragmentActivity
+import com.instructure.pandautils.features.elementary.grades.GradesRouter
+import com.instructure.student.mobius.elementary.grades.StudentGradesRouter
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.android.components.FragmentComponent
+
+@Module
+@InstallIn(FragmentComponent::class)
+class GradesModule {
+
+ @Provides
+ fun provideGradesRouter(activity: FragmentActivity): GradesRouter {
+ return StudentGradesRouter(activity)
+ }
+}
\ No newline at end of file
diff --git a/apps/student/src/main/java/com/instructure/student/di/elementary/HomeroomModule.kt b/apps/student/src/main/java/com/instructure/student/di/elementary/HomeroomModule.kt
new file mode 100644
index 0000000000..a8147a721a
--- /dev/null
+++ b/apps/student/src/main/java/com/instructure/student/di/elementary/HomeroomModule.kt
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2021 - 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.di.elementary
+
+import androidx.fragment.app.FragmentActivity
+import com.instructure.pandautils.features.elementary.homeroom.HomeroomRouter
+import com.instructure.student.mobius.elementary.homeroom.StudentHomeroomRouter
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.android.components.FragmentComponent
+
+@Module
+@InstallIn(FragmentComponent::class)
+class HomeroomModule {
+
+ @Provides
+ fun provideHomeroomRouter(activity: FragmentActivity): HomeroomRouter {
+ return StudentHomeroomRouter(activity)
+ }
+}
\ No newline at end of file
diff --git a/apps/student/src/main/java/com/instructure/student/di/elementary/ResourcesModule.kt b/apps/student/src/main/java/com/instructure/student/di/elementary/ResourcesModule.kt
new file mode 100644
index 0000000000..fc7e62e8a3
--- /dev/null
+++ b/apps/student/src/main/java/com/instructure/student/di/elementary/ResourcesModule.kt
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2021 - 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.di.elementary
+
+import androidx.fragment.app.FragmentActivity
+import com.instructure.pandautils.features.elementary.resources.itemviewmodels.ResourcesRouter
+import com.instructure.student.mobius.elementary.resources.StudentResourcesRouter
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.android.components.FragmentComponent
+
+@Module
+@InstallIn(FragmentComponent::class)
+class ResourcesModule {
+
+ @Provides
+ fun provideResourcesRouter(activity: FragmentActivity): ResourcesRouter {
+ return StudentResourcesRouter(activity)
+ }
+}
\ No newline at end of file
diff --git a/apps/student/src/main/java/com/instructure/student/di/elementary/schedule/ScheduleModule.kt b/apps/student/src/main/java/com/instructure/student/di/elementary/schedule/ScheduleModule.kt
new file mode 100644
index 0000000000..2928c63b47
--- /dev/null
+++ b/apps/student/src/main/java/com/instructure/student/di/elementary/schedule/ScheduleModule.kt
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2021 - 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.di.elementary.schedule
+
+import androidx.fragment.app.FragmentActivity
+import com.instructure.pandautils.features.elementary.schedule.ScheduleRouter
+import com.instructure.student.mobius.elementary.schedule.StudentScheduleRouter
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.android.components.FragmentComponent
+
+@Module
+@InstallIn(FragmentComponent::class)
+class ScheduleModule {
+
+ @Provides
+ fun provideScheduleRouter(activity: FragmentActivity): ScheduleRouter {
+ return StudentScheduleRouter(activity)
+ }
+}
\ No newline at end of file
diff --git a/apps/student/src/main/java/com/instructure/student/dialog/CanvasContextListDialog.kt b/apps/student/src/main/java/com/instructure/student/dialog/CanvasContextListDialog.kt
index 6ba6e9e6cf..4f059177aa 100644
--- a/apps/student/src/main/java/com/instructure/student/dialog/CanvasContextListDialog.kt
+++ b/apps/student/src/main/java/com/instructure/student/dialog/CanvasContextListDialog.kt
@@ -30,6 +30,7 @@ import com.instructure.canvasapi2.models.CanvasContext
import com.instructure.canvasapi2.models.Course
import com.instructure.canvasapi2.models.Group
import com.instructure.canvasapi2.utils.hasActiveEnrollment
+import com.instructure.canvasapi2.utils.isNotDeleted
import com.instructure.canvasapi2.utils.isValidTerm
import com.instructure.canvasapi2.utils.weave.awaitApis
import com.instructure.canvasapi2.utils.weave.catch
diff --git a/apps/student/src/main/java/com/instructure/student/dialog/HelpDialogStyled.kt b/apps/student/src/main/java/com/instructure/student/dialog/HelpDialogStyled.kt
deleted file mode 100644
index b409b9f4d3..0000000000
--- a/apps/student/src/main/java/com/instructure/student/dialog/HelpDialogStyled.kt
+++ /dev/null
@@ -1,246 +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.annotation.SuppressLint
-import android.app.Dialog
-import android.content.Intent
-import android.content.pm.PackageInfo
-import android.content.pm.PackageManager
-import android.net.Uri
-import android.os.Bundle
-import android.view.LayoutInflater
-import android.view.View
-import android.view.ViewGroup
-import androidx.appcompat.app.AlertDialog
-import androidx.fragment.app.DialogFragment
-import androidx.fragment.app.FragmentActivity
-import com.instructure.canvasapi2.managers.CourseManager
-import com.instructure.canvasapi2.managers.HelpLinksManager
-import com.instructure.canvasapi2.models.Course
-import com.instructure.canvasapi2.models.HelpLink
-import com.instructure.canvasapi2.models.HelpLinks
-import com.instructure.canvasapi2.utils.ApiPrefs
-import com.instructure.canvasapi2.utils.DateHelper
-import com.instructure.canvasapi2.utils.Logger
-import com.instructure.canvasapi2.utils.weave.awaitApi
-import com.instructure.canvasapi2.utils.weave.catch
-import com.instructure.canvasapi2.utils.weave.tryWeave
-import com.instructure.loginapi.login.dialog.ErrorReportDialog
-import com.instructure.pandautils.utils.AppType
-import com.instructure.pandautils.utils.Utils
-import com.instructure.pandautils.utils.onClick
-import com.instructure.pandautils.utils.setGone
-import com.instructure.student.R
-import com.instructure.student.activity.InternalWebViewActivity
-import com.instructure.student.util.LoggingUtility
-import kotlinx.android.synthetic.main.help_dialog.view.*
-import kotlinx.android.synthetic.main.view_help_link.view.*
-import kotlinx.coroutines.Job
-import java.util.*
-
-class HelpDialogStyled : DialogFragment() {
-
- var helpLinksJob: Job? = null
- private var helpLinks: HelpLinks? = null
-
-
- private val installDateString: String
- get() {
- return try {
- val installed = requireContext().packageManager
- .getPackageInfo(requireContext().packageName, 0)
- .firstInstallTime
- DateHelper.dayMonthYearFormat.format(Date(installed))
- } catch (e: Exception) {
- ""
- }
- }
-
- @SuppressLint("InflateParams") // Suppress lint warning about null parent when inflating layout
- override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
- val builder = AlertDialog.Builder(requireContext()).setTitle(requireContext().getString(R.string.help))
- val view = LayoutInflater.from(activity).inflate(R.layout.help_dialog, null)
-
- builder.setView(view)
-
- val dialog = builder.create()
- dialog.setCanceledOnTouchOutside(true)
-
- loadHelpLinks(view)
-
- return dialog
- }
-
- override fun onDestroyView() {
- if (retainInstance) dialog?.setDismissMessage(null)
- super.onDestroyView()
- }
-
- ///////////////////////////////////////////////////////////////////////////
- // Helpers
- ///////////////////////////////////////////////////////////////////////////
-
- private fun loadHelpLinks(layoutView: View) {
- helpLinksJob = tryWeave {
- with(layoutView) { emptyView.setLoading() }
-
- helpLinks = awaitApi { HelpLinksManager.getHelpLinks(it, true) }
-
- with(layoutView) {
- if (helpLinks?.customHelpLinks?.isNotEmpty() == true) {
- // We have custom links, let's use those
- addLinks(container, helpLinks!!.customHelpLinks)
- } else {
- // Default links
- addLinks(container, helpLinks!!.defaultHelpLinks)
- }
- emptyView.setGone()
- }
-
- } catch {
- Logger.d("Failed to grab help links: ${it.printStackTrace()}")
- }
- }
-
- /*
- Pass in the subject and first line of the e-mail, all the other data is the same
- */
- private fun populateMailIntent(subject: String, title: String, supportFlag: Boolean): Intent {
- // Let the user open their favorite mail client
- val intent = Intent(Intent.ACTION_SEND)
- intent.type = "message/rfc822"
-
- if (supportFlag) {
- intent.putExtra(Intent.EXTRA_EMAIL, arrayOf(getString(R.string.utils_supportEmailAddress)))
- } else {
- intent.putExtra(Intent.EXTRA_EMAIL, arrayOf(getString(R.string.utils_mobileSupportEmailAddress)))
- }
-
- // Try to get the version number and version code
- val pInfo: PackageInfo?
- var versionName = ""
- var versionCode = 0
- try {
- pInfo = requireContext().packageManager.getPackageInfo(requireContext().packageName, 0)
- versionName = pInfo.versionName
- versionCode = pInfo.versionCode
- } catch (e: PackageManager.NameNotFoundException) {
- LoggingUtility.logConsole(e.message)
- }
-
- intent.putExtra(Intent.EXTRA_SUBJECT, "[$subject] Issue with Canvas [Android] $versionName")
-
- val user = ApiPrefs.user
- // Populate the email body with information about the user
- var emailBody = ""
- emailBody += title + "\n"
- emailBody += getString(R.string.help_userId) + " " + user!!.id + "\n"
- emailBody += getString(R.string.help_email) + " " + user.email + "\n"
- emailBody += getString(R.string.help_domain) + " " + ApiPrefs.domain + "\n"
- emailBody += getString(R.string.help_versionNum) + " " + versionName + " " + versionCode + "\n"
- emailBody += getString(R.string.help_locale) + " " + Locale.getDefault() + "\n"
- emailBody += getString(R.string.installDate) + " " + installDateString + "\n"
- emailBody += "----------------------------------------------\n"
-
- intent.putExtra(Intent.EXTRA_TEXT, emailBody)
-
- return intent
- }
-
- // Maps links to views and then adds them to the container
- private suspend fun addLinks(container: ViewGroup, list: List) {
-
- // Share love link is specific to Android - Add it to the list returned from the API
- val linksList = list.toMutableList().apply {
- add(HelpLink("", "", listOf("student"), "#share_the_love", getString(R.string.shareYourLove), getString(R.string.shareYourLoveDetails)))
- }
- linksList
- // Only want links for students
- .filter { link ->
- (link.availableTo.contains("student") || link.availableTo.contains("user"))
- && (link.url != "#teacher_feedback" || awaitApi> { CourseManager.getAllFavoriteCourses(false, it) }.filter { !it.isTeacher }.count() > 0)
- }.forEach { link ->
- val view = layoutInflater.inflate(R.layout.view_help_link, null)
- view.title.text = link.text
- view.subtitle.text = link.subtext
- view.onClick { linkClick(link) }
- container.addView(view)
- }
- }
-
- private fun linkClick(link: HelpLink) =
- when {
- // Internal routes
- link.url[0] == '#' ->
- when (link.url) {
- "#create_ticket" -> {
- // Report a problem
- val dialog = ErrorReportDialog()
- dialog.arguments = ErrorReportDialog.createBundle(getString(R.string.appUserTypeStudent))
- dialog.show(requireActivity().supportFragmentManager, ErrorReportDialog.TAG)
- }
- "#teacher_feedback" -> {
- // Ask instructor a question
- // Open the ask instructor dialog
- AskInstructorDialogStyled().show(requireFragmentManager(), AskInstructorDialogStyled.TAG)
- }
- "#share_the_love" -> {
- Utils.goToAppStore(AppType.STUDENT, activity)
- }
- else -> { } // Not handling anything else at the moment
- }
- // External URL, but we handle within the app
- link.id.contains("submit_feature_idea") -> {
- // Before custom help links, we were handling request a feature ourselves and
- // we decided to keep that functionality instead of loading up the URL
-
- // Let the user open their favorite mail client
- val intent = populateMailIntent(getString(R.string.featureSubject), getString(R.string.understandRequest), false)
- startActivity(Intent.createChooser(intent, getString(R.string.sendMail)))
- }
- link.url.startsWith("tel:")-> {
- // Support phone links: https://community.canvaslms.com/docs/DOC-12664-4214610054
- val intent = Intent(Intent.ACTION_DIAL).apply { data = Uri.parse(link.url) }
- startActivity(intent)
- }
- link.url.startsWith("mailto:") -> {
- // Support mailto links: https://community.canvaslms.com/docs/DOC-12664-4214610054
- val intent = Intent(Intent.ACTION_SENDTO).apply { data = Uri.parse(link.url) }
- startActivity(intent)
- }
- link.url.contains("cases.canvaslms.com/liveagentchat") -> {
- // Chat with Canvas Support - Doesn't seem work properly with WebViews, so we kick it out
- // to the external browser
- val intent = Intent(Intent.ACTION_VIEW).apply { data = Uri.parse(link.url) }
- startActivity(intent)
- }
- // External URL
- else ->
- startActivity(InternalWebViewActivity.createIntent(activity, link.url, link.text, false))
- }
-
- companion object {
- const val TAG = "helpDialogStyled"
-
- fun show(activity: FragmentActivity): HelpDialogStyled =
- HelpDialogStyled().apply {
- show(activity.supportFragmentManager, TAG)
- }
- }
-}
diff --git a/apps/student/src/main/java/com/instructure/student/features/dashboard/edit/EditDashboardFragment.kt b/apps/student/src/main/java/com/instructure/student/features/dashboard/edit/EditDashboardFragment.kt
new file mode 100644
index 0000000000..263c45bb95
--- /dev/null
+++ b/apps/student/src/main/java/com/instructure/student/features/dashboard/edit/EditDashboardFragment.kt
@@ -0,0 +1,113 @@
+/*
+ * Copyright (C) 2021 - 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.dashboard.edit
+
+import android.content.Context
+import android.content.Intent
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.viewModels
+import androidx.lifecycle.Observer
+import androidx.localbroadcastmanager.content.LocalBroadcastManager
+import com.google.android.material.snackbar.Snackbar
+import com.instructure.interactions.router.Route
+import com.instructure.pandautils.utils.*
+import com.instructure.student.R
+import com.instructure.student.databinding.FragmentEditDashboardBinding
+import com.instructure.student.fragment.CourseBrowserFragment
+import com.instructure.student.router.RouteMatcher
+import dagger.hilt.android.AndroidEntryPoint
+import kotlinx.android.synthetic.main.fragment_edit_dashboard.*
+
+@AndroidEntryPoint
+class EditDashboardFragment : Fragment() {
+
+ private val viewModel: EditDashboardViewModel by viewModels()
+
+ override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
+ savedInstanceState: Bundle?): View? {
+ val binding = FragmentEditDashboardBinding.inflate(inflater, container, false)
+ binding.lifecycleOwner = this
+ binding.viewModel = viewModel
+
+ viewModel.events.observe(viewLifecycleOwner, Observer { event ->
+ event.getContentIfNotHandled()?.let {
+ handleAction(it)
+ }
+ })
+
+ return binding.root
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ setupToolbar()
+ }
+
+ override fun onHiddenChanged(hidden: Boolean) {
+ super.onHiddenChanged(hidden)
+ setupToolbar()
+ }
+
+ private fun setupToolbar() {
+ toolbar.setTitle(R.string.editDashboard)
+ toolbar.setupAsBackButton(this)
+ toolbar.addSearch {
+ viewModel.queryItems(it)
+ }
+ ViewStyler.themeToolbar(requireActivity(), toolbar, ThemePrefs.primaryColor, ThemePrefs.primaryTextColor)
+ }
+
+ override fun onStop() {
+ super.onStop()
+
+ if (viewModel.hasChanges) {
+ val intent = Intent(Const.COURSE_THING_CHANGED)
+ intent.putExtras(Bundle().apply { putBoolean(Const.COURSE_FAVORITES, true) })
+ LocalBroadcastManager.getInstance(requireContext()).sendBroadcast(intent)
+ }
+ }
+
+ private fun handleAction(action: EditDashboardItemAction) {
+ when (action) {
+ is EditDashboardItemAction.OpenItem -> {
+ RouteMatcher.route(requireContext(), CourseBrowserFragment.makeRoute(action.canvasContext))
+ }
+ is EditDashboardItemAction.ShowSnackBar -> {
+ Snackbar.make(requireView(), action.res, Snackbar.LENGTH_LONG).show()
+ }
+ }
+ }
+
+ companion object {
+
+ fun makeRoute() = Route(EditDashboardFragment::class.java, null)
+
+ fun validRoute(route: Route) = route.primaryClass == EditDashboardFragment::class.java
+
+ fun newInstance(route: Route): EditDashboardFragment? {
+ if (!validRoute(route)) return null
+ return EditDashboardFragment()
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/apps/student/src/main/java/com/instructure/student/features/dashboard/edit/EditDashboardViewData.kt b/apps/student/src/main/java/com/instructure/student/features/dashboard/edit/EditDashboardViewData.kt
new file mode 100644
index 0000000000..cac09bbbeb
--- /dev/null
+++ b/apps/student/src/main/java/com/instructure/student/features/dashboard/edit/EditDashboardViewData.kt
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2021 - 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.dashboard.edit
+
+import com.instructure.canvasapi2.models.CanvasContext
+import com.instructure.pandautils.mvvm.ItemViewModel
+import com.instructure.student.features.dashboard.edit.itemviewmodels.EditDashboardCourseItemViewModel
+import com.instructure.student.features.dashboard.edit.itemviewmodels.EditDashboardGroupItemViewModel
+
+data class EditDashboardViewData(val items: List)
+
+sealed class EditDashboardItemAction {
+ data class FavoriteCourse(val itemViewModel: EditDashboardCourseItemViewModel) : EditDashboardItemAction()
+ data class FavoriteGroup(val itemViewModel: EditDashboardGroupItemViewModel) : EditDashboardItemAction()
+ data class UnfavoriteCourse(val itemViewModel: EditDashboardCourseItemViewModel) : EditDashboardItemAction()
+ data class UnfavoriteGroup(val itemViewModel: EditDashboardGroupItemViewModel) : EditDashboardItemAction()
+ data class OpenItem(val canvasContext: CanvasContext?) : EditDashboardItemAction()
+ data class OpenCourse(val id: Long) : EditDashboardItemAction()
+ data class OpenGroup(val id: Long) : EditDashboardItemAction()
+ data class ShowSnackBar(val res: Int) : EditDashboardItemAction()
+}
+
+enum class EditDashboardItemViewType(val viewType: Int) {
+ COURSE(0),
+ GROUP(1),
+ HEADER(2),
+ DESCRIPTION(3),
+ ENROLLMENT(4)
+}
\ No newline at end of file
diff --git a/apps/student/src/main/java/com/instructure/student/features/dashboard/edit/EditDashboardViewModel.kt b/apps/student/src/main/java/com/instructure/student/features/dashboard/edit/EditDashboardViewModel.kt
new file mode 100644
index 0000000000..70c6506e12
--- /dev/null
+++ b/apps/student/src/main/java/com/instructure/student/features/dashboard/edit/EditDashboardViewModel.kt
@@ -0,0 +1,437 @@
+/*
+ * Copyright (C) 2021 - 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.dashboard.edit
+
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.instructure.canvasapi2.apis.EnrollmentAPI
+import com.instructure.canvasapi2.managers.CourseManager
+import com.instructure.canvasapi2.managers.GroupManager
+import com.instructure.canvasapi2.models.Course
+import com.instructure.canvasapi2.models.Group
+import com.instructure.canvasapi2.utils.*
+import com.instructure.pandautils.mvvm.Event
+import com.instructure.pandautils.mvvm.ItemViewModel
+import com.instructure.pandautils.mvvm.ViewState
+import com.instructure.student.R
+import com.instructure.student.features.dashboard.edit.itemviewmodels.*
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.launch
+import java.util.*
+import javax.inject.Inject
+
+@HiltViewModel
+class EditDashboardViewModel @Inject constructor(private val courseManager: CourseManager,
+ private val groupManager: GroupManager) : ViewModel() {
+
+ val state: LiveData
+ get() = _state
+ private val _state = MutableLiveData()
+
+ val data: LiveData
+ get() = _data
+ private val _data = MutableLiveData()
+
+ val events: LiveData>
+ get() = _events
+ private val _events = MutableLiveData>()
+
+ private var courseMap: Map? = null
+ private var groupMap: Map? = null
+
+ private val favoriteCourseMap: MutableMap = mutableMapOf()
+ private val favoriteGroupMap: MutableMap = mutableMapOf()
+
+ private lateinit var courses: List
+ private lateinit var groups: List
+
+ private lateinit var groupsViewData: List
+
+ private lateinit var groupHeader: EditDashboardHeaderViewModel
+ private lateinit var courseHeader: EditDashboardHeaderViewModel
+
+ private lateinit var currentCoursesViewData: List
+ private lateinit var pastCoursesViewData: List
+ private lateinit var futureCoursesViewData: List
+
+ var hasChanges = false
+
+ init {
+ _state.postValue(ViewState.Loading)
+ loadItems()
+ }
+
+ fun loadItems() {
+ viewModelScope.launch {
+ try {
+ courses = courseManager.getCoursesWithConcludedAsync(true).await().dataOrThrow
+ courses = courses.filter { it.isNotDeleted() && !it.isInvited() && !it.isEnrollmentDeleted() }
+ courseMap = courses.associateBy { it.id }
+
+ groups = groupManager.getAllGroupsAsync(true).await().dataOrThrow
+ groups = groups.filter { it.isActive(courseMap?.get(it.courseId)) }
+ groupMap = groups.associateBy { it.id }
+
+ val items = createListItems(courses, groups)
+ _data.postValue(EditDashboardViewData(items))
+ if (items.isEmpty()) {
+ _state.postValue(ViewState.Empty(R.string.edit_dashboard_empty_title, R.string.edit_dashboard_empty_message, R.drawable.ic_panda_nocourses))
+ } else {
+ _state.postValue(ViewState.Success)
+ }
+
+ } catch (e: Exception) {
+ _state.postValue(ViewState.Error())
+ Logger.d("Failed to grab courses: ${e.printStackTrace()}")
+ }
+ }
+ }
+
+ private fun handleAction(action: EditDashboardItemAction) {
+ when (action) {
+ is EditDashboardItemAction.OpenGroup -> {
+ _events.postValue(Event(EditDashboardItemAction.OpenItem(groupMap?.get(action.id))))
+ }
+
+ is EditDashboardItemAction.OpenCourse -> {
+ _events.postValue(Event(EditDashboardItemAction.OpenItem(courseMap?.get(action.id))))
+ }
+
+ is EditDashboardItemAction.FavoriteCourse -> {
+ viewModelScope.launch {
+ try {
+ addCourseToFavorites(action.itemViewModel)
+ courseManager.addCourseToFavoritesAsync(action.itemViewModel.id).await().dataOrThrow
+ _events.postValue(Event(EditDashboardItemAction.ShowSnackBar(R.string.added_to_dashboard)))
+ } catch (e: Exception) {
+ Logger.d("Failed to select course: ${e.printStackTrace()}")
+ removeCourseFromFavorites(action.itemViewModel)
+ _events.postValue(Event(EditDashboardItemAction.ShowSnackBar(R.string.errorOccurred)))
+ }
+ }
+
+ }
+
+ is EditDashboardItemAction.FavoriteGroup -> {
+ viewModelScope.launch {
+ try {
+ addGroupToFavorites(action.itemViewModel)
+ groupManager.addGroupToFavoritesAsync(action.itemViewModel.id).await().dataOrThrow
+ _events.postValue(Event(EditDashboardItemAction.ShowSnackBar(R.string.added_to_dashboard)))
+ } catch (e: Exception) {
+ Logger.d("Failed to select group: ${e.printStackTrace()}")
+ removeGroupFromFavorites(action.itemViewModel)
+ _events.postValue(Event(EditDashboardItemAction.ShowSnackBar(R.string.errorOccurred)))
+ }
+ }
+ }
+
+ is EditDashboardItemAction.UnfavoriteCourse -> {
+ viewModelScope.launch {
+ try {
+ removeCourseFromFavorites(action.itemViewModel)
+ courseManager.removeCourseFromFavoritesAsync(action.itemViewModel.id).await().dataOrThrow
+ _events.postValue(Event(EditDashboardItemAction.ShowSnackBar(R.string.removed_from_dashboard)))
+ } catch (e: Exception) {
+ Logger.d("Failed to deselect course: ${e.printStackTrace()}")
+ addCourseToFavorites(action.itemViewModel)
+ _events.postValue(Event(EditDashboardItemAction.ShowSnackBar(R.string.errorOccurred)))
+ }
+ }
+ }
+
+ is EditDashboardItemAction.UnfavoriteGroup -> {
+ viewModelScope.launch {
+ try {
+ removeGroupFromFavorites(action.itemViewModel)
+ groupManager.removeGroupFromFavoritesAsync(action.itemViewModel.id).await().dataOrThrow
+ _events.postValue(Event(EditDashboardItemAction.ShowSnackBar(R.string.removed_from_dashboard)))
+ } catch (e: Exception) {
+ Logger.d("Failed to deselect group: ${e.printStackTrace()}")
+ addGroupToFavorites(action.itemViewModel)
+ _events.postValue(Event(EditDashboardItemAction.ShowSnackBar(R.string.errorOccurred)))
+ }
+ }
+ }
+
+ is EditDashboardItemAction.ShowSnackBar -> {
+ _events.postValue(Event(action))
+ }
+ }
+ }
+
+ private fun addCourseToFavorites(item: EditDashboardCourseItemViewModel) {
+ hasChanges = true
+ favoriteCourseMap[item.id] = courseMap?.get(item.id)
+ ?: error("Course does not exist")
+ courseMap?.get(item.id)?.isFavorite = true
+ item.apply {
+ isFavorite = true
+ notifyChange()
+ }
+ courseHeader.apply {
+ hasItemSelected = favoriteCourseMap.isNotEmpty()
+ notifyChange()
+ }
+ }
+
+ private fun removeCourseFromFavorites(item: EditDashboardCourseItemViewModel) {
+ hasChanges = true
+ favoriteCourseMap.remove(item.id)
+ courseMap?.get(item.id)?.isFavorite = false
+ item.apply {
+ isFavorite = false
+ notifyChange()
+ }
+ courseHeader.apply {
+ hasItemSelected = favoriteCourseMap.isNotEmpty()
+ notifyChange()
+ }
+ }
+
+ private fun addGroupToFavorites(item: EditDashboardGroupItemViewModel) {
+ hasChanges = true
+ favoriteGroupMap[item.id] = groupMap?.get(item.id) ?: error("Group does not exist")
+ groupMap?.get(item.id)?.isFavorite = true
+ item.apply {
+ isFavorite = true
+ notifyChange()
+ }
+ groupHeader.apply {
+ hasItemSelected = favoriteGroupMap.isNotEmpty()
+ notifyChange()
+ }
+ }
+
+ private fun removeGroupFromFavorites(item: EditDashboardGroupItemViewModel) {
+ hasChanges = true
+ favoriteGroupMap.remove(item.id)
+ groupMap?.get(item.id)?.isFavorite = false
+ item.apply {
+ isFavorite = false
+ notifyChange()
+ }
+ groupHeader.apply {
+ hasItemSelected = favoriteGroupMap.isNotEmpty()
+ notifyChange()
+ }
+ }
+
+ private fun selectAllGroups() {
+ val groupsToFavorite = groupsViewData.filter { !it.isFavorite }
+ var counter = 0
+ groupsToFavorite.forEach {
+ viewModelScope.launch {
+ try {
+ addGroupToFavorites(it)
+ groupManager.addGroupToFavoritesAsync(it.id).await().dataOrThrow
+ counter++
+ if (counter == groupsToFavorite.size) {
+ _events.postValue(Event(EditDashboardItemAction.ShowSnackBar(R.string.all_added_to_dashboard)))
+ }
+ } catch (e: Exception) {
+ Logger.d("Failed to select all groups: ${e.printStackTrace()}")
+ removeGroupFromFavorites(it)
+ _events.postValue(Event(EditDashboardItemAction.ShowSnackBar(R.string.errorOccurred)))
+ }
+ }
+ }
+ }
+
+ private fun deselectAllGroups() {
+ val groupsToUnfavorite = groupsViewData.filter { it.isFavorite }
+ var counter = 0
+ groupsToUnfavorite.forEach {
+ viewModelScope.launch {
+ try {
+ removeGroupFromFavorites(it)
+ groupManager.removeGroupFromFavoritesAsync(it.id).await().dataOrThrow
+ counter++
+ if (counter == groupsToUnfavorite.size) {
+ _events.postValue(Event(EditDashboardItemAction.ShowSnackBar(R.string.all_removed_from_dashboard)))
+ }
+ } catch (e: Exception) {
+ Logger.d("Failed to select all courses: ${e.printStackTrace()}")
+ addGroupToFavorites(it)
+ _events.postValue(Event(EditDashboardItemAction.ShowSnackBar(R.string.errorOccurred)))
+ }
+ }
+ }
+ }
+
+ private fun selectAllCourses() {
+ val coursesToFavorite = (currentCoursesViewData + futureCoursesViewData).filter { !it.isFavorite && it.favoriteable }
+ var counter = 0
+ coursesToFavorite.forEach {
+ viewModelScope.launch {
+ try {
+ addCourseToFavorites(it)
+ courseManager.addCourseToFavoritesAsync(it.id).await().dataOrThrow
+ counter++
+ if (counter == coursesToFavorite.size) {
+ _events.postValue(Event(EditDashboardItemAction.ShowSnackBar(R.string.all_added_to_dashboard)))
+ }
+ } catch (e: Exception) {
+ Logger.d("Failed to select all courses: ${e.printStackTrace()}")
+ removeCourseFromFavorites(it)
+ _events.postValue(Event(EditDashboardItemAction.ShowSnackBar(R.string.errorOccurred)))
+ }
+ }
+ }
+ }
+
+ private fun deselectAllCourses() {
+ val coursesToUnfavorite = (currentCoursesViewData + futureCoursesViewData).filter { it.isFavorite }
+ var counter = 0
+ coursesToUnfavorite.forEach {
+ viewModelScope.launch {
+ try {
+ removeCourseFromFavorites(it)
+ courseManager.removeCourseFromFavoritesAsync(it.id).await().dataOrThrow
+ counter++
+ if (counter == coursesToUnfavorite.size) {
+ _events.postValue(Event(EditDashboardItemAction.ShowSnackBar(R.string.all_removed_from_dashboard)))
+ }
+ } catch (e: Exception) {
+ Logger.d("Failed to select all courses: ${e.printStackTrace()}")
+ addCourseToFavorites(it)
+ _events.postValue(Event(EditDashboardItemAction.ShowSnackBar(R.string.errorOccurred)))
+ }
+ }
+ }
+ }
+
+ private fun getCurrentCourses(courses: List): List {
+ favoriteCourseMap.clear()
+ val currentCourses = courses.filter { it.hasActiveEnrollment() && it.isBetweenValidDateRange() }
+ favoriteCourseMap.putAll(currentCourses.filter { it.isFavorite }.associateBy { it.id })
+ return currentCourses.map {
+ EditDashboardCourseItemViewModel(
+ id = it.id,
+ name = it.name,
+ isFavorite = it.isFavorite,
+ favoriteable = it.isValidTerm() && it.isNotDeleted() && it.isPublished(),
+ openable = it.isNotDeleted() && it.isPublished(),
+ termTitle = "${it.term?.name} | ${it.enrollments?.get(0)?.type?.apiTypeString}",
+ actionHandler = ::handleAction
+ )
+ }
+ }
+
+ private fun getPastCourses(courses: List): List {
+ val pastCourses = courses.filter { it.term?.endDate?.before(Date()) ?: false || it.endDate?.before(Date()) ?: false || it.isCompleted() }
+ return pastCourses.map {
+ EditDashboardCourseItemViewModel(
+ id = it.id,
+ name = it.name,
+ isFavorite = it.isFavorite,
+ favoriteable = false,
+ openable = it.isNotDeleted() && it.isPublished(),
+ termTitle = "${it.term?.name} | ${it.enrollments?.get(0)?.type?.apiTypeString}",
+ actionHandler = ::handleAction
+ )
+ }
+ }
+
+ private fun getFutureCourses(courses: List): List {
+ val futureCourses = courses.filter { it.term?.startDate?.after(Date()) ?: false || it.startDate?.after(Date()) ?: false || it.isCreationPending()}
+ favoriteCourseMap.putAll(futureCourses.filter { it.isFavorite }.associateBy { it.id })
+ return futureCourses.map {
+ EditDashboardCourseItemViewModel(
+ id = it.id,
+ name = it.name,
+ isFavorite = it.isFavorite,
+ favoriteable = it.isValidTerm() && it.isNotDeleted() && it.isPublished(),
+ openable = it.isNotDeleted() && it.isPublished(),
+ termTitle = "${it.term?.name} | ${it.enrollments?.get(0)?.type?.apiTypeString}",
+ actionHandler = ::handleAction
+ )
+ }
+ }
+
+ private fun getGroups(groups: List): List {
+ favoriteGroupMap.clear()
+ favoriteGroupMap.putAll(groups.filter { it.isFavorite }.associateBy { it.id })
+ return groups.map {
+ val course = courseMap?.get(it.courseId)
+ EditDashboardGroupItemViewModel(it.id, it.name, it.isFavorite, course?.name, course?.term?.name, ::handleAction)
+ }
+ }
+
+ private fun createListItems(courses: List, groups: List, isFiltered: Boolean = false): List {
+ currentCoursesViewData = getCurrentCourses(courses)
+ pastCoursesViewData = getPastCourses(courses)
+ futureCoursesViewData = getFutureCourses(courses)
+ groupsViewData = getGroups(groups)
+
+ val items = mutableListOf()
+ if (currentCoursesViewData.isNotEmpty() || pastCoursesViewData.isNotEmpty() || futureCoursesViewData.isNotEmpty()) {
+ val courseHeaderTitle = if (isFiltered) R.string.courses else R.string.all_courses
+ courseHeader = EditDashboardHeaderViewModel(courseHeaderTitle, favoriteCourseMap.isNotEmpty(), ::selectAllCourses, ::deselectAllCourses)
+ items.add(courseHeader)
+ items.add(EditDashboardDescriptionItemViewModel(R.string.edit_dashboard_course_description))
+ }
+
+ if (currentCoursesViewData.isNotEmpty()) {
+ items.add(EditDashboardEnrollmentItemViewModel(R.string.current_enrollments))
+ items.addAll(currentCoursesViewData)
+ }
+ if (pastCoursesViewData.isNotEmpty()) {
+ items.add(EditDashboardEnrollmentItemViewModel(R.string.past_enrollments))
+ items.addAll(pastCoursesViewData)
+ }
+ if (futureCoursesViewData.isNotEmpty()) {
+ items.add(EditDashboardEnrollmentItemViewModel(R.string.future_enrollments))
+ items.addAll(futureCoursesViewData)
+ }
+ if (groupsViewData.isNotEmpty()) {
+ val groupHeaderTitle = if (isFiltered) R.string.groups else R.string.all_groups
+ groupHeader = EditDashboardHeaderViewModel(groupHeaderTitle, favoriteGroupMap.isNotEmpty(), ::selectAllGroups, ::deselectAllGroups)
+ items.add(groupHeader)
+ items.add(EditDashboardDescriptionItemViewModel(R.string.edit_dashboard_group_description))
+ items.addAll(groupsViewData)
+ }
+
+ return items
+ }
+
+ fun queryItems(query: String) {
+ val items = if (query.isBlank()) {
+ createListItems(courses, groups)
+ } else {
+ val queriedCourses = courses.filter { it.name.contains(query, true) }
+ val queriedGroups = groups.filter { it.name?.contains(query, true) ?: false || it.description?.contains(query, true) ?: false || courseMap?.get(it.courseId)?.name?.contains(query, true) ?: false }
+
+ createListItems(queriedCourses, queriedGroups, true)
+ }
+ if (items.isEmpty()) {
+ _state.postValue(ViewState.Empty(R.string.edit_dashboard_empty_title, R.string.edit_dashboard_empty_message, R.drawable.ic_panda_nocourses))
+ } else {
+ _state.postValue(ViewState.Success)
+ }
+ _data.postValue(EditDashboardViewData(items))
+ }
+
+ fun refresh() {
+ _state.postValue(ViewState.Refresh)
+ loadItems()
+ }
+
+}
\ No newline at end of file
diff --git a/apps/student/src/main/java/com/instructure/student/features/dashboard/edit/itemviewmodels/EditDashboardCourseItemViewModel.kt b/apps/student/src/main/java/com/instructure/student/features/dashboard/edit/itemviewmodels/EditDashboardCourseItemViewModel.kt
new file mode 100644
index 0000000000..c8d740a4f6
--- /dev/null
+++ b/apps/student/src/main/java/com/instructure/student/features/dashboard/edit/itemviewmodels/EditDashboardCourseItemViewModel.kt
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2021 - 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.dashboard.edit.itemviewmodels
+
+import androidx.databinding.BaseObservable
+import androidx.databinding.Bindable
+import com.instructure.pandautils.mvvm.ItemViewModel
+import com.instructure.student.R
+import com.instructure.student.features.dashboard.edit.EditDashboardItemAction
+import com.instructure.student.features.dashboard.edit.EditDashboardItemViewType
+
+class EditDashboardCourseItemViewModel(
+ val id: Long,
+ val name: String?,
+ @get:Bindable var isFavorite: Boolean,
+ val favoriteable: Boolean,
+ val openable: Boolean,
+ val termTitle: String?,
+ private val actionHandler: (EditDashboardItemAction) -> Unit
+) : ItemViewModel, BaseObservable() {
+
+ override val layoutId: Int = R.layout.viewholder_edit_dashboard_course
+
+ override val viewType: Int = EditDashboardItemViewType.COURSE.viewType
+
+ fun onClick() {
+ if (!openable) {
+ actionHandler(EditDashboardItemAction.ShowSnackBar(R.string.unauthorized))
+ return
+ }
+
+ actionHandler(EditDashboardItemAction.OpenCourse(id))
+ }
+
+ fun onFavoriteClick() {
+ if (!favoriteable) {
+ actionHandler(EditDashboardItemAction.ShowSnackBar(R.string.inactive_courses_cant_be_added_to_dashboard))
+ return
+ }
+
+ if (isFavorite) {
+ actionHandler(EditDashboardItemAction.UnfavoriteCourse(this))
+ } else {
+ actionHandler(EditDashboardItemAction.FavoriteCourse(this))
+ }
+ }
+}
\ No newline at end of file
diff --git a/apps/student/src/main/java/com/instructure/student/features/dashboard/edit/itemviewmodels/EditDashboardDescriptionItemViewModel.kt b/apps/student/src/main/java/com/instructure/student/features/dashboard/edit/itemviewmodels/EditDashboardDescriptionItemViewModel.kt
new file mode 100644
index 0000000000..21d452805a
--- /dev/null
+++ b/apps/student/src/main/java/com/instructure/student/features/dashboard/edit/itemviewmodels/EditDashboardDescriptionItemViewModel.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2021 - 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.dashboard.edit.itemviewmodels
+
+import androidx.annotation.StringRes
+import com.instructure.pandautils.mvvm.ItemViewModel
+import com.instructure.student.features.dashboard.edit.EditDashboardItemViewType
+import com.instructure.student.R
+
+class EditDashboardDescriptionItemViewModel(@get:StringRes val description: Int) : ItemViewModel {
+ override val layoutId: Int = R.layout.viewholder_edit_dashboard_description
+
+ override val viewType: Int = EditDashboardItemViewType.DESCRIPTION.viewType
+}
\ No newline at end of file
diff --git a/apps/student/src/main/java/com/instructure/student/features/dashboard/edit/itemviewmodels/EditDashboardEnrollmentItemViewModel.kt b/apps/student/src/main/java/com/instructure/student/features/dashboard/edit/itemviewmodels/EditDashboardEnrollmentItemViewModel.kt
new file mode 100644
index 0000000000..15e97a44ce
--- /dev/null
+++ b/apps/student/src/main/java/com/instructure/student/features/dashboard/edit/itemviewmodels/EditDashboardEnrollmentItemViewModel.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2021 - 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.dashboard.edit.itemviewmodels
+
+import androidx.annotation.StringRes
+import com.instructure.pandautils.mvvm.ItemViewModel
+import com.instructure.student.R
+import com.instructure.student.features.dashboard.edit.EditDashboardItemViewType
+
+class EditDashboardEnrollmentItemViewModel(@get:StringRes val title: Int) : ItemViewModel {
+ override val layoutId: Int = R.layout.viewholder_edit_dashboard_enrollment
+
+ override val viewType: Int = EditDashboardItemViewType.ENROLLMENT.viewType
+}
\ No newline at end of file
diff --git a/apps/student/src/main/java/com/instructure/student/features/dashboard/edit/itemviewmodels/EditDashboardGroupItemViewModel.kt b/apps/student/src/main/java/com/instructure/student/features/dashboard/edit/itemviewmodels/EditDashboardGroupItemViewModel.kt
new file mode 100644
index 0000000000..55fbd34760
--- /dev/null
+++ b/apps/student/src/main/java/com/instructure/student/features/dashboard/edit/itemviewmodels/EditDashboardGroupItemViewModel.kt
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2021 - 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.dashboard.edit.itemviewmodels
+
+import androidx.databinding.BaseObservable
+import androidx.databinding.Bindable
+import com.instructure.pandautils.mvvm.ItemViewModel
+import com.instructure.student.features.dashboard.edit.EditDashboardItemAction
+import com.instructure.student.R
+import com.instructure.student.features.dashboard.edit.EditDashboardItemViewType
+
+class EditDashboardGroupItemViewModel(
+ val id: Long,
+ val name: String?,
+ @get:Bindable var isFavorite: Boolean,
+ val subtitle: String?,
+ val termTitle: String?,
+ private val actionHandler: (EditDashboardItemAction) -> Unit
+) : ItemViewModel, BaseObservable() {
+ override val layoutId: Int = R.layout.viewholder_edit_dashboard_group
+
+ override val viewType: Int = EditDashboardItemViewType.GROUP.viewType
+
+ fun onClick() {
+ actionHandler(EditDashboardItemAction.OpenGroup(id))
+ }
+
+ fun onFavoriteClick() {
+ if (isFavorite) {
+ actionHandler(EditDashboardItemAction.UnfavoriteGroup(this))
+ } else {
+ actionHandler(EditDashboardItemAction.FavoriteGroup(this))
+ }
+ }
+}
\ No newline at end of file
diff --git a/apps/student/src/main/java/com/instructure/student/features/dashboard/edit/itemviewmodels/EditDashboardHeaderViewModel.kt b/apps/student/src/main/java/com/instructure/student/features/dashboard/edit/itemviewmodels/EditDashboardHeaderViewModel.kt
new file mode 100644
index 0000000000..915ebe9cf3
--- /dev/null
+++ b/apps/student/src/main/java/com/instructure/student/features/dashboard/edit/itemviewmodels/EditDashboardHeaderViewModel.kt
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2021 - 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.dashboard.edit.itemviewmodels
+
+import androidx.annotation.StringRes
+import androidx.databinding.BaseObservable
+import androidx.databinding.Bindable
+import com.instructure.pandautils.mvvm.ItemViewModel
+import com.instructure.student.R
+import com.instructure.student.features.dashboard.edit.EditDashboardItemViewType
+
+class EditDashboardHeaderViewModel(
+ @get:StringRes val title: Int,
+ @get:Bindable var hasItemSelected: Boolean,
+ val selectAllHandler: () -> Unit,
+ val deselectAllHandler: () -> Unit
+) : ItemViewModel, BaseObservable() {
+ override val layoutId: Int = R.layout.viewholder_edit_dashboard_header
+
+ override val viewType: Int = EditDashboardItemViewType.HEADER.viewType
+
+ fun onActionClick() {
+ if (hasItemSelected) {
+ deselectAllHandler()
+ } else {
+ selectAllHandler()
+ }
+ }
+}
\ No newline at end of file
diff --git a/apps/student/src/main/java/com/instructure/student/fragment/AllCoursesFragment.kt b/apps/student/src/main/java/com/instructure/student/fragment/AllCoursesFragment.kt
deleted file mode 100644
index ab02de043b..0000000000
--- a/apps/student/src/main/java/com/instructure/student/fragment/AllCoursesFragment.kt
+++ /dev/null
@@ -1,185 +0,0 @@
-/*
- * Copyright (C) 2017 - present Instructure, Inc.
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, version 3 of the License.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
- *
- */
-
-package com.instructure.student.fragment
-
-import android.content.BroadcastReceiver
-import android.content.Context
-import android.content.Intent
-import android.content.IntentFilter
-import android.content.res.Configuration
-import android.os.Bundle
-import android.view.LayoutInflater
-import android.view.View
-import android.view.ViewGroup
-import androidx.localbroadcastmanager.content.LocalBroadcastManager
-import com.instructure.canvasapi2.managers.CourseNicknameManager
-import com.instructure.canvasapi2.managers.UserManager
-import com.instructure.canvasapi2.models.*
-import com.instructure.canvasapi2.utils.pageview.PageView
-import com.instructure.canvasapi2.utils.weave.awaitApi
-import com.instructure.canvasapi2.utils.weave.catch
-import com.instructure.canvasapi2.utils.weave.tryWeave
-import com.instructure.interactions.router.Route
-import com.instructure.pandautils.utils.*
-import com.instructure.student.R
-import com.instructure.student.adapter.AllCoursesRecyclerAdapter
-import com.instructure.student.dialog.ColorPickerDialog
-import com.instructure.student.dialog.EditCourseNicknameDialog
-import com.instructure.student.events.CourseColorOverlayToggledEvent
-import com.instructure.student.events.ShowGradesToggledEvent
-import com.instructure.student.flutterChannels.FlutterComm
-import com.instructure.student.interfaces.CourseAdapterToFragmentCallback
-import com.instructure.student.router.RouteMatcher
-import kotlinx.android.synthetic.main.fragment_all_courses.*
-import kotlinx.android.synthetic.main.panda_recycler_refresh_layout.*
-import org.greenrobot.eventbus.EventBus
-import org.greenrobot.eventbus.Subscribe
-import kotlinx.android.synthetic.main.fragment_all_courses.allCoursesFragmentContainer as rootView
-import kotlinx.android.synthetic.main.panda_recycler_refresh_layout.listView as recyclerView
-
-@PageView(url = "courses")
-class AllCoursesFragment : ParentFragment() {
- private var recyclerAdapter: AllCoursesRecyclerAdapter? = null
-
- private val somethingChangedReceiver = object : BroadcastReceiver() {
- override fun onReceive(context: Context, intent: Intent?) {
- if (recyclerAdapter != null && intent?.extras?.getBoolean(Const.COURSE_FAVORITES) == true) {
- swipeRefreshLayout?.isRefreshing = true
- recyclerAdapter?.refresh()
- }
- }
- }
-
- override fun title(): String = if (isAdded) getString(R.string.allCourses) else ""
-
- override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? =
- layoutInflater.inflate(R.layout.fragment_all_courses, container, false)
-
- override fun onActivityCreated(savedInstanceState: Bundle?) {
- super.onActivityCreated(savedInstanceState)
- recyclerAdapter = AllCoursesRecyclerAdapter(requireActivity(), object : CourseAdapterToFragmentCallback {
- override fun onRemoveAnnouncement(announcement: AccountNotification, position: Int) = Unit
- override fun onHandleCourseInvitation(course: Course, accepted: Boolean) = Unit
- override fun onConferenceSelected(conference: Conference) = Unit
- override fun onDismissConference(conference: Conference) = Unit
- override fun onSeeAllCourses() = Unit
- override fun onGroupSelected(group: Group) = Unit
-
- override fun onRefreshFinished() {
- swipeRefreshLayout?.isRefreshing = false
- }
-
- override fun onCourseSelected(course: Course) {
- RouteMatcher.route(requireContext(), CourseBrowserFragment.makeRoute(course))
- }
-
- @Suppress("EXPERIMENTAL_FEATURE_WARNING")
- override fun onEditCourseNickname(course: Course) {
- EditCourseNicknameDialog.getInstance(requireFragmentManager(), course) { s ->
- tryWeave {
- val response = awaitApi { CourseNicknameManager.setCourseNickname(course.id, s, it) }
- if (response.nickname == null) {
- course.name = response.name!!
- course.originalName = null
- } else {
- course.name = response.nickname!!
- course.originalName = response.name
- }
- recyclerAdapter?.add(course)
- } catch {
- toast(R.string.courseNicknameError)
- }
- }.show(requireFragmentManager(), EditCourseNicknameDialog::class.java.simpleName)
- }
-
- @Suppress("EXPERIMENTAL_FEATURE_WARNING")
- override fun onPickCourseColor(course: Course) {
- ColorPickerDialog.newInstance(requireFragmentManager(), course) { color ->
- tryWeave {
- awaitApi { UserManager.setColors(it, course.contextId, color) }
- ColorKeeper.addToCache(course.contextId, color)
- FlutterComm.sendUpdatedTheme()
- recyclerAdapter?.notifyDataSetChanged()
- } catch {
- toast(R.string.colorPickerError)
- }
- }.show(requireFragmentManager(), ColorPickerDialog::class.java.simpleName)
- }
- })
-
- configureRecyclerView()
- recyclerView.isSelectionEnabled = false
- }
-
- override fun applyTheme() {
- toolbar.title = getString(R.string.allCourses)
- toolbar.setupAsBackButton(this)
- ViewStyler.themeToolbar(requireActivity(), toolbar, ThemePrefs.primaryColor, ThemePrefs.primaryTextColor)
- }
-
- override fun onConfigurationChanged(newConfig: Configuration) {
- super.onConfigurationChanged(newConfig)
- configureRecyclerView()
- }
-
- private fun configureRecyclerView() {
- val courseColumns = resources.getInteger(R.integer.course_card_columns)
- configureRecyclerViewAsGrid(view!!, recyclerAdapter!!, R.id.swipeRefreshLayout, R.id.emptyView, R.id.listView, R.string.no_courses_available, courseColumns)
- }
-
- override fun onStart() {
- super.onStart()
- LocalBroadcastManager.getInstance(requireContext()).registerReceiver(somethingChangedReceiver, IntentFilter(Const.COURSE_THING_CHANGED))
- EventBus.getDefault().register(this)
- }
-
- override fun onStop() {
- super.onStop()
- LocalBroadcastManager.getInstance(requireContext()).unregisterReceiver(somethingChangedReceiver)
- EventBus.getDefault().unregister(this)
- }
-
- @Suppress("unused", "UNUSED_PARAMETER")
- @Subscribe
- fun onShowGradesToggled(event: ShowGradesToggledEvent) {
- recyclerAdapter?.notifyDataSetChanged()
- }
-
- @Suppress("unused", "UNUSED_PARAMETER")
- @Subscribe(sticky = true)
- fun onColorOverlayToggled(event: CourseColorOverlayToggledEvent) {
- recyclerAdapter?.notifyDataSetChanged()
- }
-
- override fun onDestroy() {
- if (recyclerAdapter != null) recyclerAdapter?.cancel()
- super.onDestroy()
- }
-
- companion object {
- fun makeRoute() = Route(AllCoursesFragment::class.java, null)
-
- fun validRoute(route: Route) = route.primaryClass == AllCoursesFragment::class.java
-
- fun newInstance(route: Route): AllCoursesFragment? {
- if (!validRoute(route)) return null
- return AllCoursesFragment()
- }
-
- }
-}
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 4f6f2378b2..b23cadb342 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
@@ -36,7 +36,6 @@ import com.instructure.student.R
import com.instructure.student.activity.NothingToSeeHereFragment
import com.instructure.student.activity.NotificationPreferencesActivity
import com.instructure.student.activity.SettingsActivity
-import com.instructure.student.dialog.HelpDialogStyled
import com.instructure.student.dialog.LegalDialogStyled
import com.instructure.student.mobius.settings.pairobserver.ui.PairObserverFragment
import com.instructure.student.util.Analytics
@@ -82,15 +81,16 @@ class ApplicationSettingsFragment : ParentFragment() {
}
legal.onClick { LegalDialogStyled().show(requireFragmentManager(), LegalDialogStyled.TAG) }
- help.onClick { HelpDialogStyled.show(requireActivity()) }
pinAndFingerprint.setGone() // TODO: Wire up once implemented
- pairObserver.setVisible()
- pairObserver.onClick {
- if (APIHelper.hasNetworkConnection()) {
- addFragment(PairObserverFragment.newInstance())
- } else {
- NoInternetConnectionDialog.show(requireFragmentManager())
+ if (ApiPrefs.canGeneratePairingCode == true) {
+ pairObserver.setVisible()
+ pairObserver.onClick {
+ if (APIHelper.hasNetworkConnection()) {
+ addFragment(PairObserverFragment.newInstance())
+ } else {
+ NoInternetConnectionDialog.show(requireFragmentManager())
+ }
}
}
@@ -101,15 +101,24 @@ class ApplicationSettingsFragment : ParentFragment() {
about.onClick {
AlertDialog.Builder(requireContext())
- .setTitle(R.string.about)
- .setView(R.layout.dialog_about)
- .show()
- .apply {
- domain.text = ApiPrefs.domain
- loginId.text = ApiPrefs.user!!.loginId
- email.text = ApiPrefs.user!!.email ?: ApiPrefs.user!!.primaryEmail
- version.text = "${getString(R.string.canvasVersionNum)} ${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})"
- }
+ .setTitle(R.string.about)
+ .setView(R.layout.dialog_about)
+ .show()
+ .apply {
+ domain.text = ApiPrefs.domain
+ loginId.text = ApiPrefs.user!!.loginId
+ email.text = ApiPrefs.user!!.email ?: ApiPrefs.user!!.primaryEmail
+ version.text = "${getString(R.string.canvasVersionNum)} ${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})"
+ }
+ }
+
+ if (ApiPrefs.canvasForElementary) {
+ elementaryViewSwitch.isChecked = ApiPrefs.elementaryDashboardEnabledOverride
+ elementaryViewLayout.setVisible()
+ ViewStyler.themeSwitch(requireContext(), elementaryViewSwitch, ThemePrefs.brandColor)
+ elementaryViewSwitch.setOnCheckedChangeListener { _, isChecked ->
+ ApiPrefs.elementaryDashboardEnabledOverride = isChecked
+ }
}
if (BuildConfig.DEBUG) {
diff --git a/apps/student/src/main/java/com/instructure/student/fragment/AssignmentListFragment.kt b/apps/student/src/main/java/com/instructure/student/fragment/AssignmentListFragment.kt
index c74ef0e33f..327ca6870c 100644
--- a/apps/student/src/main/java/com/instructure/student/fragment/AssignmentListFragment.kt
+++ b/apps/student/src/main/java/com/instructure/student/fragment/AssignmentListFragment.kt
@@ -17,16 +17,21 @@
package com.instructure.student.fragment
+import android.app.AlertDialog
+import android.content.DialogInterface
import android.content.res.Configuration
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.AdapterView
+import androidx.annotation.StringRes
import com.google.android.material.appbar.AppBarLayout
import com.instructure.canvasapi2.models.Assignment
import com.instructure.canvasapi2.models.CanvasContext
import com.instructure.canvasapi2.models.GradingPeriod
+import com.instructure.canvasapi2.utils.Analytics
+import com.instructure.canvasapi2.utils.AnalyticsEventConstants
import com.instructure.canvasapi2.utils.pageview.PageView
import com.instructure.interactions.bookmarks.Bookmarkable
import com.instructure.interactions.bookmarks.Bookmarker
@@ -34,11 +39,14 @@ import com.instructure.interactions.router.Route
import com.instructure.interactions.router.RouterParams
import com.instructure.pandautils.utils.*
import com.instructure.student.R
-import com.instructure.student.adapter.AssignmentDateListRecyclerAdapter
import com.instructure.student.adapter.TermSpinnerAdapter
+import com.instructure.student.adapter.assignment.AssignmentListByDateRecyclerAdapter
+import com.instructure.student.adapter.assignment.AssignmentListByTypeRecyclerAdapter
+import com.instructure.student.adapter.assignment.AssignmentListRecyclerAdapter
import com.instructure.student.interfaces.AdapterToAssignmentsCallback
import com.instructure.student.mobius.assignmentDetails.ui.AssignmentDetailsFragment
import com.instructure.student.router.RouteMatcher
+import com.instructure.student.util.StudentPrefs
import kotlinx.android.synthetic.main.assignment_list_layout.*
@PageView(url = "{canvasContext}/assignments")
@@ -46,17 +54,28 @@ class AssignmentListFragment : ParentFragment(), Bookmarkable {
private var canvasContext by ParcelableArg(key = Const.CANVAS_CONTEXT)
- private lateinit var recyclerAdapter: AssignmentDateListRecyclerAdapter
+ private lateinit var recyclerAdapter: AssignmentListRecyclerAdapter
private var termAdapter: TermSpinnerAdapter? = null
+ private var sortOrder: AssignmentsSortOrder
+ get() {
+ val preferenceKey = StudentPrefs.getString("sortBy_${canvasContext.contextId}", AssignmentsSortOrder.SORT_BY_TIME.preferenceKey)
+ return AssignmentsSortOrder.fromPreferenceKey(preferenceKey)
+ }
+ set(value) {
+ StudentPrefs.putString("sortBy_${canvasContext.contextId}", value.preferenceKey)
+ }
+
private val allTermsGradingPeriod by lazy {
- GradingPeriod().apply { title = getString(R.string.allGradingPeriods) }
+ GradingPeriod().apply { title = getString(R.string.assignmentsListAllGradingPeriods) }
}
private val adapterToAssignmentsCallback = object : AdapterToAssignmentsCallback {
- override fun setTermSpinnerState(isEnabled: Boolean) {
- termSpinner?.isEnabled = isEnabled
- termAdapter?.isLoading = !isEnabled
+ override fun assignmentLoadingFinished() {
+ // If we only have one grading period we want to disable the spinner
+ val termCount = termAdapter?.count ?: 0
+ termSpinner?.isEnabled = termCount > 1
+ termAdapter?.isLoading = false
termAdapter?.notifyDataSetChanged()
}
@@ -88,11 +107,10 @@ class AssignmentListFragment : ParentFragment(), Bookmarkable {
inflater.inflate(R.layout.assignment_list_layout, container, false)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
- recyclerAdapter = AssignmentDateListRecyclerAdapter(
- requireContext(),
- canvasContext,
- adapterToAssignmentsCallback
- )
+ recyclerAdapter = createRecyclerAdapter()
+
+ sortByTextView.setText(sortOrder.buttonTextRes)
+ sortByButton.contentDescription = getString(sortOrder.contentDescriptionRes)
configureRecyclerView(
view,
@@ -111,6 +129,41 @@ class AssignmentListFragment : ParentFragment(), Bookmarkable {
setRefreshingEnabled(false)
}
})
+
+ setupSortByButton()
+ }
+
+ private fun createRecyclerAdapter(): AssignmentListRecyclerAdapter {
+ return if (sortOrder == AssignmentsSortOrder.SORT_BY_TIME) {
+ AssignmentListByDateRecyclerAdapter(requireContext(), canvasContext, adapterToAssignmentsCallback)
+ } else {
+ AssignmentListByTypeRecyclerAdapter(requireContext(), canvasContext, adapterToAssignmentsCallback)
+ }
+ }
+
+ private fun setupSortByButton() {
+ sortByButton.onClick {
+ val checkedItemIndex = sortOrder.index
+ AlertDialog.Builder(context, R.style.AccentDialogTheme)
+ .setTitle(R.string.sortByDialogTitle)
+ .setSingleChoiceItems(R.array.assignmentsSortByOptions, checkedItemIndex, this@AssignmentListFragment::sortOrderSelected)
+ .setNegativeButton(R.string.sortByDialogCancel) { dialog, _ -> dialog.dismiss() }
+ .show()
+ }
+ }
+
+ private fun sortOrderSelected(dialog: DialogInterface, index: Int) {
+ dialog.dismiss()
+ val selectedSortOrder = AssignmentsSortOrder.fromIndex(index)
+ if (sortOrder != selectedSortOrder) {
+ sortOrder = selectedSortOrder
+ recyclerAdapter = createRecyclerAdapter()
+ listView.adapter = recyclerAdapter
+ sortByTextView.setText(selectedSortOrder.buttonTextRes)
+ sortByButton.contentDescription = getString(selectedSortOrder.contentDescriptionRes)
+ Analytics.logEvent(selectedSortOrder.analyticsKey)
+ listView.announceForAccessibility(getString(selectedSortOrder.orderSelectedAnnouncement))
+ }
}
override fun applyTheme() {
@@ -129,16 +182,19 @@ class AssignmentListFragment : ParentFragment(), Bookmarkable {
}
private fun setupGradingPeriods(periods: List) {
+ val hasGradingPeriods = periods.isNotEmpty()
val adapter = TermSpinnerAdapter(
requireContext(),
android.R.layout.simple_spinner_dropdown_item,
- periods + allTermsGradingPeriod
+ periods + allTermsGradingPeriod,
+ hasGradingPeriods
)
+ termSpinner.isEnabled = hasGradingPeriods
termAdapter = adapter
termSpinner.adapter = adapter
termSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(adapterView: AdapterView<*>, view: View?, i: Int, l: Long) {
- if (adapter.getItem(i)!!.title == getString(R.string.allGradingPeriods)) {
+ if (adapter.getItem(i)!!.title == getString(R.string.assignmentsListAllGradingPeriods)) {
recyclerAdapter.loadAssignment()
} else {
recyclerAdapter.loadAssignmentsForGradingPeriod(adapter.getItem(i)!!.id, true)
@@ -153,7 +209,7 @@ class AssignmentListFragment : ParentFragment(), Bookmarkable {
}
// If we have a "current" grading period select it
- if (recyclerAdapter.currentGradingPeriod != null) {
+ if (hasGradingPeriods && recyclerAdapter.currentGradingPeriod != null) {
val position = adapter.getPositionForId(recyclerAdapter.currentGradingPeriod?.id ?: 0)
if (position != -1) {
termSpinner.setSelection(position)
@@ -161,8 +217,6 @@ class AssignmentListFragment : ParentFragment(), Bookmarkable {
toast(R.string.errorOccurred)
}
}
-
- termSpinnerLayout.setVisible()
}
override fun handleBackPressed() = toolbar.closeSearch()
@@ -215,3 +269,36 @@ class AssignmentListFragment : ParentFragment(), Bookmarkable {
}
}
+
+enum class AssignmentsSortOrder(
+ val index: Int,
+ val preferenceKey: String,
+ @StringRes val buttonTextRes: Int,
+ @StringRes val contentDescriptionRes: Int,
+ @StringRes val orderSelectedAnnouncement: Int,
+ val analyticsKey: String) {
+
+ SORT_BY_TIME(0, "time", R.string.sortByTime, R.string.a11y_sortByTimeButton,
+ R.string.a11y_assignmentsSortedByTime, AnalyticsEventConstants.ASSIGNMENT_LIST_SORT_BY_TIME_SELECTED),
+
+ SORT_BY_TYPE(1, "type", R.string.sortByType, R.string.a11y_sortByTypeButton,
+ R.string.a11y_assignmentsSortedByType, AnalyticsEventConstants.ASSIGNMENT_LIST_SORT_BY_TYPE_SELECTED);
+
+ companion object {
+ fun fromPreferenceKey(key: String?): AssignmentsSortOrder {
+ return when (key) {
+ SORT_BY_TIME.preferenceKey -> SORT_BY_TIME
+ SORT_BY_TYPE.preferenceKey -> SORT_BY_TYPE
+ else -> SORT_BY_TIME // This will be the default value
+ }
+ }
+
+ fun fromIndex(key: Int): AssignmentsSortOrder {
+ return when (key) {
+ SORT_BY_TIME.index -> SORT_BY_TIME
+ SORT_BY_TYPE.index -> SORT_BY_TYPE
+ else -> SORT_BY_TIME // This will be the default value
+ }
+ }
+ }
+}
diff --git a/apps/student/src/main/java/com/instructure/student/fragment/CalendarFragment.kt b/apps/student/src/main/java/com/instructure/student/fragment/CalendarFragment.kt
index c6089f8f66..ee0ece3622 100644
--- a/apps/student/src/main/java/com/instructure/student/fragment/CalendarFragment.kt
+++ b/apps/student/src/main/java/com/instructure/student/fragment/CalendarFragment.kt
@@ -17,11 +17,13 @@
package com.instructure.student.fragment
+import android.app.AlertDialog
import android.graphics.Color
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
+import com.instructure.canvasapi2.models.PlannableType
import com.instructure.canvasapi2.models.PlannerItem
import com.instructure.canvasapi2.utils.ApiPrefs
import com.instructure.canvasapi2.utils.pageview.PageView
@@ -33,6 +35,8 @@ import com.instructure.student.activity.NavigationActivity
import com.instructure.student.flutterChannels.FlutterComm
import com.instructure.student.mobius.assignmentDetails.ui.AssignmentDetailsFragment
import com.instructure.student.router.RouteMatcher
+import io.flutter.plugin.common.MethodCall
+import io.flutter.plugin.common.MethodChannel
import kotlinx.android.extensions.CacheImplementation
import kotlinx.android.extensions.ContainerOptions
@@ -66,6 +70,7 @@ class CalendarFragment : ParentFragment() {
private fun setupChannelCallbacks(channel: CalendarScreenChannel) {
channel.onRouteToItem = ::routeToItem
channel.onOpenDrawer = ::openDrawer
+ channel.onShowDialog = ::showDialog
}
private fun openDrawer() {
@@ -74,13 +79,13 @@ class CalendarFragment : ParentFragment() {
private fun routeToItem(item: PlannerItem) {
val route: Route? = when (item.plannableType) {
- "assignment" -> {
+ PlannableType.ASSIGNMENT -> {
AssignmentDetailsFragment.makeRoute(item.canvasContext, item.plannable.id)
}
- "discussion_topic" -> {
+ PlannableType.DISCUSSION_TOPIC -> {
DiscussionDetailsFragment.makeRoute(item.canvasContext, item.plannable.id, title = item.plannable.title)
}
- "quiz" -> {
+ PlannableType.QUIZ -> {
if (item.plannable.assignmentId != null) {
// This is a quiz assignment, go to the assignment page
AssignmentDetailsFragment.makeRoute(item.canvasContext, item.plannable.assignmentId!!)
@@ -92,7 +97,7 @@ class CalendarFragment : ParentFragment() {
} else null
}
}
- "calendar_event" -> {
+ PlannableType.CALENDAR_EVENT -> {
CalendarEventFragment.makeRoute(item.canvasContext, item.plannable.id)
}
else -> {
@@ -104,6 +109,16 @@ class CalendarFragment : ParentFragment() {
route?.let { RouteMatcher.route(requireContext(), it) }
}
+ private fun showDialog(call: MethodCall, result: MethodChannel.Result) {
+ AlertDialog.Builder(activity, R.style.AccentDialogTheme)
+ .setTitle(call.argument("title"))
+ .setMessage(call.argument("message"))
+ .setPositiveButton(call.argument("positiveButtonText")) { _, _ -> result.success(true) }
+ .setNegativeButton(call.argument("negativeButtonText")) { _, _ -> result.success(false) }
+ .create()
+ .show()
+ }
+
override fun handleBackPressed(): Boolean = flutterFragment?.handleBackPressed() ?: false
private val flutterFragment: FlutterCalendarFragment?
diff --git a/apps/student/src/main/java/com/instructure/student/fragment/CourseModuleProgressionFragment.kt b/apps/student/src/main/java/com/instructure/student/fragment/CourseModuleProgressionFragment.kt
index 7e43381a73..942e8b5d3f 100644
--- a/apps/student/src/main/java/com/instructure/student/fragment/CourseModuleProgressionFragment.kt
+++ b/apps/student/src/main/java/com/instructure/student/fragment/CourseModuleProgressionFragment.kt
@@ -31,10 +31,7 @@ import androidx.viewpager.widget.ViewPager
import com.instructure.canvasapi2.StatusCallback
import com.instructure.canvasapi2.managers.ModuleManager
import com.instructure.canvasapi2.models.*
-import com.instructure.canvasapi2.utils.ApiType
-import com.instructure.canvasapi2.utils.LinkHeaders
-import com.instructure.canvasapi2.utils.Logger
-import com.instructure.canvasapi2.utils.isRtl
+import com.instructure.canvasapi2.utils.*
import com.instructure.canvasapi2.utils.weave.WeaveJob
import com.instructure.canvasapi2.utils.weave.awaitApi
import com.instructure.canvasapi2.utils.weave.catch
@@ -73,7 +70,7 @@ class CourseModuleProgressionFragment : ParentFragment(), Bookmarkable {
private var moduleItemId: String by StringArg(key = ITEM_ID)
// Default number will get reset
- private var NUM_ITEMS = 3
+ private var itemsCount = 3
private lateinit var adapter: CourseModuleProgressionAdapter
@@ -164,7 +161,7 @@ class CourseModuleProgressionFragment : ParentFragment(), Bookmarkable {
private val nextItemClickCallback = View.OnClickListener {
setupNextModuleName(currentPos)
setupNextModule(getModuleItemGroup(currentPos))
- if (currentPos < NUM_ITEMS - 1) {
+ if (currentPos < itemsCount - 1) {
viewPager.currentItem = ++currentPos
}
updateBottomNavBarButtons()
@@ -175,23 +172,20 @@ class CourseModuleProgressionFragment : ParentFragment(), Bookmarkable {
next_item.setOnClickListener(nextItemClickCallback)
markDoneButton.setOnClickListener {
- if (getModelObject() != null && getModelObject()!!.completionRequirement != null) {
- if (getModelObject()!!.completionRequirement!!.completed) {
- ModuleManager.markAsNotDone(canvasContext, getModelObject()!!.moduleId, getModelObject()!!.id,
+ val moduleItem = getModelObject()
+ if (moduleItem?.completionRequirement != null) {
+ if (moduleItem.completionRequirement!!.completed) {
+ ModuleManager.markAsNotDone(canvasContext, moduleItem.moduleId, moduleItem.id,
object : StatusCallback() {
override fun onResponse(response: Response, linkHeaders: LinkHeaders, type: ApiType) {
- markDoneCheckbox.isChecked = false
- getModelObject()!!.completionRequirement!!.completed = false
- notifyOfItemChanged(getModelObject())
+ setMarkDone(moduleItem, false)
}
})
} else {
- ModuleManager.markAsDone(canvasContext, getModelObject()!!.moduleId, getModelObject()!!.id,
+ ModuleManager.markAsDone(canvasContext, moduleItem.moduleId, moduleItem.id,
object : StatusCallback() {
override fun onResponse(response: Response, linkHeaders: LinkHeaders, type: ApiType) {
- markDoneCheckbox.isChecked = true
- getModelObject()!!.completionRequirement!!.completed = true
- notifyOfItemChanged(getModelObject())
+ setMarkDone(moduleItem, true)
}
})
}
@@ -199,6 +193,15 @@ class CourseModuleProgressionFragment : ParentFragment(), Bookmarkable {
}
}
+ private fun setMarkDone(moduleItem: ModuleItem, markDone: Boolean) {
+ val currentModuleItem = getModelObject()
+ if (isAdded && moduleItem == currentModuleItem) {
+ markDoneCheckbox.isChecked = markDone
+ }
+ moduleItem.completionRequirement?.completed = markDone
+ notifyOfItemChanged(moduleItem)
+ }
+
private fun setModuleName(name: String) {
// Set the label at the bottom
moduleName.text = name
@@ -209,7 +212,7 @@ class CourseModuleProgressionFragment : ParentFragment(), Bookmarkable {
// Figure out the total size so the adapter knows how many items it will have
var size = 0
for (i in items.indices) { size += items[i].size }
- NUM_ITEMS = size
+ itemsCount = size
currentPos = if (bundle != null && bundle.containsKey(Const.MODULE_POSITION)) {
bundle.getInt(Const.MODULE_POSITION)
@@ -308,7 +311,7 @@ class CourseModuleProgressionFragment : ParentFragment(), Bookmarkable {
prev_item.setInvisible()
}
// Don't show the next_item button if we're on the last item
- if (currentPosition >= NUM_ITEMS - 1) {
+ if (currentPosition >= itemsCount - 1) {
next_item.visibility = View.INVISIBLE
}
}
@@ -319,7 +322,7 @@ class CourseModuleProgressionFragment : ParentFragment(), Bookmarkable {
markDoneWrapper.setGone()
} else {
val completionRequirement = item.completionRequirement
- if (completionRequirement != null && ModuleItem.MUST_MARK_DONE == completionRequirement.type) {
+ if (completionRequirement != null && ModuleItem.MUST_MARK_DONE == completionRequirement.type && !item.isLocked()) {
markDoneWrapper.setVisible()
markDoneCheckbox.isChecked = completionRequirement.completed
} else {
@@ -367,7 +370,7 @@ class CourseModuleProgressionFragment : ParentFragment(), Bookmarkable {
itemsAdded++
}
}
- NUM_ITEMS += itemsAdded
+ itemsCount += itemsAdded
//only add to currentPos if we're adding to the module that is the previous module
//Without this check it will modify the index of the array while we are progressing through
@@ -588,7 +591,7 @@ class CourseModuleProgressionFragment : ParentFragment(), Bookmarkable {
}
//endregion
- fun getModelObject(): ModuleItem? = getCurrentModuleItem(currentPos)
+ private fun getModelObject(): ModuleItem? = getCurrentModuleItem(currentPos)
//region Adapter
inner class CourseModuleProgressionAdapter(fm: FragmentManager) : FragmentStatePagerAdapter(fm) {
@@ -609,7 +612,7 @@ class CourseModuleProgressionFragment : ParentFragment(), Bookmarkable {
override fun getItemPosition(`object`: Any): Int = PagerAdapter.POSITION_NONE
- override fun getCount(): Int = NUM_ITEMS
+ override fun getCount(): Int = itemsCount
override fun getItem(position: Int): Fragment {
expectingUpdate = true
@@ -650,7 +653,7 @@ class CourseModuleProgressionFragment : ParentFragment(), Bookmarkable {
}
// For RTL - this prevents the scrolling animations (ViewPager doesn't come with RTL support and default page transition animations are backwards)
- val pageTransformer = ViewPager.PageTransformer { page, position ->
+ private val pageTransformer = ViewPager.PageTransformer { page, position ->
// Page on right, position = 1
// Page on left, position = -1
// Page on screen, position = 0
@@ -737,7 +740,6 @@ class CourseModuleProgressionFragment : ParentFragment(), Bookmarkable {
const val MODULE_ITEMS = "module_item"
const val MODULE_OBJECTS = "module_objects"
- const val MODULE_ID = "module_id"
const val MODULE_POSITION = "module_position"
const val GROUP_POSITION = "group_position"
const val CHILD_POSITION = "child_position"
diff --git a/apps/student/src/main/java/com/instructure/student/fragment/CreateDiscussionFragment.kt b/apps/student/src/main/java/com/instructure/student/fragment/CreateDiscussionFragment.kt
index bb9e1277e5..3239aefa6e 100644
--- a/apps/student/src/main/java/com/instructure/student/fragment/CreateDiscussionFragment.kt
+++ b/apps/student/src/main/java/com/instructure/student/fragment/CreateDiscussionFragment.kt
@@ -46,8 +46,10 @@ import com.instructure.student.view.AssignmentOverrideView
import kotlinx.android.synthetic.main.fragment_create_discussion.*
import kotlinx.coroutines.Job
import okhttp3.MediaType
+import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.MultipartBody
import okhttp3.RequestBody
+import okhttp3.RequestBody.Companion.asRequestBody
import java.io.File
import java.util.*
@@ -318,7 +320,7 @@ class CreateDiscussionFragment : ParentFragment() {
var filePart: MultipartBody.Part? = null
attachment?.let {
val file = File(it.fullPath)
- val requestBody = RequestBody.create(MediaType.parse(it.contentType), file)
+ val requestBody = file.asRequestBody(it.contentType.toMediaTypeOrNull())
filePart = MultipartBody.Part.createFormData("attachment", file.name, requestBody)
}
awaitApi { DiscussionManager.createStudentDiscussion(canvasContext, discussionTopicHeader, filePart, it) }
diff --git a/apps/student/src/main/java/com/instructure/student/fragment/DashboardFragment.kt b/apps/student/src/main/java/com/instructure/student/fragment/DashboardFragment.kt
index ed85dd88b4..85f040797b 100644
--- a/apps/student/src/main/java/com/instructure/student/fragment/DashboardFragment.kt
+++ b/apps/student/src/main/java/com/instructure/student/fragment/DashboardFragment.kt
@@ -24,7 +24,10 @@ import android.content.IntentFilter
import android.content.res.Configuration
import android.net.Uri
import android.os.Bundle
-import android.view.*
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.browser.customtabs.CustomTabColorSchemeParams
import androidx.browser.customtabs.CustomTabsIntent
import androidx.localbroadcastmanager.content.LocalBroadcastManager
import androidx.recyclerview.widget.DefaultItemAnimator
@@ -50,6 +53,7 @@ import com.instructure.student.dialog.EditCourseNicknameDialog
import com.instructure.student.events.CoreDataFinishedLoading
import com.instructure.student.events.CourseColorOverlayToggledEvent
import com.instructure.student.events.ShowGradesToggledEvent
+import com.instructure.student.features.dashboard.edit.EditDashboardFragment
import com.instructure.student.flutterChannels.FlutterComm
import com.instructure.student.interfaces.CourseAdapterToFragmentCallback
import com.instructure.student.router.RouteMatcher
@@ -106,7 +110,7 @@ class DashboardFragment : ParentFragment() {
}
override fun onSeeAllCourses() {
- RouteMatcher.route(requireContext(), AllCoursesFragment.makeRoute())
+ RouteMatcher.route(requireContext(), EditDashboardFragment.makeRoute())
}
override fun onRemoveAnnouncement(announcement: AccountNotification, position: Int) {
@@ -162,7 +166,6 @@ class DashboardFragment : ParentFragment() {
}
override fun applyTheme() {
- setupToolbarMenu(toolbar, R.menu.menu_favorite)
toolbar.title = title()
navigation?.attachNavigationDrawer(this, toolbar)
// Styling done in attachNavigationDrawer
@@ -234,26 +237,11 @@ class DashboardFragment : ParentFragment() {
if (!APIHelper.hasNetworkConnection()) {
toast(R.string.notAvailableOffline)
} else {
- RouteMatcher.route(requireContext(), EditFavoritesFragment.makeRoute())
+ RouteMatcher.route(requireContext(), EditDashboardFragment.makeRoute())
}
}
}
- override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
- inflater.inflate(R.menu.menu_favorite, menu)
- }
-
- override fun onOptionsItemSelected(item: MenuItem): Boolean {
- if (item.itemId == R.id.selectFavorites) {
- if (!APIHelper.hasNetworkConnection()) {
- toast(R.string.notAvailableOffline)
- return true
- }
- RouteMatcher.route(requireContext(), EditFavoritesFragment.makeRoute())
- }
- return super.onOptionsItemSelected(item)
- }
-
override fun onStart() {
super.onStart()
LocalBroadcastManager.getInstance(requireContext()).registerReceiver(somethingChangedReceiver, IntentFilter(Const.COURSE_THING_CHANGED))
@@ -298,8 +286,12 @@ class DashboardFragment : ParentFragment() {
}
}
- var intent = CustomTabsIntent.Builder()
+ val colorSchemeParams = CustomTabColorSchemeParams.Builder()
.setToolbarColor(conference.canvasContext.color)
+ .build()
+
+ var intent = CustomTabsIntent.Builder()
+ .setDefaultColorSchemeParams(colorSchemeParams)
.setShowTitle(true)
.build()
.intent
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 bddc25bc78..f586f055fd 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
@@ -17,14 +17,17 @@
package com.instructure.student.fragment
import android.annotation.SuppressLint
+import android.content.Context
import android.graphics.Color
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
+import android.view.accessibility.AccessibilityManager
import android.webkit.CookieManager
import android.webkit.JavascriptInterface
import android.webkit.WebView
+import android.widget.ScrollView
import androidx.appcompat.app.AlertDialog
import androidx.core.content.ContextCompat
import com.google.android.material.snackbar.Snackbar
@@ -60,6 +63,7 @@ import com.instructure.student.router.RouteMatcher
import com.instructure.student.util.Const
import kotlinx.android.synthetic.main.fragment_discussions_details.*
import kotlinx.coroutines.Job
+import kotlinx.coroutines.delay
import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode
@@ -124,6 +128,7 @@ class DiscussionDetailsFragment : ParentFragment(), Bookmarkable {
super.onResume()
discussionTopicHeaderWebView.onResume()
discussionRepliesWebView.onResume()
+ addAccessibilityButton()
/* TODO - Comms - 868
EventBus.getDefault().getStickyEvent(DiscussionTopicHeaderDeletedEvent::class.java)?.once(javaClass.simpleName + ".onResume()") {
@@ -195,9 +200,9 @@ class DiscussionDetailsFragment : ParentFragment(), Bookmarkable {
// Show lock message if file is locked
if (remoteFile.lockedForUser) {
if (remoteFile.lockExplanation.isValid()) {
- Snackbar.make(view!!, remoteFile.lockExplanation!!, Snackbar.LENGTH_SHORT).show()
+ Snackbar.make(requireView(), remoteFile.lockExplanation!!, Snackbar.LENGTH_SHORT).show()
} else {
- Snackbar.make(view!!, R.string.fileCurrentlyLocked, Snackbar.LENGTH_SHORT).show()
+ Snackbar.make(requireView(), R.string.fileCurrentlyLocked, Snackbar.LENGTH_SHORT).show()
}
}
@@ -529,7 +534,7 @@ class DiscussionDetailsFragment : ParentFragment(), Bookmarkable {
discussionRepliesWebView.loadDataWithBaseURL(CanvasWebView.getReferrer(true), html, "text/html", "UTF-8", null)
}
- private fun populateDiscussionData(forceRefresh: Boolean = false) {
+ private fun populateDiscussionData(forceRefresh: Boolean = false, topLevelReplyPosted: Boolean = false) {
discussionsLoadingJob = tryWeave {
discussionProgressBar.setVisible()
discussionRepliesWebView.loadHtml("", "")
@@ -558,6 +563,7 @@ class DiscussionDetailsFragment : ParentFragment(), Bookmarkable {
determinePermissions()
loadDiscussionTopicHeaderViews(discussionTopicHeader)
+ addAccessibilityButton()
if (forceRefresh || discussionTopic == null) {
// forceRefresh is true, fetch the discussion topic
@@ -583,7 +589,15 @@ class DiscussionDetailsFragment : ParentFragment(), Bookmarkable {
}
loadDiscussionTopicViews(html)
- discussionsScrollView.post { discussionsScrollView?.scrollTo(0, scrollPosition) }
+
+ delay(300)
+ discussionsScrollView.post {
+ if (topLevelReplyPosted) {
+ discussionsScrollView?.fullScroll(ScrollView.FOCUS_DOWN)
+ } else {
+ discussionsScrollView?.scrollTo(0, scrollPosition)
+ }
+ }
}
} catch {
Logger.e("Error loading discussion topic " + it.message)
@@ -732,6 +746,16 @@ class DiscussionDetailsFragment : ParentFragment(), Bookmarkable {
dueDateTextView.text = DateHelper.getMonthDayAtTime(requireContext(), it, atSeparator)
}
}
+
+ private fun addAccessibilityButton() {
+ if (isAccessibilityEnabled(requireContext()) && discussionTopicHeader.htmlUrl != null) {
+ alternateViewButton.visibility = View.VISIBLE
+ alternateViewButton.setOnClickListener {
+ RouteMatcher.route(requireActivity(), InternalWebviewFragment.makeRoute(canvasContext, discussionTopicHeader.htmlUrl!!, authenticate = true, shouldRouteInternally = false, allowRoutingTheSameUrlInternally = false, isUnsupportedFeature = false, allowUnsupportedRouting = false))
+ }
+ }
+ }
+
//endregion Functionality
// region Bus Events
@@ -739,7 +763,7 @@ class DiscussionDetailsFragment : ParentFragment(), Bookmarkable {
@Subscribe(threadMode = ThreadMode.MAIN, sticky = true)
fun onDiscussionReplyCreated(event: DiscussionEntryEvent) {
event.once(discussionTopicHeader.id.toString()) {
- populateDiscussionData(true)
+ populateDiscussionData(true, event.topLevelReplyPosted)
discussionTopicHeader.incrementDiscussionSubentryCount() // Update subentry count
discussionTopicHeader.lastReplyDate?.time = Date().time // Update last post time
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 8681125c63..0964b88508 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
@@ -167,7 +167,7 @@ class DiscussionsReplyFragment : ParentFragment() {
postDiscussionJob = tryWeave {
if (attachment == null) {
if (discussionEntryId == discussionTopicHeaderId) {
- messageSentResponse(awaitApiResponse { DiscussionManager.postToDiscussionTopic(canvasContext, discussionTopicHeaderId, message!!, it) })
+ messageSentResponse(awaitApiResponse { DiscussionManager.postToDiscussionTopic(canvasContext, discussionTopicHeaderId, message!!, it) }, topLevel = true)
} else {
messageSentResponse(awaitApiResponse { DiscussionManager.replyToDiscussionEntry(canvasContext, discussionTopicHeaderId, discussionEntryId, message!!, it) })
}
@@ -176,7 +176,7 @@ class DiscussionsReplyFragment : ParentFragment() {
messageSentResponse(awaitApiResponse {
DiscussionManager.postToDiscussionTopic(canvasContext, discussionTopicHeaderId, message!!, File(attachment!!.fullPath), attachment?.contentType
?: "multipart/form-data", it)
- })
+ }, topLevel = true)
} else {
messageSentResponse(awaitApiResponse {
DiscussionManager.replyToDiscussionEntry(canvasContext, discussionTopicHeaderId, discussionEntryId, message!!, File(attachment!!.fullPath), attachment?.contentType
@@ -189,7 +189,7 @@ class DiscussionsReplyFragment : ParentFragment() {
}
}
- private fun messageSentResponse(response: Response) {
+ private fun messageSentResponse(response: Response, topLevel: Boolean = false) {
if (response.code() in 200..299 && response.body() != null) {
val discussionEntry = response.body()
@@ -205,7 +205,7 @@ class DiscussionsReplyFragment : ParentFragment() {
// Post successful
DiscussionCaching(discussionTopicHeaderId).saveEntry(discussionEntry) // Save to cache
- DiscussionEntryEvent(discussionEntry!!.id).postSticky() // Notify about new reply
+ DiscussionEntryEvent(discussionEntry!!.id, topLevel).postSticky() // Notify about new reply
toast(R.string.utils_discussionSentSuccess)
activity?.onBackPressed()
} else {
diff --git a/apps/student/src/main/java/com/instructure/student/fragment/EditFavoritesFragment.kt b/apps/student/src/main/java/com/instructure/student/fragment/EditFavoritesFragment.kt
deleted file mode 100644
index 9f71169057..0000000000
--- a/apps/student/src/main/java/com/instructure/student/fragment/EditFavoritesFragment.kt
+++ /dev/null
@@ -1,180 +0,0 @@
-/*
- * Copyright (C) 2017 - present Instructure, Inc.
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, version 3 of the License.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
- *
- */
-package com.instructure.student.fragment
-
-import android.content.Intent
-import android.content.res.Configuration
-import android.graphics.Color
-import android.graphics.drawable.ColorDrawable
-import android.os.Bundle
-import android.view.LayoutInflater
-import android.view.View
-import android.view.ViewGroup
-import androidx.localbroadcastmanager.content.LocalBroadcastManager
-import com.instructure.canvasapi2.managers.CourseManager
-import com.instructure.canvasapi2.managers.GroupManager
-import com.instructure.canvasapi2.models.CanvasComparable
-import com.instructure.canvasapi2.models.Course
-import com.instructure.canvasapi2.models.Favorite
-import com.instructure.canvasapi2.models.Group
-import com.instructure.canvasapi2.utils.pageview.PageView
-import com.instructure.canvasapi2.utils.weave.WeaveJob
-import com.instructure.canvasapi2.utils.weave.awaitApi
-import com.instructure.canvasapi2.utils.weave.catch
-import com.instructure.canvasapi2.utils.weave.tryWeave
-import com.instructure.interactions.router.Route
-import com.instructure.pandautils.utils.*
-import com.instructure.student.R
-import com.instructure.student.adapter.EditFavoritesRecyclerAdapter
-import com.instructure.student.interfaces.AdapterToFragmentCallback
-import kotlinx.android.synthetic.main.fragment_favoriting.*
-import kotlinx.android.synthetic.main.panda_recycler_refresh_layout.*
-
-@PageView(url = "courses")
-class EditFavoritesFragment : ParentFragment() {
-
- private var recyclerAdapter: EditFavoritesRecyclerAdapter? = null
- private var hasChanges = false
- private var courseCall: WeaveJob? = null
- private var groupCall: WeaveJob? = null
-
- override fun title(): String = requireContext().getString(R.string.editDashboard)
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- if (isTablet) setStyle(STYLE_NORMAL, R.style.LightStatusBarDialog)
- }
-
- override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? =
- layoutInflater.inflate(R.layout.fragment_favoriting, container, false)
-
- override fun onActivityCreated(savedInstanceState: Bundle?) {
- super.onActivityCreated(savedInstanceState)
- applyTheme()
- recyclerAdapter = EditFavoritesRecyclerAdapter(requireActivity(), object : AdapterToFragmentCallback> {
- override fun onRefreshFinished() {
- setRefreshing(false)
- if (recyclerAdapter?.size() == 0) {
- setEmptyView(emptyView, R.drawable.ic_panda_nocourses, R.string.noCourses, R.string.noCoursesSubtext)
- }
- }
-
- override fun onRowClicked(canvasContext: CanvasComparable<*>, position: Int, isOpenDetail: Boolean) {
- hasChanges = true
- when(canvasContext) {
- is Course -> updateCourseFavorite(canvasContext)
- is Group -> updateGroupFavorite(canvasContext)
- }
- }
- })
- configureRecyclerView(view!!, requireContext(), recyclerAdapter!!, R.id.swipeRefreshLayout, R.id.emptyView, R.id.listView, R.string.no_courses_available)
- listView.isSelectionEnabled = false
-
- // Disable item animator when TalkBack is enabled, otherwise a11y focus resets when a favorite is toggled
- if (requireContext().a11yManager.hasSpokenFeedback) listView.itemAnimator = null
- }
-
- override fun onConfigurationChanged(newConfig: Configuration) {
- super.onConfigurationChanged(newConfig)
- if (recyclerAdapter!!.size() == 0) {
- emptyView.changeTextSize()
- if (resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT) {
- if (isTablet) {
- emptyView.setGuidelines(.24f, .53f, .62f, .12f, .88f)
- } else {
- emptyView.setGuidelines(.28f, .6f, .73f, .12f, .88f)
-
- }
- } else {
- if (isTablet) {
- //change nothing, at least for now
- } else {
- emptyView.setGuidelines(.25f, .7f, .74f, .15f, .85f)
- }
- }
- }
- }
-
- override fun applyTheme() {
- toolbar.setTitle(R.string.editDashboard)
- toolbar.setupAsBackButton(this)
- ViewStyler.themeToolbar(requireActivity(), toolbar, Color.WHITE, Color.BLACK, false)
- }
-
- private fun updateCourseFavorite(course: Course) {
- courseCall?.cancel()
- course.isFavorite = !course.isFavorite
- recyclerAdapter?.addOrUpdateItem(EditFavoritesRecyclerAdapter.ItemType.COURSE_HEADER,course)
- courseCall = tryWeave {
- awaitApi {
- if (course.isFavorite) CourseManager.addCourseToFavorites(course.id, it, true)
- else CourseManager.removeCourseFromFavorites(course.id, it, true)
- }
- } catch {
- course.isFavorite = !course.isFavorite
- recyclerAdapter?.addOrUpdateItem(EditFavoritesRecyclerAdapter.ItemType.COURSE_HEADER,course)
- }
- }
-
- private fun updateGroupFavorite(group: Group) {
- groupCall?.cancel()
- group.isFavorite = !group.isFavorite
- recyclerAdapter?.addOrUpdateItem(EditFavoritesRecyclerAdapter.ItemType.GROUP_HEADER,group)
- groupCall = tryWeave {
- awaitApi {
- if (group.isFavorite) GroupManager.addGroupToFavorites(group.id, it)
- else GroupManager.removeGroupFromFavorites(group.id, it)
- }
- } catch {
- group.isFavorite = !group.isFavorite
- recyclerAdapter?.addOrUpdateItem(EditFavoritesRecyclerAdapter.ItemType.GROUP_HEADER,group)
- }
- }
-
- override fun onStart() {
- super.onStart()
- if (!isTablet && dialog != null) {
- dialog?.window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
- dialog?.window?.setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)
- }
- }
-
- override fun onStop() {
- super.onStop()
- courseCall?.cancel()
- groupCall?.cancel()
- recyclerAdapter?.cancel()
- if (hasChanges) {
- val intent = Intent(Const.COURSE_THING_CHANGED)
- intent.putExtras(Bundle().apply { putBoolean(Const.COURSE_FAVORITES, true) })
- LocalBroadcastManager.getInstance(requireContext()).sendBroadcast(intent)
- }
- }
-
- companion object {
-
- fun makeRoute() = Route(EditFavoritesFragment::class.java, null)
-
- fun validRoute(route: Route) = route.primaryClass == EditFavoritesFragment::class.java
-
- fun newInstance(route: Route): EditFavoritesFragment? {
- if (!validRoute(route)) return null
- return EditFavoritesFragment()
- }
-
- }
-}
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 149574e101..98e43fdfd1 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
@@ -450,6 +450,8 @@ class FileListFragment : ParentFragment(), Bookmarkable {
private fun animateFabs() = if (mFabOpen) {
addFab.startAnimation(fabRotateBackwards)
+ addFab.announceForAccessibility(getString(R.string.a11y_create_file_folder_gone))
+ addFab.contentDescription = getString(R.string.createFileFolderFabContentDesc)
addFolderFab.startAnimation(fabHide)
addFolderFab.isClickable = false
@@ -462,6 +464,8 @@ class FileListFragment : ParentFragment(), Bookmarkable {
mFabOpen = false
} else {
addFab.startAnimation(fabRotateForward)
+ addFab.announceForAccessibility(getString(R.string.a11y_create_file_folder_visible))
+ addFab.contentDescription = getString(R.string.hideCreateFileFolderFabContentDesc)
addFolderFab.apply {
startAnimation(fabReveal)
isClickable = true
diff --git a/apps/student/src/main/java/com/instructure/student/fragment/FlutterCalendarFragment.kt b/apps/student/src/main/java/com/instructure/student/fragment/FlutterCalendarFragment.kt
index 24256f398c..f6953342f8 100644
--- a/apps/student/src/main/java/com/instructure/student/fragment/FlutterCalendarFragment.kt
+++ b/apps/student/src/main/java/com/instructure/student/fragment/FlutterCalendarFragment.kt
@@ -24,10 +24,12 @@ import com.google.gson.Gson
import com.instructure.canvasapi2.models.PlannerItem
import com.instructure.student.flutterChannels.FlutterComm
import com.instructure.student.util.AppManager
+import com.instructure.student.util.BaseAppManager
import io.flutter.embedding.android.FlutterFragment
import io.flutter.embedding.android.FlutterView
import io.flutter.embedding.android.RenderMode
import io.flutter.embedding.engine.FlutterEngine
+import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import java.util.*
@@ -35,7 +37,7 @@ class FlutterCalendarFragment : FlutterFragment() {
var calendarScreenChannel = CalendarScreenChannel()
var hidden: Boolean = false
- override fun provideFlutterEngine(context: Context): FlutterEngine? = AppManager.flutterEngine
+ override fun provideFlutterEngine(context: Context): FlutterEngine? = BaseAppManager.flutterEngine
// Use texture mode instead of surface mode so the FlutterView doesn't render on top of the nav drawer and a11y borders
override fun getRenderMode() = RenderMode.texture
@@ -87,7 +89,7 @@ class FlutterCalendarFragment : FlutterFragment() {
return flutterViewField.get(delegate) as FlutterView
}
- if (resuming) getFlutterView().attachToFlutterEngine(AppManager.flutterEngine)
+ if (resuming) getFlutterView().attachToFlutterEngine(BaseAppManager.flutterEngine)
val lifecycle1 = delegate::class.java.getDeclaredMethod(if (resuming) "onStart" else "onPause")
lifecycle1.isAccessible = true
@@ -107,7 +109,9 @@ class FlutterCalendarFragment : FlutterFragment() {
// Perform onBackPressed on the FlutterFragment, which will attempt to pop the current route and update
// the 'shouldPop' value for future use.
- onBackPressed()
+ if (!shouldPop) {
+ onBackPressed()
+ }
// If 'shouldPop' was true it means we just popped a CalendarScreen in Flutter and that we should also
// allow this fragment to be popped
@@ -122,20 +126,23 @@ class FlutterCalendarFragment : FlutterFragment() {
class CalendarScreenChannel {
val channelId: String = UUID.randomUUID().toString()
- private val channel = MethodChannel(AppManager.flutterEngine.dartExecutor.binaryMessenger, channelId)
+ private val channel = MethodChannel(BaseAppManager.flutterEngine.dartExecutor.binaryMessenger, channelId)
var onRouteToItem: ((item: PlannerItem) -> Unit)? = null
var onOpenDrawer: (() -> Unit)? = null
+ var onShowDialog: ((call: MethodCall, result: MethodChannel.Result) -> Unit)? = null
+
init {
- channel.setMethodCallHandler { call, _ ->
+ channel.setMethodCallHandler { call: MethodCall, result: MethodChannel.Result ->
when (call.method) {
"openDrawer" -> onOpenDrawer?.invoke()
"routeToItem" -> {
val item = Gson().fromJson(call.arguments as String, PlannerItem::class.java)
onRouteToItem?.invoke(item)
}
+ "showDialog" -> onShowDialog?.invoke(call, result)
}
}
}
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 bb486b6824..515e5660a4 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
@@ -60,6 +60,7 @@ class InboxComposeMessageFragment : ParentFragment() {
private val includedMessageIds by LongArrayArg(key = Const.MESSAGE)
private val isReply by BooleanArg(key = IS_REPLY)
private val currentMessage by NullableParcelableArg(key = Const.MESSAGE_TO_USER)
+ private val homeroomMessage by BooleanArg(key = HOMEROOM_MESSAGE)
private var selectedContext by NullableParcelableArg(key = Const.CANVAS_CONTEXT)
private val isNewMessage by lazy { conversation == null }
@@ -203,6 +204,16 @@ class InboxComposeMessageFragment : ParentFragment() {
// Get courses and groups if this is a new compose message
if (isNewMessage) getAllCoursesAndGroups()
+
+ if (homeroomMessage) {
+ hideFieldsForHomeroomMessage()
+ }
+ }
+
+ private fun hideFieldsForHomeroomMessage() {
+ spinnerWrapper.setGone()
+ recipientWrapper.setGone()
+ sendIndividualMessageWrapper.setGone()
}
private fun getAllCoursesAndGroups() {
@@ -234,6 +245,7 @@ class InboxComposeMessageFragment : ParentFragment() {
chips.clearRecipients()
selectedContext = canvasContext
courseWasSelected()
+ courseSpinner.contentDescription = getString(R.string.a11y_content_description_inbox_course_spinner, selectedContext?.name)
}
}
}
@@ -243,8 +255,10 @@ class InboxComposeMessageFragment : ParentFragment() {
}
private fun courseWasSelected() {
- recipientWrapper.visibility = View.VISIBLE
- contactsImageButton.visibility = View.VISIBLE
+ if (!homeroomMessage) {
+ recipientWrapper.visibility = View.VISIBLE
+ contactsImageButton.visibility = View.VISIBLE
+ }
requireActivity().invalidateOptionsMenu()
chips.canvasContext = selectedContext
}
@@ -354,13 +368,11 @@ class InboxComposeMessageFragment : ParentFragment() {
private fun sendMessage(selectedRecipients: List, message: String) {
- // Encode the message here, tell the api not to encode it
- val formattedMessage = URLEncoder.encode(message, "UTF-8")
sendCall = tryWeave {
val attachmentIds = attachments.map { it.id }.toLongArray()
val recipientIds = selectedRecipients.mapNotNull { it.stringId }
val conversation = awaitApi {
- InboxManager.addMessage(conversation?.id ?: 0, formattedMessage, recipientIds, includedMessageIds, attachmentIds, conversation?.contextCode, it)
+ InboxManager.addMessage(conversation?.id ?: 0, message, recipientIds, includedMessageIds, attachmentIds, conversation?.contextCode, it)
}
messageSuccess(conversation)
} catch {
@@ -371,13 +383,11 @@ class InboxComposeMessageFragment : ParentFragment() {
private fun createConversation(selectedRecipients: List, message: String, subject: String, contextId: String, isBulk: Boolean) {
sendCall?.cancel()
- val formattedMessage = URLEncoder.encode(message, "UTF-8")
- val formattedSubject = URLEncoder.encode(subject, "UTF-8")
sendCall = tryWeave {
val attachmentIds = attachments.map { it.id }.toLongArray()
val recipientIds = selectedRecipients.mapNotNull { it.stringId }
val conversation = awaitApi> {
- InboxManager.createConversation(recipientIds, formattedMessage, formattedSubject, contextId, attachmentIds, isBulk, it)
+ InboxManager.createConversation(recipientIds, message, subject, contextId, attachmentIds, isBulk, it)
}.first()
messageSuccess(conversation)
} catch { error ->
@@ -445,6 +455,7 @@ class InboxComposeMessageFragment : ParentFragment() {
private const val IS_REPLY = "is_reply"
private const val PARTICIPANTS = "participants"
+ private const val HOMEROOM_MESSAGE = "homeroom_message"
fun makeRoute(
isReply: Boolean,
@@ -467,11 +478,13 @@ class InboxComposeMessageFragment : ParentFragment() {
fun makeRoute(
canvasContext: CanvasContext,
- participants: ArrayList
+ participants: ArrayList,
+ homeroomMessage: Boolean = false
): Route {
val bundle = Bundle().apply {
putParcelableArrayList(PARTICIPANTS, participants)
putParcelable(Const.CANVAS_CONTEXT, canvasContext)
+ putBoolean(HOMEROOM_MESSAGE, homeroomMessage)
}
return Route(InboxComposeMessageFragment::class.java, canvasContext, bundle)
}
diff --git a/apps/student/src/main/java/com/instructure/student/fragment/LtiLaunchFragment.kt b/apps/student/src/main/java/com/instructure/student/fragment/LtiLaunchFragment.kt
index 5f88d962ba..e5d6eac0dd 100644
--- a/apps/student/src/main/java/com/instructure/student/fragment/LtiLaunchFragment.kt
+++ b/apps/student/src/main/java/com/instructure/student/fragment/LtiLaunchFragment.kt
@@ -23,6 +23,7 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
+import androidx.browser.customtabs.CustomTabColorSchemeParams
import androidx.browser.customtabs.CustomTabsIntent
import com.instructure.canvasapi2.managers.AssignmentManager
import com.instructure.canvasapi2.managers.SubmissionManager
@@ -125,8 +126,12 @@ class LtiLaunchFragment : ParentFragment() {
.appendQueryParameter("platform", "android")
.build()
- var intent = CustomTabsIntent.Builder()
+ val colorSchemeParams = CustomTabColorSchemeParams.Builder()
.setToolbarColor(canvasContext.color)
+ .build()
+
+ var intent = CustomTabsIntent.Builder()
+ .setDefaultColorSchemeParams(colorSchemeParams)
.setShowTitle(true)
.build()
.intent
diff --git a/apps/student/src/main/java/com/instructure/student/fragment/ModuleListFragment.kt b/apps/student/src/main/java/com/instructure/student/fragment/ModuleListFragment.kt
index 9644a1936d..387ab06584 100644
--- a/apps/student/src/main/java/com/instructure/student/fragment/ModuleListFragment.kt
+++ b/apps/student/src/main/java/com/instructure/student/fragment/ModuleListFragment.kt
@@ -129,7 +129,9 @@ class ModuleListFragment : ParentFragment(), Bookmarkable {
override fun getSelectedParamName(): String = RouterParams.MODULE_ID
fun setupViews() {
- recyclerAdapter = ModuleListRecyclerAdapter(canvasContext, requireContext(), object : ModuleAdapterToFragmentCallback {
+ val navigatingToSpecificModule = !arguments?.getString(MODULE_ID).isNullOrEmpty()
+
+ recyclerAdapter = ModuleListRecyclerAdapter(canvasContext, requireContext(), navigatingToSpecificModule, object : ModuleAdapterToFragmentCallback {
override fun onRowClicked(moduleObject: ModuleObject, moduleItem: ModuleItem, position: Int, isOpenDetail: Boolean) {
if (moduleItem.type != null && moduleItem.type == ModuleObject.State.UnlockRequirements.apiString) return
@@ -162,16 +164,17 @@ class ModuleListFragment : ParentFragment(), Bookmarkable {
} else if (recyclerAdapter.size() == 0) {
setEmptyView(emptyView, R.drawable.ic_panda_nomodules, R.string.noModules, R.string.noModulesSubtext)
} else if (!arguments?.getString(MODULE_ID).isNullOrEmpty()) {
- val groupPosition = recyclerAdapter.getGroupItemPosition(arguments!!.getString(MODULE_ID)!!.toLong())
- if (groupPosition >= 0) {
- // We need to delay scrolling until the expand animation has completed, otherwise modules
- // that appear near the end of the list will not have the extra 'expanded' space needed
- // to scroll as far as possible toward the top
- listView?.postDelayed({
+ // We need to delay scrolling until the expand animation has completed, otherwise modules
+ // that appear near the end of the list will not have the extra 'expanded' space needed
+ // to scroll as far as possible toward the top
+ listView?.postDelayed({
+ val groupPosition = recyclerAdapter.getGroupItemPosition(arguments!!.getString(MODULE_ID)!!.toLong())
+ if (groupPosition >= 0) {
val lm = listView?.layoutManager as? LinearLayoutManager
lm?.scrollToPositionWithOffset(groupPosition, 0)
- }, 1000)
- }
+ arguments?.remove(MODULE_ID)
+ }
+ }, 1000)
}
}
})
diff --git a/apps/student/src/main/java/com/instructure/student/fragment/NotificationListFragment.kt b/apps/student/src/main/java/com/instructure/student/fragment/NotificationListFragment.kt
index 096df4382f..93aac69450 100644
--- a/apps/student/src/main/java/com/instructure/student/fragment/NotificationListFragment.kt
+++ b/apps/student/src/main/java/com/instructure/student/fragment/NotificationListFragment.kt
@@ -234,7 +234,7 @@ class NotificationListFragment : ParentFragment(), Bookmarkable {
}
COLLABORATION -> UnsupportedTabFragment.makeRoute(canvasContext, Tab.COLLABORATIONS_ID)
CONFERENCE -> ConferenceListFragment.makeRoute(canvasContext)
- else -> UnsupportedFeatureFragment.makeRoute(canvasContext, streamItem.type, streamItem.url ?: streamItem.htmlUrl)
+ else -> UnsupportedFeatureFragment.makeRoute(canvasContext, featureName = streamItem.type, url = streamItem.url ?: streamItem.htmlUrl)
}
if (route != null) RouteMatcher.route(context, route)
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 25b2501649..321c79d4d3 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
@@ -67,7 +67,7 @@ import com.instructure.student.util.FileUtils
import com.instructure.student.util.LoggingUtility
import com.instructure.student.util.StudentPrefs
import com.instructure.student.util.onMainThread
-import com.instructure.student.view.EmptyView
+import com.instructure.pandautils.views.EmptyView
import java.io.File
import java.io.FileOutputStream
@@ -253,7 +253,9 @@ abstract class ParentFragment : DialogFragment(), FragmentInteractions {
}
private fun addBookmarkMenuIfAllowed(toolbar: Toolbar) {
- if (this is Bookmarkable && this.bookmark.canBookmark && toolbar.menu.findItem(R.id.bookmark) == null) {
+ val navigation = activity as? Navigation
+ val bookmarkFeatureAllowed = navigation?.canBookmark() ?: false
+ if (bookmarkFeatureAllowed && this is Bookmarkable && this.bookmark.canBookmark && toolbar.menu.findItem(R.id.bookmark) == null) {
toolbar.inflateMenu(R.menu.bookmark_menu)
}
}
diff --git a/apps/student/src/main/java/com/instructure/student/fragment/PeopleDetailsFragment.kt b/apps/student/src/main/java/com/instructure/student/fragment/PeopleDetailsFragment.kt
index c4a9ad9cf8..0d1bff38ca 100644
--- a/apps/student/src/main/java/com/instructure/student/fragment/PeopleDetailsFragment.kt
+++ b/apps/student/src/main/java/com/instructure/student/fragment/PeopleDetailsFragment.kt
@@ -17,6 +17,7 @@
package com.instructure.student.fragment
+import android.content.res.ColorStateList
import android.graphics.Color
import android.os.Bundle
import android.view.LayoutInflater
@@ -69,10 +70,8 @@ class PeopleDetailsFragment : ParentFragment(), Bookmarkable {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val color = ColorKeeper.getOrGenerateColor(canvasContext)
- compose.colorNormal = color
- compose.colorPressed = color
-
- compose.setIconDrawable(ColorKeeper.getColoredDrawable(requireContext(), R.drawable.ic_send, Color.WHITE))
+ compose.backgroundTintList = ColorStateList.valueOf(color)
+ compose.setImageDrawable(ColorKeeper.getColoredDrawable(requireContext(), R.drawable.ic_send, Color.WHITE))
compose.setOnClickListener {
// Messaging other users is not available in Student view
val route = if (ApiPrefs.isStudentView) NothingToSeeHereFragment.makeRoute() else {
diff --git a/apps/student/src/main/java/com/instructure/student/fragment/UnsupportedFeatureFragment.kt b/apps/student/src/main/java/com/instructure/student/fragment/UnsupportedFeatureFragment.kt
index c8c93f165d..a32fddca96 100644
--- a/apps/student/src/main/java/com/instructure/student/fragment/UnsupportedFeatureFragment.kt
+++ b/apps/student/src/main/java/com/instructure/student/fragment/UnsupportedFeatureFragment.kt
@@ -32,12 +32,13 @@ open class UnsupportedFeatureFragment : ParentFragment() {
private var canvasContext: CanvasContext by ParcelableArg(key = Const.CANVAS_CONTEXT)
private var featureName by NullableStringArg(key = Const.FEATURE_NAME)
+ private var unsupportedDescription by NullableStringArg(key = Const.UNSUPPORTED_DESCRIPTION)
private var url by NullableStringArg(key = Const.URL)
override fun title(): String = getString(R.string.unsupported)
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
- return inflater?.inflate(R.layout.fragment_unsupported_feature, container, false)
+ return inflater.inflate(R.layout.fragment_unsupported_feature, container, false)
}
override fun applyTheme() {
@@ -49,10 +50,14 @@ open class UnsupportedFeatureFragment : ParentFragment() {
private fun initViews() {
// Set the text
- if (featureName != null) {
- featureText.text = String.format(getString(R.string.isNotSupportedFeature), featureName)
- } else {
- featureText.text = getString(R.string.isNotSupported)
+ when {
+ unsupportedDescription != null -> featureText.text = unsupportedDescription
+ featureName != null -> featureText.text = String.format(getString(R.string.isNotSupportedFeature), featureName)
+ else -> featureText.text = getString(R.string.isNotSupported)
+ }
+
+ if (url.isNullOrEmpty()) {
+ openInBrowser.setGone()
}
openInBrowser.setOnClickListener {
@@ -72,10 +77,11 @@ open class UnsupportedFeatureFragment : ParentFragment() {
companion object {
- fun makeRoute(canvasContext: CanvasContext, title: String, url: String? = null): Route {
+ fun makeRoute(canvasContext: CanvasContext, featureName: String? = null, unsupportedDescription: String? = null, url: String? = null): Route {
val bundle = Bundle().apply {
putParcelable(Const.CANVAS_CONTEXT, canvasContext)
- putString(Const.FEATURE_NAME, title)
+ putString(Const.FEATURE_NAME, featureName)
+ putString(Const.UNSUPPORTED_DESCRIPTION, unsupportedDescription)
putString(Const.URL, url)
}
return Route(UnsupportedFeatureFragment::class.java, canvasContext, bundle)
diff --git a/apps/student/src/main/java/com/instructure/student/holders/CourseHeaderViewHolder.kt b/apps/student/src/main/java/com/instructure/student/holders/CourseHeaderViewHolder.kt
index de90c90428..eed5c7a164 100644
--- a/apps/student/src/main/java/com/instructure/student/holders/CourseHeaderViewHolder.kt
+++ b/apps/student/src/main/java/com/instructure/student/holders/CourseHeaderViewHolder.kt
@@ -32,8 +32,8 @@ class CourseHeaderViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView)
}
fun bind(callback: CourseAdapterToFragmentCallback) = with(itemView) {
- seeAllTextView.setTextColor(ThemePrefs.buttonColor)
- seeAllTextView.onClick { callback.onSeeAllCourses() }
+ editDashboardTextView.setTextColor(ThemePrefs.buttonColor)
+ editDashboardTextView.onClick { callback.onSeeAllCourses() }
}
}
diff --git a/apps/student/src/main/java/com/instructure/student/interfaces/AdapterToAssignmentsCallback.kt b/apps/student/src/main/java/com/instructure/student/interfaces/AdapterToAssignmentsCallback.kt
index 5e0fa80110..4f93b14c0d 100644
--- a/apps/student/src/main/java/com/instructure/student/interfaces/AdapterToAssignmentsCallback.kt
+++ b/apps/student/src/main/java/com/instructure/student/interfaces/AdapterToAssignmentsCallback.kt
@@ -20,6 +20,6 @@ import com.instructure.canvasapi2.models.Assignment
import com.instructure.canvasapi2.models.GradingPeriod
interface AdapterToAssignmentsCallback : AdapterToFragmentCallback {
- fun setTermSpinnerState(isEnabled: Boolean)
+ fun assignmentLoadingFinished()
fun gradingPeriodsFetched(periods: List)
}
diff --git a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/AssignmentDetailsEffectHandler.kt b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/AssignmentDetailsEffectHandler.kt
index 846e938216..f92d6fcbc5 100644
--- a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/AssignmentDetailsEffectHandler.kt
+++ b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/AssignmentDetailsEffectHandler.kt
@@ -129,6 +129,7 @@ class AssignmentDetailsEffectHandler(val context: Context, val assignmentId: Lon
Assignment.SubmissionType.ONLINE_UPLOAD -> view?.showFileUploadView(effect.assignment)
Assignment.SubmissionType.ONLINE_TEXT_ENTRY -> view?.showOnlineTextEntryView(effect.assignment.id, effect.assignment.name)
Assignment.SubmissionType.ONLINE_URL -> view?.showOnlineUrlEntryView(effect.assignment.id, effect.assignment.name, effect.course)
+ Assignment.SubmissionType.STUDENT_ANNOTATION -> view?.showStudentAnnotationView(effect.assignment.htmlUrl ?: "")
Assignment.SubmissionType.EXTERNAL_TOOL, Assignment.SubmissionType.BASIC_LTI_LAUNCH -> view?.showLTIView(effect.course, effect.assignment.name ?: "", effect.ltiTool)
else -> view?.showMediaRecordingView(effect.assignment) // Assignment.SubmissionType.MEDIA_RECORDING
}
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 12666d8646..9cba37d395 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
@@ -17,6 +17,7 @@
package com.instructure.student.mobius.assignmentDetails
import android.content.Context
+import android.view.accessibility.AccessibilityManager
import androidx.core.content.ContextCompat
import com.instructure.canvasapi2.models.*
import com.instructure.canvasapi2.utils.*
@@ -178,7 +179,7 @@ object AssignmentDetailsPresenter : Presenter visibilities.textEntry = true
Assignment.SubmissionType.ONLINE_URL -> visibilities.urlEntry = true
Assignment.SubmissionType.MEDIA_RECORDING -> visibilities.mediaRecording = true
+ Assignment.SubmissionType.STUDENT_ANNOTATION -> visibilities.studentAnnotation = true
}
}
diff --git a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submission/picker/ui/PickerSubmissionUploadView.kt b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submission/picker/ui/PickerSubmissionUploadView.kt
index 3632692565..e27531ffa5 100644
--- a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submission/picker/ui/PickerSubmissionUploadView.kt
+++ b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submission/picker/ui/PickerSubmissionUploadView.kt
@@ -102,9 +102,9 @@ class PickerSubmissionUploadView(inflater: LayoutInflater, parent: ViewGroup, va
private fun renderEmpty() {
pickerEmptyView.setVisible(true)
- pickerEmptyView.setListEmpty()
pickerEmptyView.setImageVisible(true)
pickerEmptyView.setEmptyViewImage(context.getDrawableCompat(R.drawable.ic_panda_choosefile))
+ pickerEmptyView.setListEmpty()
pickerEmptyView.setTitleText(R.string.chooseFile)
pickerEmptyView.setMessageText(
if (mode.isForComment) R.string.chooseFileForCommentSubtext else R.string.chooseFileSubtext
diff --git a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/SubmissionDetailsEffectHandler.kt b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/SubmissionDetailsEffectHandler.kt
index c9a5a8849c..084d8b9c8d 100644
--- a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/SubmissionDetailsEffectHandler.kt
+++ b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/SubmissionDetailsEffectHandler.kt
@@ -22,20 +22,19 @@ import com.instructure.canvasapi2.managers.EnrollmentManager
import com.instructure.canvasapi2.managers.QuizManager
import com.instructure.canvasapi2.managers.SubmissionManager
import com.instructure.canvasapi2.models.Assignment
-import com.instructure.canvasapi2.models.Enrollment
import com.instructure.canvasapi2.models.LTITool
import com.instructure.canvasapi2.utils.ApiPrefs
import com.instructure.canvasapi2.utils.DataResult
import com.instructure.canvasapi2.utils.Failure
import com.instructure.canvasapi2.utils.exhaustive
import com.instructure.canvasapi2.utils.weave.StatusCallbackError
-import com.instructure.canvasapi2.utils.weave.awaitApi
import com.instructure.student.mobius.assignmentDetails.submissionDetails.drawer.comments.SubmissionCommentsSharedEvent
import com.instructure.student.mobius.assignmentDetails.submissionDetails.ui.SubmissionDetailsView
import com.instructure.student.mobius.common.ChannelSource
import com.instructure.student.mobius.common.ui.EffectHandler
import com.instructure.student.util.getStudioLTITool
import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.ObsoleteCoroutinesApi
import kotlinx.coroutines.launch
import java.io.File
@@ -126,17 +125,17 @@ class SubmissionDetailsEffectHandler : EffectHandler().offer(
- SubmissionCommentsSharedEvent.SendMediaCommentClicked(file)
+ ChannelSource.getChannel().trySend(
+ SubmissionCommentsSharedEvent.SendMediaCommentClicked(file)
)
}
- @ExperimentalCoroutinesApi
+ @ObsoleteCoroutinesApi
private fun mediaDialogClosed() {
- ChannelSource.getChannel().offer(
- SubmissionCommentsSharedEvent.MediaCommentDialogClosed
+ ChannelSource.getChannel().trySend(
+ SubmissionCommentsSharedEvent.MediaCommentDialogClosed
)
}
}
diff --git a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/SubmissionDetailsModels.kt b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/SubmissionDetailsModels.kt
index 51899c51c3..9bfdb00661 100644
--- a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/SubmissionDetailsModels.kt
+++ b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/SubmissionDetailsModels.kt
@@ -89,4 +89,5 @@ sealed class SubmissionDetailsContentType {
data class UrlContent(val url: String, val previewUrl: String?) : SubmissionDetailsContentType()
data class DiscussionContent(val previewUrl: String?) : SubmissionDetailsContentType()
object LockedContent : SubmissionDetailsContentType()
+ object StudentAnnotationContent : SubmissionDetailsContentType()
}
diff --git a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/SubmissionDetailsUpdate.kt b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/SubmissionDetailsUpdate.kt
index 06569b44df..1fb6195f2b 100644
--- a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/SubmissionDetailsUpdate.kt
+++ b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/SubmissionDetailsUpdate.kt
@@ -199,6 +199,8 @@ class SubmissionDetailsUpdate : UpdateInit SubmissionDetailsContentType.DiscussionContent(submission.previewUrl)
+
+ Assignment.SubmissionType.STUDENT_ANNOTATION -> SubmissionDetailsContentType.StudentAnnotationContent
else -> SubmissionDetailsContentType.UnsupportedContent(assignment?.id ?: -1)
}
}
diff --git a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/content/TextSubmissionViewFragment.kt b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/content/TextSubmissionViewFragment.kt
index 49210adc52..2fa94b20eb 100644
--- a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/content/TextSubmissionViewFragment.kt
+++ b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/content/TextSubmissionViewFragment.kt
@@ -77,13 +77,13 @@ class TextSubmissionViewFragment : Fragment() {
override fun shouldLaunchInternalWebViewFragment(url: String): Boolean = true
}
- textSubmissionWebView.loadRawHtml(formatHtml(submissionText), submissionText)
+ textSubmissionWebView.loadRawHtml(formatHtml(submissionText))
}
private fun formatHtml(src: String): String {
/* Pre-format using CanvasWebView's formatter, which will wrap the source string in an HTML body to
set the viewport and apply basic CSS properties */
- val formatted = textSubmissionWebView.formatHtml(src)
+ val formatted = textSubmissionWebView.formatHtml(src, getString(R.string.a11y_submissionText))
/* If the source content begins with a paragraph tag, the WebView automatically applies some vertical padding.
For other content, we need to apply the padding ourselves. */
diff --git a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/content/emptySubmission/SubmissionDetailsEmptyContentEffectHandler.kt b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/content/emptySubmission/SubmissionDetailsEmptyContentEffectHandler.kt
index 975ab6bba0..6ac8123bae 100644
--- a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/content/emptySubmission/SubmissionDetailsEmptyContentEffectHandler.kt
+++ b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/content/emptySubmission/SubmissionDetailsEmptyContentEffectHandler.kt
@@ -67,6 +67,7 @@ class SubmissionDetailsEmptyContentEffectHandler(val context: Context, val assig
Assignment.SubmissionType.ONLINE_UPLOAD -> view?.showFileUploadView(effect.assignment)
Assignment.SubmissionType.ONLINE_TEXT_ENTRY -> view?.showOnlineTextEntryView(effect.assignment.id, effect.assignment.name)
Assignment.SubmissionType.ONLINE_URL -> view?.showOnlineUrlEntryView(effect.assignment.id, effect.assignment.name, effect.course)
+ Assignment.SubmissionType.STUDENT_ANNOTATION -> view?.showStudentAnnotationView(effect.assignment.htmlUrl ?: "")
Assignment.SubmissionType.EXTERNAL_TOOL, Assignment.SubmissionType.BASIC_LTI_LAUNCH -> view?.showLTIView(effect.course, title = effect.assignment.name ?: "", ltiTool = effect.ltiTool)
else -> view?.showMediaRecordingView() // e.g. Assignment.SubmissionType.MEDIA_RECORDING
}
diff --git a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/content/emptySubmission/SubmissionDetailsEmptyContentPresenter.kt b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/content/emptySubmission/SubmissionDetailsEmptyContentPresenter.kt
index 454936de4f..ae75bf5dbb 100644
--- a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/content/emptySubmission/SubmissionDetailsEmptyContentPresenter.kt
+++ b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/content/emptySubmission/SubmissionDetailsEmptyContentPresenter.kt
@@ -75,7 +75,7 @@ object SubmissionDetailsEmptyContentPresenter : Presenter view?.scrollToBottom()
is SubmissionCommentsEffect.BroadcastSubmissionSelected -> {
- ChannelSource.getChannel().offer(
+ ChannelSource.getChannel().trySend(
SubmissionDetailsSharedEvent.SubmissionClicked(effect.submission)
)
Unit
}
is SubmissionCommentsEffect.BroadcastSubmissionAttachmentSelected -> {
- ChannelSource.getChannel().offer(
- SubmissionDetailsSharedEvent.SubmissionAttachmentClicked(effect.submission, effect.attachment)
+ ChannelSource.getChannel().trySend(
+ SubmissionDetailsSharedEvent.SubmissionAttachmentClicked(
+ effect.submission,
+ effect.attachment
+ )
)
Unit
}
@@ -124,14 +127,14 @@ class SubmissionCommentsEffectHandler(val context: Context) : EffectHandler().offer(
- SubmissionDetailsSharedEvent.VideoRecordingViewLaunched
+ ChannelSource.getChannel().trySend(
+ SubmissionDetailsSharedEvent.VideoRecordingViewLaunched
)
}
private fun showAudioCommentDialog() {
- ChannelSource.getChannel().offer(
- SubmissionDetailsSharedEvent.AudioRecordingViewLaunched
+ ChannelSource.getChannel().trySend(
+ SubmissionDetailsSharedEvent.AudioRecordingViewLaunched
)
}
diff --git a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/drawer/comments/ui/views/CommentMediaAttachmentView.kt b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/drawer/comments/ui/views/CommentMediaAttachmentView.kt
index 4e8eefe154..9f523d0ac2 100644
--- a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/drawer/comments/ui/views/CommentMediaAttachmentView.kt
+++ b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/drawer/comments/ui/views/CommentMediaAttachmentView.kt
@@ -46,6 +46,10 @@ class CommentMediaAttachmentView(
view.iconImageView.setImageResource(R.drawable.ic_media)
view.attachmentNameTextView.text = context.getString(R.string.mediaUploadVideo)
}
+ else -> {
+ view.iconImageView.setImageResource(R.drawable.ic_media)
+ view.attachmentNameTextView.text = context.getString(R.string.mediaUpload)
+ }
}
view.iconImageView.setColorFilter(tint)
view.onClickWithRequireNetwork { onAttachmentClicked(mediaComment.asAttachment()) }
diff --git a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/drawer/comments/ui/views/CommentSubmissionView.kt b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/drawer/comments/ui/views/CommentSubmissionView.kt
index 172a602198..bc4f379c9a 100644
--- a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/drawer/comments/ui/views/CommentSubmissionView.kt
+++ b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/drawer/comments/ui/views/CommentSubmissionView.kt
@@ -94,9 +94,10 @@ class CommentSubmissionView(
}
SubmissionType.MEDIA_RECORDING -> {
val media = submission.mediaComment ?: throw IllegalStateException("Media comment is null for media submission. WHY!?")
- val subtitle = when (media.mediaType!!) {
+ val subtitle = when (media.mediaType) {
MediaComment.MediaType.AUDIO -> context.getString(R.string.commentSubmissionTypeAudio)
MediaComment.MediaType.VIDEO -> context.getString(R.string.commentSubmissionTypeVideo)
+ else -> ""
}
Triple(R.drawable.ic_media, context.getString(R.string.commentSubmissionTypeMediaFile), subtitle)
}
diff --git a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/drawer/files/SubmissionFilesEffectHandler.kt b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/drawer/files/SubmissionFilesEffectHandler.kt
index 9aad249f8c..059a8ad5dc 100644
--- a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/drawer/files/SubmissionFilesEffectHandler.kt
+++ b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/drawer/files/SubmissionFilesEffectHandler.kt
@@ -20,14 +20,14 @@ import com.instructure.student.mobius.assignmentDetails.submissionDetails.Submis
import com.instructure.student.mobius.assignmentDetails.submissionDetails.drawer.files.ui.SubmissionFilesView
import com.instructure.student.mobius.common.ChannelSource
import com.instructure.student.mobius.common.ui.EffectHandler
-import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.ObsoleteCoroutinesApi
class SubmissionFilesEffectHandler : EffectHandler() {
- @ExperimentalCoroutinesApi
+ @ObsoleteCoroutinesApi
override fun accept(value: SubmissionFilesEffect) {
when (value) {
is SubmissionFilesEffect.BroadcastFileSelected -> {
- ChannelSource.getChannel().offer(
+ ChannelSource.getChannel().trySend(
SubmissionDetailsSharedEvent.FileSelected(value.file)
)
}
diff --git a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/drawer/rubric/SubmissionRubricPresenter.kt b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/drawer/rubric/SubmissionRubricPresenter.kt
index ba7a763975..821c2449fe 100644
--- a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/drawer/rubric/SubmissionRubricPresenter.kt
+++ b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/drawer/rubric/SubmissionRubricPresenter.kt
@@ -71,7 +71,7 @@ object SubmissionRubricPresenter : Presenter {
- submissionContent.importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_YES
+ submissionContent.importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS
}
SlidingUpPanelLayout.PanelState.EXPANDED -> {
submissionContent.importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS
@@ -287,6 +291,7 @@ class SubmissionDetailsView(
SubmissionDetailsContentType.NoneContent -> SubmissionMessageFragment.newInstance(title = R.string.noOnlineSubmissions, subtitle = R.string.noneContentMessage)
SubmissionDetailsContentType.OnPaperContent -> SubmissionMessageFragment.newInstance(title = R.string.noOnlineSubmissions, subtitle = R.string.onPaperContentMessage)
SubmissionDetailsContentType.LockedContent -> SubmissionMessageFragment.newInstance(title = R.string.submissionDetailsAssignmentLocked, subtitle = R.string.could_not_route_locked)
+ SubmissionDetailsContentType.StudentAnnotationContent -> SubmissionMessageFragment.newInstance(title = R.string.unsupportedSubmissionType, message = R.string.studentAnnotationUnsupportedMessage)
is SubmissionDetailsContentType.UnsupportedContent -> {
// Users shouldn't get here, but we'll handle the case and send up some analytics if they do
val bundle = Bundle().apply {
diff --git a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/ui/AssignmentDetailsFragment.kt b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/ui/AssignmentDetailsFragment.kt
index 748b7cc43d..c75a3e7188 100644
--- a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/ui/AssignmentDetailsFragment.kt
+++ b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/ui/AssignmentDetailsFragment.kt
@@ -16,8 +16,10 @@
*/
package com.instructure.student.mobius.assignmentDetails.ui
+import android.content.Context
import android.view.LayoutInflater
import android.view.ViewGroup
+import android.view.accessibility.AccessibilityManager
import com.instructure.canvasapi2.CanvasRestAdapter
import com.instructure.canvasapi2.models.CanvasContext
import com.instructure.canvasapi2.models.Course
@@ -56,7 +58,7 @@ class AssignmentDetailsFragment :
override fun makeUpdate() = AssignmentDetailsUpdate()
override fun makeView(inflater: LayoutInflater, parent: ViewGroup) =
- AssignmentDetailsView(canvasContext, inflater, parent)
+ AssignmentDetailsView(canvasContext, isAccessibilityEnabled(requireContext()), inflater, parent)
override fun makePresenter() = AssignmentDetailsPresenter
diff --git a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/ui/AssignmentDetailsView.kt b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/ui/AssignmentDetailsView.kt
index 463f580303..554e06d08a 100644
--- a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/ui/AssignmentDetailsView.kt
+++ b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/ui/AssignmentDetailsView.kt
@@ -28,6 +28,7 @@ import android.view.View
import android.view.ViewGroup
import android.webkit.WebView
import android.widget.Toast
+import androidx.constraintlayout.widget.ConstraintSet
import androidx.fragment.app.FragmentActivity
import com.instructure.canvasapi2.models.*
import com.instructure.canvasapi2.utils.AnalyticsEventConstants
@@ -61,9 +62,10 @@ import kotlinx.coroutines.Job
import java.net.URLDecoder
class AssignmentDetailsView(
- val canvasContext: CanvasContext,
- inflater: LayoutInflater,
- parent: ViewGroup
+ val canvasContext: CanvasContext,
+ isAccessiblityEnabled: Boolean = false,
+ inflater: LayoutInflater,
+ parent: ViewGroup
) :
MobiusView(
R.layout.fragment_assignment_details,
@@ -77,12 +79,30 @@ class AssignmentDetailsView(
toolbar.setupAsBackButton { (context as? Activity)?.onBackPressed() }
toolbar.title = context.getString(R.string.assignmentDetails)
toolbar.subtitle = canvasContext.name
- toolbar.setMenu(R.menu.bookmark_menu) { consumer?.accept(AssignmentDetailsEvent.AddBookmarkClicked) }
+
+ val navigation = context as? Navigation
+ val bookmarkFeatureAllowed = navigation?.canBookmark() ?: true // We allow bookmarking by default if it's not explicitly disabled
+ if (bookmarkFeatureAllowed) {
+ toolbar.setMenu(R.menu.bookmark_menu) { consumer?.accept(AssignmentDetailsEvent.AddBookmarkClicked) }
+ }
+
submissionStatusFailedSubtitle.setTextColor(ThemePrefs.buttonColor)
submissionStatusUploadingSubtitle.setTextColor(ThemePrefs.buttonColor)
submissionAndRubricLabel.setTextColor(ThemePrefs.buttonColor)
submitButton.setBackgroundColor(ThemePrefs.buttonColor)
submitButton.setTextColor(ThemePrefs.buttonTextColor)
+
+ if (isAccessiblityEnabled) {
+ val constraintSet = ConstraintSet()
+ constraintSet.clone(constraintParent)
+ constraintSet.clear(submitButton.id, ConstraintSet.BOTTOM)
+ constraintSet.clear(swipeRefreshLayout.id, ConstraintSet.TOP)
+ constraintSet.clear(swipeRefreshLayout.id, ConstraintSet.BOTTOM)
+ constraintSet.connect(submitButton.id, ConstraintSet.TOP, toolbar.id, ConstraintSet.BOTTOM)
+ constraintSet.connect(swipeRefreshLayout.id, ConstraintSet.TOP, submitButton.id, ConstraintSet.BOTTOM)
+ constraintSet.connect(swipeRefreshLayout.id, ConstraintSet.BOTTOM, ConstraintSet.PARENT_ID, ConstraintSet.BOTTOM)
+ constraintSet.applyTo(constraintParent)
+ }
}
override fun applyTheme() {
@@ -95,14 +115,18 @@ class AssignmentDetailsView(
submissionRubricButton.onClick { output.accept(AssignmentDetailsEvent.ViewSubmissionClicked) }
gradeContainer.onClick { output.accept(AssignmentDetailsEvent.ViewSubmissionClicked) }
submitButton.onClick {
- logEvent(AnalyticsEventConstants.ASSIGNMENT_SUBMIT_SELECTED)
- output.accept(AssignmentDetailsEvent.SubmitAssignmentClicked)
+ onSubmitClick(output)
}
attachmentIcon.onClick { output.accept(AssignmentDetailsEvent.DiscussionAttachmentClicked) }
swipeRefreshLayout.setOnRefreshListener { output.accept(AssignmentDetailsEvent.PullToRefresh) }
setupDescriptionView()
}
+ private fun onSubmitClick(output: Consumer) {
+ logEvent(AnalyticsEventConstants.ASSIGNMENT_SUBMIT_SELECTED)
+ output.accept(AssignmentDetailsEvent.SubmitAssignmentClicked)
+ }
+
private fun setupDescriptionView() {
descriptionWebView.canvasWebViewClientCallback = object : CanvasWebView.CanvasWebViewClientCallback {
override fun openMediaFromWebView(mime: String, url: String, filename: String) {
@@ -148,12 +172,13 @@ class AssignmentDetailsView(
noDescriptionContainer.setVisible(visibilities.noDescriptionLabel)
descriptionWebView.setVisible(visibilities.description)
allowedAttemptsContainer.setVisible(visibilities.allowedAttempts)
- submitButton.isEnabled = visibilities.submitButtonEnabled
+ submitButton.isEnabled = visibilities.submitButtonEnabled && visibilities.submitButton
if (visibilities.submitButtonEnabled) {
submitButton.alpha = 1f
} else {
submitButton.alpha = 0.2f
}
+ submitButton.importantForAccessibility = if (visibilities.submitButton) View.IMPORTANT_FOR_ACCESSIBILITY_YES else View.IMPORTANT_FOR_ACCESSIBILITY_NO
submitButton.setVisible(visibilities.submitButton)
submissionUploadStatusContainer.setVisible(visibilities.submissionUploadStatusInProgress || visibilities.submissionUploadStatusFailed)
submissionStatusUploading.setVisible(visibilities.submissionUploadStatusInProgress)
@@ -252,6 +277,9 @@ class AssignmentDetailsView(
// The LTI info shouldn't be null if we are showing the Studio upload option
showStudioUploadView(assignment, ltiToolUrl!!, ltiToolName!!)
}
+ setupDialogRow(dialog, dialog.submissionEntryStudentAnnotation, visibilities.studentAnnotation) {
+ showStudentAnnotationView(assignment.htmlUrl ?: "")
+ }
}
dialog.show()
}
@@ -373,6 +401,12 @@ class AssignmentDetailsView(
RouteMatcher.route(context, StudioWebViewFragment.makeRoute(canvasContext, ltiUrl, studioLtiToolName, true, assignment))
}
+ fun showStudentAnnotationView(assignmentUrl: String) {
+ logEvent(AnalyticsEventConstants.SUBMIT_STUDENT_ANNOTATION_SELECTED)
+ RouteMatcher.route(context,
+ UnsupportedFeatureFragment.makeRoute(canvasContext, unsupportedDescription = context.getString(R.string.studentAnnotationUnsupportedDescription), url = assignmentUrl))
+ }
+
fun showQuizOrDiscussionView(url: String) {
if (!RouteMatcher.canRouteInternally(context, url, ApiPrefs.domain, true)) {
val intent = Intent(context, InternalWebViewActivity::class.java)
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 b6ac4de1cf..9c9984b8fa 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
@@ -79,7 +79,8 @@ data class SubmissionTypesVisibilities(
var urlEntry: Boolean = false,
var fileUpload: Boolean = false,
var mediaRecording: Boolean = false,
- var studioUpload: Boolean = false
+ var studioUpload: Boolean = false,
+ var studentAnnotation: Boolean = false
)
data class QuizDescriptionViewState(
diff --git a/apps/student/src/main/java/com/instructure/student/mobius/common/ChannelSource.kt b/apps/student/src/main/java/com/instructure/student/mobius/common/ChannelSource.kt
index 9ac32b063b..c8bea100f7 100644
--- a/apps/student/src/main/java/com/instructure/student/mobius/common/ChannelSource.kt
+++ b/apps/student/src/main/java/com/instructure/student/mobius/common/ChannelSource.kt
@@ -24,7 +24,6 @@ import com.spotify.mobius.functions.Consumer
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.channels.BroadcastChannel
import kotlinx.coroutines.channels.consumeEach
-import kotlinx.coroutines.channels.mapNotNull
import kotlinx.coroutines.launch
/**
@@ -35,8 +34,13 @@ import kotlinx.coroutines.launch
abstract class ChannelSource (private val channel: BroadcastChannel) : EventSource {
override fun subscribe(eventConsumer: Consumer): Disposable {
- val receiveChannel = channel.openSubscription().mapNotNull { mapEvent(it) }
- GlobalScope.launch { receiveChannel.consumeEach { eventConsumer.accept(it) } }
+ val receiveChannel = channel.openSubscription()
+ GlobalScope.launch {
+ receiveChannel.consumeEach {
+ val event = mapEvent(it)
+ event?.let { nunNullEvent -> eventConsumer.accept(nunNullEvent) }
+ }
+ }
return Disposable { receiveChannel.cancel() }
}
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 86b1048144..7b256d2a92 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
@@ -48,10 +48,7 @@ import com.instructure.student.db.getInstance
import com.instructure.student.db.sqlColAdapters.Date
import com.instructure.student.events.ShowConfettiEvent
import com.instructure.student.mobius.common.ChannelSource
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.GlobalScope
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.*
import org.greenrobot.eventbus.EventBus
import org.threeten.bp.OffsetDateTime
import java.io.File
@@ -258,7 +255,8 @@ class SubmissionService : IntentService(SubmissionService::class.java.simpleName
return attachmentIds
}
- @UseExperimental(ExperimentalCoroutinesApi::class)
+ @ObsoleteCoroutinesApi
+ @OptIn(ExperimentalCoroutinesApi::class)
private fun uploadComment(intent: Intent) {
runBlocking {
val db = Db.getInstance(this@SubmissionService)
@@ -411,7 +409,7 @@ class SubmissionService : IntentService(SubmissionService::class.java.simpleName
}
val newComment = submission.submissionComments.last()
- ChannelSource.getChannel().offer(newComment)
+ ChannelSource.getChannel().trySend(newComment)
// Remove db entry
commentDb.deleteCommentById(comment.id)
diff --git a/apps/student/src/main/java/com/instructure/student/mobius/conferences/conference_details/ui/ConferenceDetailsView.kt b/apps/student/src/main/java/com/instructure/student/mobius/conferences/conference_details/ui/ConferenceDetailsView.kt
index cd928ded30..5d34bb0033 100644
--- a/apps/student/src/main/java/com/instructure/student/mobius/conferences/conference_details/ui/ConferenceDetailsView.kt
+++ b/apps/student/src/main/java/com/instructure/student/mobius/conferences/conference_details/ui/ConferenceDetailsView.kt
@@ -23,6 +23,7 @@ import android.os.Build
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
+import androidx.browser.customtabs.CustomTabColorSchemeParams
import androidx.browser.customtabs.CustomTabsIntent
import com.google.android.material.snackbar.Snackbar
import com.instructure.canvasapi2.models.CanvasContext
@@ -102,8 +103,12 @@ class ConferenceDetailsView(val canvasContext: CanvasContext, inflater: LayoutIn
}
fun launchUrl(url: String) {
- var intent = CustomTabsIntent.Builder()
+ val colorSchemeParams = CustomTabColorSchemeParams.Builder()
.setToolbarColor(canvasContext.color)
+ .build()
+
+ var intent = CustomTabsIntent.Builder()
+ .setDefaultColorSchemeParams(colorSchemeParams)
.setShowTitle(true)
.build()
.intent
diff --git a/apps/student/src/main/java/com/instructure/student/mobius/conferences/conference_list/ui/ConferenceListView.kt b/apps/student/src/main/java/com/instructure/student/mobius/conferences/conference_list/ui/ConferenceListView.kt
index 577b32d740..9c012970b0 100644
--- a/apps/student/src/main/java/com/instructure/student/mobius/conferences/conference_list/ui/ConferenceListView.kt
+++ b/apps/student/src/main/java/com/instructure/student/mobius/conferences/conference_list/ui/ConferenceListView.kt
@@ -18,17 +18,14 @@ package com.instructure.student.mobius.conferences.conference_list.ui
import android.app.Activity
import android.net.Uri
-import android.os.Build
import android.view.LayoutInflater
import android.view.MenuItem
import android.view.ViewGroup
-import android.widget.ProgressBar
+import androidx.browser.customtabs.CustomTabColorSchemeParams
import androidx.browser.customtabs.CustomTabsIntent
import androidx.recyclerview.widget.LinearLayoutManager
import com.instructure.canvasapi2.models.CanvasContext
import com.instructure.canvasapi2.models.Conference
-import com.instructure.canvasapi2.utils.ApiPrefs
-import com.instructure.canvasapi2.utils.ContextKeeper
import com.instructure.canvasapi2.utils.exhaustive
import com.instructure.pandautils.utils.*
import com.instructure.student.R
@@ -105,8 +102,12 @@ class ConferenceListView(val canvasContext: CanvasContext, inflater: LayoutInfla
}
fun launchUrl(url: String) {
- var intent = CustomTabsIntent.Builder()
+ val colorSchemeParams = CustomTabColorSchemeParams.Builder()
.setToolbarColor(canvasContext.color)
+ .build()
+
+ var intent = CustomTabsIntent.Builder()
+ .setDefaultColorSchemeParams(colorSchemeParams)
.setShowTitle(true)
.build()
.intent
diff --git a/apps/student/src/main/java/com/instructure/student/mobius/elementary/ElementaryDashboardFragment.kt b/apps/student/src/main/java/com/instructure/student/mobius/elementary/ElementaryDashboardFragment.kt
new file mode 100644
index 0000000000..e7844fdb00
--- /dev/null
+++ b/apps/student/src/main/java/com/instructure/student/mobius/elementary/ElementaryDashboardFragment.kt
@@ -0,0 +1,114 @@
+/*
+ * Copyright (C) 2021 - 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.mobius.elementary
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.viewpager.widget.ViewPager
+import com.google.android.material.tabs.TabLayout
+import com.instructure.canvasapi2.models.CanvasContext
+import com.instructure.interactions.router.Route
+import com.instructure.pandautils.features.elementary.ElementaryDashboardPagerAdapter
+import com.instructure.pandautils.features.elementary.grades.GradesFragment
+import com.instructure.pandautils.features.elementary.homeroom.HomeroomFragment
+import com.instructure.pandautils.features.elementary.resources.ResourcesFragment
+import com.instructure.pandautils.features.elementary.schedule.pager.SchedulePagerFragment
+import com.instructure.pandautils.utils.Const
+import com.instructure.pandautils.utils.ParcelableArg
+import com.instructure.pandautils.utils.isTablet
+import com.instructure.pandautils.utils.makeBundle
+import com.instructure.student.R
+import com.instructure.student.databinding.FragmentElementaryDashboardBinding
+import com.instructure.student.fragment.ParentFragment
+import kotlinx.android.synthetic.main.fragment_course_grid.toolbar
+import kotlinx.android.synthetic.main.fragment_elementary_dashboard.*
+
+class ElementaryDashboardFragment : ParentFragment() {
+
+ private val canvasContext by ParcelableArg(key = Const.CANVAS_CONTEXT)
+
+ private val schedulePagerFragment = SchedulePagerFragment.newInstance()
+
+ private val fragments = listOf(
+ HomeroomFragment.newInstance(),
+ schedulePagerFragment,
+ GradesFragment.newInstance(),
+ ResourcesFragment.newInstance()
+ )
+
+ override fun title(): String = if (isAdded) getString(R.string.dashboard) else ""
+
+ override fun applyTheme() {
+ toolbar.title = title()
+ navigation?.attachNavigationDrawer(this, toolbar)
+ }
+
+ override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
+ val binding = FragmentElementaryDashboardBinding.inflate(inflater, container, false)
+ binding.lifecycleOwner = this
+ binding.todayButtonVisibility = schedulePagerFragment.getTodayButtonVisibility()
+
+ binding.todayButton.setOnClickListener {
+ schedulePagerFragment.jumpToToday()
+ }
+
+ binding.dashboardPager.offscreenPageLimit = fragments.size
+
+ return binding.root
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+
+ dashboardPager.adapter = ElementaryDashboardPagerAdapter(fragments, childFragmentManager)
+ dashboardTabLayout.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener {
+ override fun onTabReselected(tab: TabLayout.Tab?) = Unit
+
+ override fun onTabUnselected(tab: TabLayout.Tab?) = Unit
+
+ override fun onTabSelected(tab: TabLayout.Tab?) {
+ tab?.let {
+ dashboardPager.setCurrentItem(it.position, !isTablet)
+ if (it.position != fragments.indexOf(schedulePagerFragment)) {
+ todayButton.visibility = View.GONE
+ } else {
+ todayButton.visibility =
+ if (schedulePagerFragment.getTodayButtonVisibility().value == true) View.VISIBLE else View.GONE
+ }
+ }
+ }
+ })
+ }
+
+ override fun onHiddenChanged(hidden: Boolean) {
+ super.onHiddenChanged(hidden)
+ if (!hidden) {
+ (dashboardPager?.adapter as? ElementaryDashboardPagerAdapter)?.refreshHomeroomAssignments()
+ }
+ }
+
+ companion object {
+ fun newInstance(route: Route) =
+ ElementaryDashboardFragment().apply {
+ arguments = route.canvasContext?.makeBundle(route.arguments) ?: route.arguments
+ }
+
+ fun makeRoute(canvasContext: CanvasContext?) = Route(ElementaryDashboardFragment::class.java, canvasContext)
+ }
+}
\ No newline at end of file
diff --git a/apps/student/src/main/java/com/instructure/student/mobius/elementary/grades/StudentGradesRouter.kt b/apps/student/src/main/java/com/instructure/student/mobius/elementary/grades/StudentGradesRouter.kt
new file mode 100644
index 0000000000..0475d8f222
--- /dev/null
+++ b/apps/student/src/main/java/com/instructure/student/mobius/elementary/grades/StudentGradesRouter.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2021 - 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.mobius.elementary.grades
+
+import androidx.fragment.app.FragmentActivity
+import com.instructure.canvasapi2.models.Course
+import com.instructure.pandautils.features.elementary.grades.GradesRouter
+import com.instructure.student.fragment.GradesListFragment
+import com.instructure.student.router.RouteMatcher
+
+class StudentGradesRouter(private val activity: FragmentActivity) : GradesRouter {
+
+ override fun openCourseGrades(course: Course) {
+ val route = GradesListFragment.makeRoute(course)
+ RouteMatcher.route(activity, route)
+ }
+}
\ No newline at end of file
diff --git a/apps/student/src/main/java/com/instructure/student/mobius/elementary/homeroom/StudentHomeroomRouter.kt b/apps/student/src/main/java/com/instructure/student/mobius/elementary/homeroom/StudentHomeroomRouter.kt
new file mode 100644
index 0000000000..a4e73ebc8f
--- /dev/null
+++ b/apps/student/src/main/java/com/instructure/student/mobius/elementary/homeroom/StudentHomeroomRouter.kt
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2021 - 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.mobius.elementary.homeroom
+
+import androidx.fragment.app.FragmentActivity
+import com.instructure.canvasapi2.models.CanvasContext
+import com.instructure.canvasapi2.models.Course
+import com.instructure.canvasapi2.models.DiscussionTopicHeader
+import com.instructure.canvasapi2.utils.ApiPrefs
+import com.instructure.pandautils.features.elementary.homeroom.HomeroomRouter
+import com.instructure.student.flutterChannels.FlutterComm
+import com.instructure.student.fragment.*
+import com.instructure.student.mobius.assignmentDetails.ui.AssignmentDetailsFragment
+import com.instructure.student.router.RouteMatcher
+
+class StudentHomeroomRouter(private val activity: FragmentActivity) : HomeroomRouter {
+
+ override fun openAnnouncements(canvasContext: CanvasContext) {
+ val route = AnnouncementListFragment.makeRoute(canvasContext)
+ RouteMatcher.route(activity, route)
+ }
+
+ override fun openCourse(course: Course) {
+ RouteMatcher.route(activity, CourseBrowserFragment.makeRoute(course))
+ }
+
+ override fun openAssignments(course: Course) {
+ RouteMatcher.route(activity, AssignmentListFragment.makeRoute(course))
+ }
+
+ override fun openAnnouncementDetails(course: Course, announcement: DiscussionTopicHeader) {
+ RouteMatcher.route(activity, DiscussionDetailsFragment.makeRoute(course, announcement))
+ }
+
+ override fun updateColors() {
+ FlutterComm.sendUpdatedTheme()
+ }
+}
\ No newline at end of file
diff --git a/apps/student/src/main/java/com/instructure/student/mobius/elementary/resources/StudentResourcesRouter.kt b/apps/student/src/main/java/com/instructure/student/mobius/elementary/resources/StudentResourcesRouter.kt
new file mode 100644
index 0000000000..c6c5ceff05
--- /dev/null
+++ b/apps/student/src/main/java/com/instructure/student/mobius/elementary/resources/StudentResourcesRouter.kt
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2021 - 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.mobius.elementary.resources
+
+import androidx.fragment.app.FragmentActivity
+import com.instructure.canvasapi2.models.*
+import com.instructure.pandautils.features.elementary.resources.itemviewmodels.ResourcesRouter
+import com.instructure.student.fragment.InboxComposeMessageFragment
+import com.instructure.student.fragment.LtiLaunchFragment
+import com.instructure.student.router.RouteMatcher
+
+class StudentResourcesRouter(private val activity: FragmentActivity) : ResourcesRouter {
+
+ override fun openLti(ltiTool: LTITool) {
+ val course = Course(id = ltiTool.contextId ?: 0, name = ltiTool.contextName ?: "")
+ val route = LtiLaunchFragment.makeRoute(
+ course,
+ ltiTool.url ?: ltiTool.courseNavigation?.url ?: "",
+ ltiTool.courseNavigation?.text ?: ltiTool.name ?: "",
+ sessionLessLaunch = true,
+ isAssignmentLTI = false,
+ ltiTool = ltiTool)
+ RouteMatcher.route(activity, route)
+ }
+
+ override fun openComposeMessage(user: User) {
+ val recipient = Recipient.from(user)
+ val context = Course(id = user.enrollments[0].courseId, homeroomCourse = true)
+ val route = InboxComposeMessageFragment.makeRoute(context, arrayListOf(recipient), homeroomMessage = true)
+ RouteMatcher.route(activity, route)
+ }
+}
\ No newline at end of file
diff --git a/apps/student/src/main/java/com/instructure/student/mobius/elementary/schedule/StudentScheduleRouter.kt b/apps/student/src/main/java/com/instructure/student/mobius/elementary/schedule/StudentScheduleRouter.kt
new file mode 100644
index 0000000000..580c9ec153
--- /dev/null
+++ b/apps/student/src/main/java/com/instructure/student/mobius/elementary/schedule/StudentScheduleRouter.kt
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2021 - 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.mobius.elementary.schedule
+
+import androidx.fragment.app.FragmentActivity
+import com.instructure.canvasapi2.models.CanvasContext
+import com.instructure.canvasapi2.models.Course
+import com.instructure.canvasapi2.models.DiscussionTopicHeader
+import com.instructure.pandautils.features.elementary.schedule.ScheduleRouter
+import com.instructure.student.fragment.BasicQuizViewFragment
+import com.instructure.student.fragment.CalendarEventFragment
+import com.instructure.student.fragment.CourseBrowserFragment
+import com.instructure.student.fragment.DiscussionDetailsFragment
+import com.instructure.student.mobius.assignmentDetails.ui.AssignmentDetailsFragment
+import com.instructure.student.router.RouteMatcher
+
+class StudentScheduleRouter(private val activity: FragmentActivity) : ScheduleRouter {
+
+ override fun openAssignment(canvasContext: CanvasContext, assignmentId: Long) {
+ RouteMatcher.route(activity, AssignmentDetailsFragment.makeRoute(canvasContext, assignmentId))
+ }
+
+ override fun openCalendarEvent(canvasContext: CanvasContext, scheduleItemId: Long) {
+ RouteMatcher.route(activity, CalendarEventFragment.makeRoute(canvasContext, scheduleItemId))
+ }
+
+ override fun openAnnouncementDetails(course: Course, announcement: DiscussionTopicHeader) {
+ RouteMatcher.route(activity, DiscussionDetailsFragment.makeRoute(course, announcement))
+ }
+
+ override fun openQuiz(canvasContext: CanvasContext, htmlUrl: String) {
+ RouteMatcher.route(activity, BasicQuizViewFragment.makeRoute(canvasContext, htmlUrl))
+ }
+
+ override fun openDiscussion(canvasContext: CanvasContext, discussionId: Long, discussionTitle: String) {
+ RouteMatcher.route(
+ activity,
+ DiscussionDetailsFragment.makeRoute(canvasContext, discussionId, title = discussionTitle)
+ )
+ }
+
+ override fun openCourse(course: Course) {
+ RouteMatcher.route(activity, CourseBrowserFragment.makeRoute(course))
+ }
+}
\ No newline at end of file
diff --git a/apps/student/src/main/java/com/instructure/student/mobius/settings/help/StudentHelpDialogFragmentBehavior.kt b/apps/student/src/main/java/com/instructure/student/mobius/settings/help/StudentHelpDialogFragmentBehavior.kt
new file mode 100644
index 0000000000..b9aea5dad9
--- /dev/null
+++ b/apps/student/src/main/java/com/instructure/student/mobius/settings/help/StudentHelpDialogFragmentBehavior.kt
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2021 - 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.mobius.settings.help
+
+import androidx.fragment.app.FragmentActivity
+import com.instructure.loginapi.login.dialog.ErrorReportDialog
+import com.instructure.pandautils.features.help.HelpDialogFragmentBehavior
+import com.instructure.pandautils.utils.AppType
+import com.instructure.pandautils.utils.Utils
+import com.instructure.student.R
+import com.instructure.student.activity.InternalWebViewActivity
+import com.instructure.student.dialog.AskInstructorDialogStyled
+
+class StudentHelpDialogFragmentBehavior(private val activity: FragmentActivity) : HelpDialogFragmentBehavior {
+
+ override fun reportProblem() {
+ val dialog = ErrorReportDialog()
+ dialog.arguments = ErrorReportDialog.createBundle(activity.getString(R.string.appUserTypeStudent))
+ dialog.show(activity.supportFragmentManager, ErrorReportDialog.TAG)
+ }
+
+ override fun rateTheApp() {
+ Utils.goToAppStore(AppType.STUDENT, activity)
+ }
+
+ override fun askInstructor() {
+ AskInstructorDialogStyled().show(activity.supportFragmentManager, AskInstructorDialogStyled.TAG)
+ }
+
+ override fun openWebView(url: String, title: String) {
+ activity.startActivity(InternalWebViewActivity.createIntent(activity, url, title, false))
+ }
+}
\ No newline at end of file
diff --git a/apps/student/src/main/java/com/instructure/student/mobius/settings/help/StudentHelpLinkFilter.kt b/apps/student/src/main/java/com/instructure/student/mobius/settings/help/StudentHelpLinkFilter.kt
new file mode 100644
index 0000000000..62c982cb35
--- /dev/null
+++ b/apps/student/src/main/java/com/instructure/student/mobius/settings/help/StudentHelpLinkFilter.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2021 - 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.mobius.settings.help
+
+import com.instructure.canvasapi2.models.Course
+import com.instructure.canvasapi2.models.HelpLink
+import com.instructure.pandautils.features.help.HelpLinkFilter
+
+class StudentHelpLinkFilter : HelpLinkFilter {
+
+ override fun isLinkAllowed(link: HelpLink, favoriteCourses: List): Boolean {
+ return ((link.availableTo.contains("student") || link.availableTo.contains("user"))
+ && (link.url != "#teacher_feedback" || favoriteCourses.filter { !it.isTeacher }.count() > 0))
+ }
+}
\ No newline at end of file
diff --git a/apps/student/src/main/java/com/instructure/student/mobius/syllabus/ui/SyllabusView.kt b/apps/student/src/main/java/com/instructure/student/mobius/syllabus/ui/SyllabusView.kt
index 1599d246b9..3a120fad4b 100644
--- a/apps/student/src/main/java/com/instructure/student/mobius/syllabus/ui/SyllabusView.kt
+++ b/apps/student/src/main/java/com/instructure/student/mobius/syllabus/ui/SyllabusView.kt
@@ -23,7 +23,6 @@ import com.google.android.material.tabs.TabLayout
import com.instructure.canvasapi2.models.Assignment
import com.instructure.canvasapi2.models.CanvasContext
import com.instructure.canvasapi2.models.ScheduleItem
-import com.instructure.canvasapi2.utils.exhaustive
import com.instructure.pandautils.utils.*
import com.instructure.student.R
import com.instructure.student.fragment.CalendarEventFragment
@@ -31,7 +30,7 @@ import com.instructure.student.mobius.assignmentDetails.ui.AssignmentDetailsFrag
import com.instructure.student.mobius.common.ui.MobiusView
import com.instructure.student.mobius.syllabus.SyllabusEvent
import com.instructure.student.router.RouteMatcher
-import com.instructure.student.view.EmptyView
+import com.instructure.pandautils.views.EmptyView
import com.spotify.mobius.functions.Consumer
import kotlinx.android.synthetic.main.fragment_syllabus.*
import kotlinx.android.synthetic.main.fragment_syllabus_events.*
diff --git a/apps/student/src/main/java/com/instructure/student/navigation/DefaultNavigationBehavior.kt b/apps/student/src/main/java/com/instructure/student/navigation/DefaultNavigationBehavior.kt
new file mode 100644
index 0000000000..9229b7757e
--- /dev/null
+++ b/apps/student/src/main/java/com/instructure/student/navigation/DefaultNavigationBehavior.kt
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2021 - 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.navigation
+
+import com.instructure.canvasapi2.models.CanvasContext
+import com.instructure.canvasapi2.utils.ApiPrefs
+import com.instructure.interactions.router.Route
+import com.instructure.student.R
+import com.instructure.student.fragment.*
+
+class DefaultNavigationBehavior() : NavigationBehavior {
+
+ override val bottomNavBarFragments: List> = listOf(
+ DashboardFragment::class.java,
+ CalendarFragment::class.java,
+ ToDoListFragment::class.java,
+ NotificationListFragment::class.java,
+ InboxFragment::class.java
+ )
+
+ override val homeFragmentClass: Class = DashboardFragment::class.java
+
+ override val visibleNavigationMenuItems: Set = setOf(NavigationMenuItem.FILES, NavigationMenuItem.BOOKMARKS, NavigationMenuItem.SETTINGS)
+
+ override val visibleOptionsMenuItems: Set = setOf(OptionsMenuItem.SHOW_GRADES, OptionsMenuItem.COLOR_OVERLAY)
+
+ override val visibleAccountMenuItems: Set = setOf(AccountMenuItem.HELP, AccountMenuItem.CHANGE_USER, AccountMenuItem.LOGOUT)
+
+ override val shouldOverrideFont: Boolean
+ get() = false
+
+ override val bottomBarMenu: Int = R.menu.bottom_bar_menu
+
+ override fun createHomeFragmentRoute(canvasContext: CanvasContext?): Route {
+ return DashboardFragment.makeRoute(ApiPrefs.user)
+ }
+
+ override fun createHomeFragment(route: Route): ParentFragment {
+ return DashboardFragment.newInstance(route)
+ }
+}
\ No newline at end of file
diff --git a/apps/student/src/main/java/com/instructure/student/navigation/ElementaryNavigationBehavior.kt b/apps/student/src/main/java/com/instructure/student/navigation/ElementaryNavigationBehavior.kt
new file mode 100644
index 0000000000..8e3291174c
--- /dev/null
+++ b/apps/student/src/main/java/com/instructure/student/navigation/ElementaryNavigationBehavior.kt
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2021 - 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.navigation
+
+import com.instructure.canvasapi2.models.CanvasContext
+import com.instructure.canvasapi2.utils.ApiPrefs
+import com.instructure.interactions.router.Route
+import com.instructure.student.R
+import com.instructure.student.fragment.*
+import com.instructure.student.mobius.elementary.ElementaryDashboardFragment
+
+class ElementaryNavigationBehavior() : NavigationBehavior {
+
+ override val bottomNavBarFragments: List> = listOf(
+ ElementaryDashboardFragment::class.java,
+ CalendarFragment::class.java,
+ ToDoListFragment::class.java,
+ NotificationListFragment::class.java,
+ InboxFragment::class.java
+ )
+
+ override val homeFragmentClass: Class = ElementaryDashboardFragment::class.java
+
+ override val visibleNavigationMenuItems: Set = setOf(NavigationMenuItem.FILES, NavigationMenuItem.SETTINGS)
+
+ override val visibleOptionsMenuItems: Set = emptySet()
+
+ override val visibleAccountMenuItems: Set = setOf(AccountMenuItem.HELP, AccountMenuItem.CHANGE_USER, AccountMenuItem.LOGOUT)
+
+ override val shouldOverrideFont: Boolean
+ get() = true
+
+ override val bottomBarMenu: Int = R.menu.bottom_bar_menu_elementary
+
+ override fun createHomeFragmentRoute(canvasContext: CanvasContext?): Route {
+ return ElementaryDashboardFragment.makeRoute(ApiPrefs.user)
+ }
+
+ override fun createHomeFragment(route: Route): ParentFragment {
+ return ElementaryDashboardFragment.newInstance(route)
+ }
+}
\ No newline at end of file
diff --git a/apps/student/src/main/java/com/instructure/student/navigation/NavigationBehavior.kt b/apps/student/src/main/java/com/instructure/student/navigation/NavigationBehavior.kt
new file mode 100644
index 0000000000..1c84ada09c
--- /dev/null
+++ b/apps/student/src/main/java/com/instructure/student/navigation/NavigationBehavior.kt
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2021 - 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.navigation
+
+import androidx.annotation.MenuRes
+import com.instructure.canvasapi2.models.CanvasContext
+import com.instructure.interactions.router.Route
+import com.instructure.student.fragment.ParentFragment
+
+interface NavigationBehavior {
+
+ /** 'Root' fragments that should include the bottom nav bar */
+ val bottomNavBarFragments: List>
+
+ val homeFragmentClass: Class
+
+ val visibleNavigationMenuItems: Set
+
+ val visibleOptionsMenuItems: Set
+
+ val visibleAccountMenuItems: Set
+
+ val shouldOverrideFont: Boolean
+
+ @get:MenuRes
+ val bottomBarMenu: Int
+
+ fun createHomeFragmentRoute(canvasContext: CanvasContext?): Route
+
+ fun createHomeFragment(route: Route): ParentFragment
+}
+
+enum class NavigationMenuItem {
+ FILES, BOOKMARKS, SETTINGS
+}
+
+enum class OptionsMenuItem {
+ SHOW_GRADES, COLOR_OVERLAY
+}
+
+enum class AccountMenuItem {
+ HELP, CHANGE_USER, LOGOUT
+}
\ No newline at end of file
diff --git a/apps/student/src/main/java/com/instructure/student/navigation/StudentWebViewRouter.kt b/apps/student/src/main/java/com/instructure/student/navigation/StudentWebViewRouter.kt
new file mode 100644
index 0000000000..634e3baa55
--- /dev/null
+++ b/apps/student/src/main/java/com/instructure/student/navigation/StudentWebViewRouter.kt
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2021 - 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.navigation
+
+import androidx.fragment.app.FragmentActivity
+import com.instructure.canvasapi2.utils.ApiPrefs
+import com.instructure.pandautils.navigation.WebViewRouter
+import com.instructure.student.router.RouteMatcher
+
+class StudentWebViewRouter(val activity: FragmentActivity) : WebViewRouter {
+
+ override fun canRouteInternally(url: String): Boolean {
+ return RouteMatcher.canRouteInternally(activity, url, ApiPrefs.domain, routeIfPossible = false, allowUnsupported = false)
+ }
+
+ override fun routeInternally(url: String) {
+ RouteMatcher.canRouteInternally(activity, url, ApiPrefs.domain, routeIfPossible = true, allowUnsupported = false)
+ }
+
+ override fun openMedia(url: String) {
+ RouteMatcher.openMedia(activity, url)
+ }
+}
\ No newline at end of file
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 4378945e68..f505162065 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
@@ -6,6 +6,7 @@ import com.instructure.interactions.router.Route
import com.instructure.pandautils.utils.Const
import com.instructure.student.AnnotationComments.AnnotationCommentListFragment
import com.instructure.student.activity.NothingToSeeHereFragment
+import com.instructure.student.features.dashboard.edit.EditDashboardFragment
import com.instructure.student.features.files.search.FileSearchFragment
import com.instructure.student.fragment.*
import com.instructure.student.mobius.assignmentDetails.submission.file.ui.UploadStatusSubmissionFragment
@@ -17,6 +18,7 @@ import com.instructure.student.mobius.assignmentDetails.submissionDetails.ui.Sub
import com.instructure.student.mobius.assignmentDetails.ui.AssignmentDetailsFragment
import com.instructure.student.mobius.conferences.conference_details.ui.ConferenceDetailsFragment
import com.instructure.student.mobius.conferences.conference_list.ui.ConferenceListFragment
+import com.instructure.student.mobius.elementary.ElementaryDashboardFragment
import com.instructure.student.mobius.syllabus.ui.SyllabusFragment
object RouteResolver {
@@ -56,12 +58,12 @@ object RouteResolver {
return when {
cls.isA() -> DashboardFragment.newInstance(route)
+ cls.isA() -> ElementaryDashboardFragment.newInstance(route)
cls.isA() -> ToDoListFragment.newInstance(route)
cls.isA() -> NotificationListFragment.newInstance(route)
cls.isA() -> InboxFragment.newInstance(route)
cls.isA() -> CourseBrowserFragment.newInstance(route)
- cls.isA() -> AllCoursesFragment.newInstance(route)
- cls.isA() -> EditFavoritesFragment.newInstance(route)
+ cls.isA() -> EditDashboardFragment.newInstance(route)
cls.isA() -> ModuleQuizDecider.newInstance(route)
cls.isA() -> EditPageDetailsFragment.newInstance(route)
cls.isA() -> InboxConversationFragment.newInstance(route)
diff --git a/apps/student/src/main/java/com/instructure/student/tasks/StudentLogoutTask.kt b/apps/student/src/main/java/com/instructure/student/tasks/StudentLogoutTask.kt
index 4bd0ea1461..d690df0dc8 100644
--- a/apps/student/src/main/java/com/instructure/student/tasks/StudentLogoutTask.kt
+++ b/apps/student/src/main/java/com/instructure/student/tasks/StudentLogoutTask.kt
@@ -22,12 +22,18 @@ import android.net.Uri
import com.google.firebase.iid.FirebaseInstanceId
import com.instructure.canvasapi2.utils.tryOrNull
import com.instructure.loginapi.login.tasks.LogoutTask
+import com.instructure.pandautils.typeface.TypefaceBehavior
import com.instructure.student.activity.LoginActivity
import com.instructure.student.flutterChannels.FlutterComm
import com.instructure.student.util.StudentPrefs
import com.instructure.student.widget.WidgetUpdater
-class StudentLogoutTask(type: Type, uri: Uri? = null) : LogoutTask(type, uri) {
+class StudentLogoutTask(
+ type: Type,
+ uri: Uri? = null,
+ canvasForElementaryFeatureFlag: Boolean = false,
+ typefaceBehavior: TypefaceBehavior? = null
+) : LogoutTask(type, uri, canvasForElementaryFeatureFlag, typefaceBehavior) {
override fun onCleanup() {
FlutterComm.reset()
diff --git a/apps/student/src/main/java/com/instructure/student/util/AppManager.kt b/apps/student/src/main/java/com/instructure/student/util/AppManager.kt
index 090daa670f..549c7cfde6 100644
--- a/apps/student/src/main/java/com/instructure/student/util/AppManager.kt
+++ b/apps/student/src/main/java/com/instructure/student/util/AppManager.kt
@@ -33,6 +33,7 @@ import com.instructure.canvasapi2.utils.MasqueradeHelper
import com.instructure.canvasapi2.utils.RemoteConfigUtils
import com.instructure.canvasapi2.utils.pageview.PageViewUploadService
import com.instructure.loginapi.login.tasks.LogoutTask
+import com.instructure.pandautils.typeface.TypefaceBehavior
import com.instructure.pandautils.utils.ColorKeeper
import com.instructure.student.BuildConfig
import com.instructure.student.R
@@ -42,182 +43,24 @@ import com.instructure.student.tasks.StudentLogoutTask
import com.pspdfkit.PSPDFKit
import com.pspdfkit.exceptions.InvalidPSPDFKitLicenseException
import com.pspdfkit.exceptions.PSPDFKitInitializationFailedException
+import dagger.hilt.android.HiltAndroidApp
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.embedding.engine.FlutterEngineCache
import io.flutter.embedding.engine.dart.DartExecutor.DartEntrypoint
+import javax.inject.Inject
-class AppManager : com.instructure.canvasapi2.AppManager(), AnalyticsEventHandling {
+@HiltAndroidApp
+class AppManager : BaseAppManager() {
- // 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)
- }
+ @Inject
+ lateinit var typefaceBehavior: TypefaceBehavior
override fun onCreate() {
- if (MissingSplitsManagerFactory.create(this).disableAppIfMissingRequiredSplits()) {
- // Skip app initialization.
- return
- }
super.onCreate()
-
- // Call it superstition, but I don't trust BuildConfig flags to be set correctly
- // in library builds. IS_TESTING, for example, does not percolate down to libraries
- // correctly. So I'm reading/setting these user properties here instead of canvasapi2/AppManager.
- Analytics.setUserProperty(USER_PROPERTY_BUILD_TYPE, if(BuildConfig.DEBUG) "debug" else "release")
- Analytics.setUserProperty(USER_PROPERTY_OS_VERSION, Build.VERSION.SDK_INT.toString())
-
- // Hold off on initializing this until we emit the user properties.
- RemoteConfigUtils.initialize()
-
- initPSPDFKit()
-
- if (BuildConfig.DEBUG) {
- FirebaseCrashlytics.getInstance().setCrashlyticsCollectionEnabled(false)
- } else {
- FirebaseCrashlytics.getInstance().setCrashlyticsCollectionEnabled(true)
- }
-
- MasqueradeHelper.masqueradeLogoutTask = Runnable { StudentLogoutTask(LogoutTask.Type.LOGOUT).execute() }
-
- ColorKeeper.defaultColor = ContextCompat.getColor(this, R.color.defaultPrimary)
-
- // There appears to be a bug when the user is installing/updating the android webview stuff.
- // http://code.google.com/p/android/issues/detail?id=175124
- try {
- WebView.setWebContentsDebuggingEnabled(true)
- } catch (e: Exception) {
- FirebaseCrashlytics.getInstance().log("Exception trying to setWebContentsDebuggingEnabled")
- }
-
- PageViewUploadService.schedule(this, StudentPageViewService::class.java)
-
- initFlutterEngine()
- }
-
- private fun initFlutterEngine() {
- flutterEngine = FlutterEngine(this)
-
- FlutterComm.init(flutterEngine, applicationContext)
-
- // Execute the 'main' entrypoint
- flutterEngine.dartExecutor.executeDartEntrypoint(DartEntrypoint.createDefault())
-
- // Cache the FlutterEngine
- FlutterEngineCache.getInstance().put(FLUTTER_ENGINE_ID, flutterEngine)
- }
-
- 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() {
- try {
- PSPDFKit.initialize(this, BuildConfig.PSPDFKIT_LICENSE_KEY)
- } catch (e: PSPDFKitInitializationFailedException) {
- Logger.e("Current device is not compatible with PSPDFKIT!")
- } catch (e: InvalidPSPDFKitLicenseException) {
- Logger.e("Invalid or Trial PSPDFKIT License!")
- }
+ MasqueradeHelper.masqueradeLogoutTask = Runnable { StudentLogoutTask(LogoutTask.Type.LOGOUT, typefaceBehavior = typefaceBehavior).execute() }
}
override fun performLogoutOnAuthError() {
- StudentLogoutTask(LogoutTask.Type.LOGOUT).execute()
+ StudentLogoutTask(LogoutTask.Type.LOGOUT, typefaceBehavior = typefaceBehavior).execute()
}
-
- companion object {
- private const val FLUTTER_ENGINE_ID = "flutter_engine_embed"
-
- lateinit var flutterEngine: FlutterEngine
- }
-
}
diff --git a/apps/student/src/main/java/com/instructure/student/util/AppShortcutManager.kt b/apps/student/src/main/java/com/instructure/student/util/AppShortcutManager.kt
index 7de18a5dd3..f86cc513ba 100644
--- a/apps/student/src/main/java/com/instructure/student/util/AppShortcutManager.kt
+++ b/apps/student/src/main/java/com/instructure/student/util/AppShortcutManager.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2017 - present Instructure, Inc.
+ * Copyright (C) 2021 - 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
@@ -16,80 +16,20 @@
*/
package com.instructure.student.util
-import android.annotation.TargetApi
import android.content.Context
-import android.content.Intent
-import android.content.pm.ShortcutInfo
-import android.content.pm.ShortcutManager
-import android.graphics.drawable.Icon
-import android.os.Build
-import com.instructure.student.R
-import com.instructure.student.activity.LoginActivity
-import java.util.*
-@TargetApi(25)
-object AppShortcutManager {
+interface AppShortcutManager {
- const val ACTION_APP_SHORTCUT = "com.instructure.action.APP_SHORTCUT"
- const val APP_SHORTCUT_PLACEMENT = "com.instructure.APP_SHORTCUT_PLACEMENT"
+ fun make(context: Context)
- const val APP_SHORTCUT_BOOKMARKS = "com.instructure.APP_SHORTCUT_BOOKMARKS"
- const val APP_SHORTCUT_CALENDAR = "com.instructure.APP_SHORTCUT_CALENDAR"
- const val APP_SHORTCUT_TODO = "com.instructure.APP_SHORTCUT_TODO"
- const val APP_SHORTCUT_NOTIFICATIONS = "com.instructure.APP_SHORTCUT_NOTIFICATIONS"
- const val APP_SHORTCUT_INBOX = "com.instructure.APP_SHORTCUT_INBOX"
+ companion object {
+ const val ACTION_APP_SHORTCUT = "com.instructure.action.APP_SHORTCUT"
+ const val APP_SHORTCUT_PLACEMENT = "com.instructure.APP_SHORTCUT_PLACEMENT"
- fun make(context: Context) {
-
- if (Build.VERSION.SDK_INT < 25) return
-
- val manager = context.getSystemService(ShortcutManager::class.java)
-
- val bookmarksIntent = Intent(context, LoginActivity::class.java)
- bookmarksIntent.action = ACTION_APP_SHORTCUT
- bookmarksIntent.putExtra(APP_SHORTCUT_PLACEMENT, APP_SHORTCUT_BOOKMARKS)
- val shortcutBookmarks = createShortcut(context, APP_SHORTCUT_BOOKMARKS,
- context.getString(R.string.bookmarks),
- context.getString(R.string.bookmarks),
- R.mipmap.ic_shortcut_bookmarks,
- bookmarksIntent)
-
- val calendarIntent = Intent(context, LoginActivity::class.java)
- calendarIntent.action = ACTION_APP_SHORTCUT
- calendarIntent.putExtra(APP_SHORTCUT_PLACEMENT, APP_SHORTCUT_CALENDAR)
- val shortcutCalendar = createShortcut(context, APP_SHORTCUT_CALENDAR,
- context.getString(R.string.calendar),
- context.getString(R.string.calendar),
- R.mipmap.ic_shortcut_calendar,
- calendarIntent)
-
- val todoIntent = Intent(context, LoginActivity::class.java)
- todoIntent.action = ACTION_APP_SHORTCUT
- todoIntent.putExtra(APP_SHORTCUT_PLACEMENT, APP_SHORTCUT_TODO)
- val shortcutTodo = createShortcut(context, APP_SHORTCUT_TODO,
- context.getString(R.string.toDoList),
- context.getString(R.string.toDoList),
- R.mipmap.ic_shortcut_todo,
- todoIntent)
-
- val notificationsIntent = Intent(context, LoginActivity::class.java)
- notificationsIntent.action = ACTION_APP_SHORTCUT
- notificationsIntent.putExtra(APP_SHORTCUT_PLACEMENT, APP_SHORTCUT_NOTIFICATIONS)
- val shortcutNotifications = createShortcut(context, APP_SHORTCUT_NOTIFICATIONS,
- context.getString(R.string.notifications),
- context.getString(R.string.notifications),
- R.mipmap.ic_shortcut_notifications,
- notificationsIntent)
-
- manager?.dynamicShortcuts = Arrays.asList(shortcutNotifications, shortcutTodo, shortcutCalendar, shortcutBookmarks)
- }
-
- private fun createShortcut(context: Context, id: String, label: String, longLabel: String, iconResId: Int, intent: Intent): ShortcutInfo {
- return ShortcutInfo.Builder(context, id)
- .setShortLabel(label)
- .setLongLabel(longLabel)
- .setIcon(Icon.createWithResource(context, iconResId))
- .setIntent(intent)
- .build()
+ const val APP_SHORTCUT_BOOKMARKS = "com.instructure.APP_SHORTCUT_BOOKMARKS"
+ const val APP_SHORTCUT_CALENDAR = "com.instructure.APP_SHORTCUT_CALENDAR"
+ const val APP_SHORTCUT_TODO = "com.instructure.APP_SHORTCUT_TODO"
+ const val APP_SHORTCUT_NOTIFICATIONS = "com.instructure.APP_SHORTCUT_NOTIFICATIONS"
+ const val APP_SHORTCUT_INBOX = "com.instructure.APP_SHORTCUT_INBOX"
}
-}
+}
\ No newline at end of file
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
new file mode 100644
index 0000000000..655aceeebf
--- /dev/null
+++ b/apps/student/src/main/java/com/instructure/student/util/BaseAppManager.kt
@@ -0,0 +1,219 @@
+/*
+ * Copyright (C) 2021 - 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.os.Build
+import android.webkit.WebView
+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.*
+import com.instructure.canvasapi2.utils.Analytics
+import com.instructure.canvasapi2.utils.pageview.PageViewUploadService
+import com.instructure.loginapi.login.tasks.LogoutTask
+import com.instructure.pandautils.typeface.TypefaceBehavior
+import com.instructure.pandautils.utils.ColorKeeper
+import com.instructure.student.BuildConfig
+import com.instructure.student.R
+import com.instructure.student.flutterChannels.FlutterComm
+import com.instructure.student.service.StudentPageViewService
+import com.instructure.student.tasks.StudentLogoutTask
+import com.pspdfkit.PSPDFKit
+import com.pspdfkit.exceptions.InvalidPSPDFKitLicenseException
+import com.pspdfkit.exceptions.PSPDFKitInitializationFailedException
+import dagger.hilt.EntryPoint
+import dagger.hilt.EntryPoints
+import dagger.hilt.InstallIn
+import dagger.hilt.components.SingletonComponent
+import io.flutter.embedding.engine.FlutterEngine
+import io.flutter.embedding.engine.FlutterEngineCache
+import io.flutter.embedding.engine.dart.DartExecutor
+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.
+ return
+ }
+ super.onCreate()
+
+ // Call it superstition, but I don't trust BuildConfig flags to be set correctly
+ // in library builds. IS_TESTING, for example, does not percolate down to libraries
+ // correctly. So I'm reading/setting these user properties here instead of canvasapi2/AppManager.
+ Analytics.setUserProperty(AnalyticsEventConstants.USER_PROPERTY_BUILD_TYPE, if(BuildConfig.DEBUG) "debug" else "release")
+ Analytics.setUserProperty(AnalyticsEventConstants.USER_PROPERTY_OS_VERSION, Build.VERSION.SDK_INT.toString())
+
+ // Hold off on initializing this until we emit the user properties.
+ RemoteConfigUtils.initialize()
+
+ initPSPDFKit()
+
+ if (BuildConfig.DEBUG) {
+ FirebaseCrashlytics.getInstance().setCrashlyticsCollectionEnabled(false)
+ } else {
+ FirebaseCrashlytics.getInstance().setCrashlyticsCollectionEnabled(true)
+ }
+
+ ColorKeeper.defaultColor = ContextCompat.getColor(this, R.color.defaultPrimary)
+
+ // There appears to be a bug when the user is installing/updating the android webview stuff.
+ // http://code.google.com/p/android/issues/detail?id=175124
+ try {
+ WebView.setWebContentsDebuggingEnabled(true)
+ } catch (e: Exception) {
+ FirebaseCrashlytics.getInstance().log("Exception trying to setWebContentsDebuggingEnabled")
+ }
+
+ PageViewUploadService.schedule(this, StudentPageViewService::class.java)
+
+ initFlutterEngine()
+ }
+
+ private fun initFlutterEngine() {
+ flutterEngine = FlutterEngine(this)
+
+ FlutterComm.init(flutterEngine, applicationContext)
+
+ // Execute the 'main' entrypoint
+ flutterEngine.dartExecutor.executeDartEntrypoint(DartExecutor.DartEntrypoint.createDefault())
+
+ // Cache the FlutterEngine
+ FlutterEngineCache.getInstance().put(FLUTTER_ENGINE_ID, flutterEngine)
+ }
+
+ 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() {
+ try {
+ PSPDFKit.initialize(this, BuildConfig.PSPDFKIT_LICENSE_KEY)
+ } catch (e: PSPDFKitInitializationFailedException) {
+ Logger.e("Current device is not compatible with PSPDFKIT!")
+ } catch (e: InvalidPSPDFKitLicenseException) {
+ Logger.e("Invalid or Trial PSPDFKIT License!")
+ }
+ }
+
+ override fun performLogoutOnAuthError() = Unit
+
+ companion object {
+ private const val FLUTTER_ENGINE_ID = "flutter_engine_embed"
+
+ lateinit var flutterEngine: FlutterEngine
+ }
+}
\ No newline at end of file
diff --git a/apps/student/src/main/java/com/instructure/student/util/DefaultAppShortcutManager.kt b/apps/student/src/main/java/com/instructure/student/util/DefaultAppShortcutManager.kt
new file mode 100644
index 0000000000..0d100a26d4
--- /dev/null
+++ b/apps/student/src/main/java/com/instructure/student/util/DefaultAppShortcutManager.kt
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2021 - 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.content.Context
+import android.content.Intent
+import android.content.pm.ShortcutInfo
+import android.content.pm.ShortcutManager
+import android.graphics.drawable.Icon
+import android.os.Build
+import com.instructure.student.R
+import com.instructure.student.activity.LoginActivity
+import java.util.*
+
+class DefaultAppShortcutManager : AppShortcutManager {
+
+ override fun make(context: Context) {
+ if (Build.VERSION.SDK_INT < 25) return
+
+ val manager = context.getSystemService(ShortcutManager::class.java)
+
+ val bookmarksIntent = Intent(context, LoginActivity::class.java)
+ bookmarksIntent.action = AppShortcutManager.ACTION_APP_SHORTCUT
+ bookmarksIntent.putExtra(AppShortcutManager.APP_SHORTCUT_PLACEMENT, AppShortcutManager.APP_SHORTCUT_BOOKMARKS)
+ val shortcutBookmarks = createShortcut(context, AppShortcutManager.APP_SHORTCUT_BOOKMARKS,
+ context.getString(R.string.bookmarks),
+ context.getString(R.string.bookmarks),
+ R.mipmap.ic_shortcut_bookmarks,
+ bookmarksIntent)
+
+ val calendarIntent = Intent(context, LoginActivity::class.java)
+ calendarIntent.action = AppShortcutManager.ACTION_APP_SHORTCUT
+ calendarIntent.putExtra(AppShortcutManager.APP_SHORTCUT_PLACEMENT, AppShortcutManager.APP_SHORTCUT_CALENDAR)
+ val shortcutCalendar = createShortcut(context, AppShortcutManager.APP_SHORTCUT_CALENDAR,
+ context.getString(R.string.calendar),
+ context.getString(R.string.calendar),
+ R.mipmap.ic_shortcut_calendar,
+ calendarIntent)
+
+ val todoIntent = Intent(context, LoginActivity::class.java)
+ todoIntent.action = AppShortcutManager.ACTION_APP_SHORTCUT
+ todoIntent.putExtra(AppShortcutManager.APP_SHORTCUT_PLACEMENT, AppShortcutManager.APP_SHORTCUT_TODO)
+ val shortcutTodo = createShortcut(context, AppShortcutManager.APP_SHORTCUT_TODO,
+ context.getString(R.string.toDoList),
+ context.getString(R.string.toDoList),
+ R.mipmap.ic_shortcut_todo,
+ todoIntent)
+
+ val notificationsIntent = Intent(context, LoginActivity::class.java)
+ notificationsIntent.action = AppShortcutManager.ACTION_APP_SHORTCUT
+ notificationsIntent.putExtra(AppShortcutManager.APP_SHORTCUT_PLACEMENT, AppShortcutManager.APP_SHORTCUT_NOTIFICATIONS)
+ val shortcutNotifications = createShortcut(context, AppShortcutManager.APP_SHORTCUT_NOTIFICATIONS,
+ context.getString(R.string.notifications),
+ context.getString(R.string.notifications),
+ R.mipmap.ic_shortcut_notifications,
+ notificationsIntent)
+
+ manager?.dynamicShortcuts = Arrays.asList(shortcutNotifications, shortcutTodo, shortcutCalendar, shortcutBookmarks)
+ }
+
+ private fun createShortcut(context: Context, id: String, label: String, longLabel: String, iconResId: Int, intent: Intent): ShortcutInfo {
+ return ShortcutInfo.Builder(context, id)
+ .setShortLabel(label)
+ .setLongLabel(longLabel)
+ .setIcon(Icon.createWithResource(context, iconResId))
+ .setIntent(intent)
+ .build()
+ }
+}
\ No newline at end of file
diff --git a/apps/student/src/main/java/com/instructure/student/util/FeatureFlagPrefs.kt b/apps/student/src/main/java/com/instructure/student/util/FeatureFlagPrefs.kt
index 0074090c23..251ba56c6a 100644
--- a/apps/student/src/main/java/com/instructure/student/util/FeatureFlagPrefs.kt
+++ b/apps/student/src/main/java/com/instructure/student/util/FeatureFlagPrefs.kt
@@ -18,4 +18,5 @@ package com.instructure.student.util
import com.instructure.canvasapi2.utils.PrefManager
object FeatureFlagPrefs : PrefManager("feature_flags") {
+
}
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 4c48671db4..468dafb8f7 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
@@ -124,7 +124,7 @@ class FileDownloadJobIntentService : JobIntentService() {
try {
val okHttp = OkHttpClient.Builder().build()
val request = Request.Builder().url(fileUrl).build()
- val source = okHttp.newCall(request).execute().body()?.source() ?: return DownloadFailed()
+ val source = okHttp.newCall(request).execute().body?.source() ?: return DownloadFailed()
val sink = downloadedFile.sink().buffer()
var startTime = System.currentTimeMillis()
diff --git a/apps/student/src/main/java/com/instructure/student/view/CanvasLoading.kt b/apps/student/src/main/java/com/instructure/student/view/CanvasLoading.kt
deleted file mode 100644
index 644d522be3..0000000000
--- a/apps/student/src/main/java/com/instructure/student/view/CanvasLoading.kt
+++ /dev/null
@@ -1,40 +0,0 @@
-/*
- * Copyright (C) 2017 - present Instructure, Inc.
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, version 3 of the License.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
- *
- */
-package com.instructure.student.view
-
-import android.content.Context
-import android.util.AttributeSet
-import android.view.LayoutInflater
-import android.widget.FrameLayout
-import com.instructure.student.R
-import com.instructure.pandautils.utils.setVisible
-import kotlinx.android.synthetic.main.canvas_loading.view.*
-
-class CanvasLoading @JvmOverloads constructor(
- context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
-) : FrameLayout(context, attrs, defStyleAttr) {
-
- init {
- LayoutInflater.from(context).inflate(R.layout.canvas_loading, this)
- }
-
- fun displayNoConnection(isNoConnection: Boolean) {
- noConnectionView.setVisible(isNoConnection)
- loadingView.setVisible(!isNoConnection)
- }
-
-}
diff --git a/apps/student/src/main/java/com/instructure/student/view/EmptyInboxView.kt b/apps/student/src/main/java/com/instructure/student/view/EmptyInboxView.kt
index f94a9780fa..1f84da35cf 100644
--- a/apps/student/src/main/java/com/instructure/student/view/EmptyInboxView.kt
+++ b/apps/student/src/main/java/com/instructure/student/view/EmptyInboxView.kt
@@ -14,8 +14,7 @@ import com.instructure.student.R
import com.instructure.pandarecycler.interfaces.EmptyInterface
import com.instructure.pandautils.utils.setGone
import com.instructure.pandautils.utils.setVisible
-import kotlinx.android.synthetic.main.empty_view.view.*
-import kotlinx.android.synthetic.main.loading_lame.view.*
+import kotlinx.android.synthetic.main.empty_inbox_view.view.*
class EmptyInboxView @JvmOverloads constructor(
context: Context,
@@ -46,7 +45,8 @@ class EmptyInboxView @JvmOverloads constructor(
override fun setListEmpty() {
if (isDisplayNoConnection) {
- noConnection.text = noConnectionText
+ // TODO We can move this also to commons later, and than we can use the synthetic properties.
+ findViewById(R.id.noConnection).text = noConnectionText
} else {
title.text = titleText
message.text = messageText
@@ -87,7 +87,7 @@ class EmptyInboxView @JvmOverloads constructor(
override fun setNoConnectionText(s: String) {
noConnectionText = s
- noConnection.text = noConnectionText
+ findViewById(R.id.noConnection).text = noConnectionText
}
override fun getEmptyViewImage(): ImageView? = image
diff --git a/apps/student/src/main/res/layout-sw720dp/fragment_elementary_dashboard.xml b/apps/student/src/main/res/layout-sw720dp/fragment_elementary_dashboard.xml
new file mode 100644
index 0000000000..3c596cc38e
--- /dev/null
+++ b/apps/student/src/main/res/layout-sw720dp/fragment_elementary_dashboard.xml
@@ -0,0 +1,121 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/apps/student/src/main/res/layout/activity_navigation.xml b/apps/student/src/main/res/layout/activity_navigation.xml
index 7ccd6dea5e..6423d17983 100644
--- a/apps/student/src/main/res/layout/activity_navigation.xml
+++ b/apps/student/src/main/res/layout/activity_navigation.xml
@@ -58,8 +58,7 @@
android:layout_height="wrap_content"
android:background="@color/white"
app:elevation="0dp"
- app:labelVisibilityMode="labeled"
- app:menu="@menu/bottom_bar_menu"/>
+ app:labelVisibilityMode="labeled" />
diff --git a/apps/student/src/main/res/layout/assignment_list_layout.xml b/apps/student/src/main/res/layout/assignment_list_layout.xml
index 648ca2820e..8608a402e7 100644
--- a/apps/student/src/main/res/layout/assignment_list_layout.xml
+++ b/apps/student/src/main/res/layout/assignment_list_layout.xml
@@ -36,28 +36,63 @@
android:layout_height="?android:actionBarSize"
android:layout_alignParentTop="true"
android:background="@color/defaultPrimary"
- android:elevation="6dp"
app:popupTheme="@style/ToolBarPopupStyle"
app:theme="@style/ToolBarStyle"
tools:targetApi="lollipop" />
+
+
+
+
+
+
+
+
+ android:layout_centerVertical="true"
+ android:minWidth="48dp"
+ android:minHeight="48dp"
+ android:layout_toStartOf="@id/sortByButton" />
@@ -78,7 +113,7 @@
-
-
-
-
-
-
-
-
-
diff --git a/apps/student/src/main/res/layout/course_discussion_topic.xml b/apps/student/src/main/res/layout/course_discussion_topic.xml
index 51094c1405..cc09febe77 100644
--- a/apps/student/src/main/res/layout/course_discussion_topic.xml
+++ b/apps/student/src/main/res/layout/course_discussion_topic.xml
@@ -52,7 +52,7 @@
-
diff --git a/apps/student/src/main/res/layout/dialog_canvas_context_list.xml b/apps/student/src/main/res/layout/dialog_canvas_context_list.xml
index 12175695c4..d4c270a87d 100644
--- a/apps/student/src/main/res/layout/dialog_canvas_context_list.xml
+++ b/apps/student/src/main/res/layout/dialog_canvas_context_list.xml
@@ -26,7 +26,7 @@
-
diff --git a/apps/student/src/main/res/layout/dialog_submission_picker.xml b/apps/student/src/main/res/layout/dialog_submission_picker.xml
index 159b947a8a..0e924df377 100644
--- a/apps/student/src/main/res/layout/dialog_submission_picker.xml
+++ b/apps/student/src/main/res/layout/dialog_submission_picker.xml
@@ -121,4 +121,24 @@
+
+
+
+
+
+
+
+
diff --git a/apps/student/src/main/res/layout/fragment_all_courses.xml b/apps/student/src/main/res/layout/fragment_all_courses.xml
deleted file mode 100644
index e43f966135..0000000000
--- a/apps/student/src/main/res/layout/fragment_all_courses.xml
+++ /dev/null
@@ -1,46 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/apps/student/src/main/res/layout/fragment_application_settings.xml b/apps/student/src/main/res/layout/fragment_application_settings.xml
index 4dc4317713..9e3885f29a 100644
--- a/apps/student/src/main/res/layout/fragment_application_settings.xml
+++ b/apps/student/src/main/res/layout/fragment_application_settings.xml
@@ -48,6 +48,54 @@
android:animateLayoutChanges="true"
android:orientation="vertical">
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
@@ -32,18 +32,21 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:elevation="6dp"
+ app:layout_constraintTop_toTopOf="parent"
tools:background="#00bcd5"
tools:navigationIcon="@drawable/ic_back_arrow"
tools:subtitle="Biology 101"
tools:subtitleTextColor="@color/white"
tools:title="Ecosystem Health and Human Well-Being"
- tools:titleTextColor="@color/white"/>
+ tools:titleTextColor="@color/white" />
+ android:layout_weight="1"
+ app:layout_constraintTop_toBottomOf="@id/toolbar"
+ app:layout_constraintBottom_toTopOf="@+id/submitButton">
+ android:tint="@color/defaultTextGray" />
+ android:text="@string/errorLoadingAssignment" />
@@ -101,9 +104,9 @@
style="@style/TextFont.Medium"
android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:textSize="20sp"
android:accessibilityHeading="true"
- tools:text="Ecosystem Health and Human Well-Being"/>
+ android:textSize="20sp"
+ tools:text="Ecosystem Health and Human Well-Being" />
+ tools:text="30 pts" />
+ tools:tint="@color/alertGreen" />
+ tools:textColor="#00ac18" />
@@ -156,12 +159,12 @@
android:id="@+id/submissionStatusUploading"
android:layout_width="match_parent"
android:layout_height="match_parent"
+ android:background="?attr/selectableItemBackground"
android:clickable="true"
android:focusable="true"
android:gravity="center"
android:orientation="vertical"
android:padding="16dp"
- android:background="?attr/selectableItemBackground"
android:visibility="gone"
tools:visibility="visible">
@@ -172,7 +175,7 @@
android:gravity="center"
android:text="@string/submissionStatusUploadingTitle"
android:textColor="@color/defaultTextDark"
- android:textSize="20sp"/>
+ android:textSize="20sp" />
+ android:textColor="@color/canvasDefaultAccent" />
@@ -190,12 +193,12 @@
android:id="@+id/submissionStatusFailed"
android:layout_width="match_parent"
android:layout_height="match_parent"
+ android:background="?attr/selectableItemBackground"
android:clickable="true"
android:focusable="true"
android:gravity="center"
android:orientation="vertical"
android:padding="16dp"
- android:background="?attr/selectableItemBackground"
android:visibility="gone">
+ android:textSize="20sp" />
+ android:textColor="@color/canvasDefaultAccent" />
@@ -227,8 +230,8 @@
android:id="@+id/dueDateContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:orientation="vertical"
android:accessibilityHeading="true"
+ android:orientation="vertical"
android:padding="16dp">
+ android:textColor="@color/defaultTextGray" />
+ tools:text="April 1 at 11:59 PM" />
@@ -256,8 +259,8 @@
android:id="@+id/submissionTypesContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:orientation="vertical"
android:accessibilityHeading="true"
+ android:orientation="vertical"
android:padding="16dp">
+ android:textColor="@color/defaultTextGray" />
+ tools:text="File upload, Website URL" />
@@ -286,8 +289,8 @@
android:id="@+id/fileTypesContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:orientation="vertical"
android:accessibilityHeading="true"
+ android:orientation="vertical"
android:padding="16dp">
+ android:textColor="@color/defaultTextGray" />
+ tools:text="png, pdf, xls, SVG" />
@@ -389,9 +392,9 @@
android:id="@+id/gradeContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
+ android:accessibilityHeading="true"
android:background="?attr/selectableItemBackground"
android:orientation="vertical"
- android:accessibilityHeading="true"
android:padding="16dp">
+ android:textColor="@color/defaultTextGray" />
+ android:layout_marginTop="8dp" />
@@ -425,7 +428,7 @@
android:layout_height="wrap_content"
android:layout_marginBottom="4dp"
android:text="@string/locked"
- android:textColor="@color/defaultTextGray"/>
+ android:textColor="@color/defaultTextGray" />
+ tools:text="April 1 at 11:59 PM" />
@@ -489,7 +492,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:importantForAccessibility="no"
- android:src="@drawable/ic_panda_locked"/>
+ android:src="@drawable/ic_panda_locked" />
@@ -499,10 +502,10 @@
android:id="@+id/quizDetails"
android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:visibility="gone"
+ android:accessibilityHeading="true"
android:orientation="vertical"
android:padding="16dp"
- android:accessibilityHeading="true"
+ android:visibility="gone"
tools:visibility="visible">
@@ -515,23 +518,23 @@
+ android:textStyle="bold"
+ android:tint="@color/defaultTextGray" />
+ tools:text="5" />
@@ -544,23 +547,23 @@
+ android:textStyle="bold"
+ android:tint="@color/defaultTextGray" />
+ tools:text="None" />
@@ -572,23 +575,23 @@
+ android:textStyle="bold"
+ android:tint="@color/defaultTextGray" />
+ tools:text="Unlimited" />
@@ -613,7 +616,7 @@
android:layout_marginEnd="16dp"
android:labelFor="@+id/noDescriptionContainer"
android:text="@string/description"
- android:textColor="@color/defaultTextGray"/>
+ android:textColor="@color/defaultTextGray" />
+ tools:visibility="visible" />
@@ -639,8 +642,8 @@
android:layout_marginStart="16dp"
android:layout_marginTop="10dp"
android:layout_marginEnd="16dp"
- android:visibility="gone"
android:orientation="horizontal"
+ android:visibility="gone"
tools:visibility="visible">
+ android:focusableInTouchMode="false"
+ android:minHeight="48dp"
+ android:scrollbars="none" />
@@ -716,20 +719,21 @@
android:id="@+id/submitButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:foreground="?attr/selectableItemBackground"
android:background="@color/canvasDefaultAccent"
+ android:foreground="?attr/selectableItemBackground"
android:text="@string/submitAssignment"
android:textAllCaps="false"
- android:textColor="@color/white"/>
+ android:textColor="@color/white"
+ app:layout_constraintBottom_toBottomOf="parent" />
-
+
+ android:layout_height="wrap_content"
+ android:layout_gravity="bottom"
+ android:elevation="12dp"
+ android:visibility="gone" />
diff --git a/apps/student/src/main/res/layout/fragment_course_browser.xml b/apps/student/src/main/res/layout/fragment_course_browser.xml
index 7fbfa14072..6046aae2d8 100644
--- a/apps/student/src/main/res/layout/fragment_course_browser.xml
+++ b/apps/student/src/main/res/layout/fragment_course_browser.xml
@@ -144,7 +144,7 @@
-
-
diff --git a/apps/student/src/main/res/layout/fragment_create_announcement.xml b/apps/student/src/main/res/layout/fragment_create_announcement.xml
index 9c70c62ac4..9b437bc25a 100644
--- a/apps/student/src/main/res/layout/fragment_create_announcement.xml
+++ b/apps/student/src/main/res/layout/fragment_create_announcement.xml
@@ -93,7 +93,8 @@
android:layout_marginStart="10dp"
android:layout_marginTop="20dp"
android:text="@string/utils_description"
- android:textColor="@color/defaultTextGray"/>
+ android:textColor="@color/defaultTextGray"
+ android:focusable="true"/>
+ android:textColor="@color/defaultTextGray"
+ android:focusable="true" />
+ android:orientation="vertical">
+
+
+ android:layout_marginTop="4dp"
+ android:minHeight="48dp" />
@@ -334,9 +349,9 @@
style="@style/TextFont.Medium"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:layout_marginEnd="16dp"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
+ android:layout_marginEnd="16dp"
android:gravity="center_vertical|start"
android:minHeight="42dp"
android:text="@string/utils_discussionsReplies_Title"
@@ -346,15 +361,15 @@
+ tools:visibility="visible" />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/apps/student/src/main/res/layout/fragment_edit_page.xml b/apps/student/src/main/res/layout/fragment_edit_page.xml
index 7c5c9dd445..7b4b5e6916 100644
--- a/apps/student/src/main/res/layout/fragment_edit_page.xml
+++ b/apps/student/src/main/res/layout/fragment_edit_page.xml
@@ -60,7 +60,8 @@
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:text="@string/description"
- android:textColor="@color/defaultTextGray"/>
+ android:textColor="@color/defaultTextGray"
+ android:focusable="true"/>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/apps/student/src/main/res/layout/fragment_favoriting.xml b/apps/student/src/main/res/layout/fragment_favoriting.xml
deleted file mode 100644
index f0ec14cda8..0000000000
--- a/apps/student/src/main/res/layout/fragment_favoriting.xml
+++ /dev/null
@@ -1,54 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/apps/student/src/main/res/layout/fragment_file_list.xml b/apps/student/src/main/res/layout/fragment_file_list.xml
index a2f2f21083..91c68e11f9 100644
--- a/apps/student/src/main/res/layout/fragment_file_list.xml
+++ b/apps/student/src/main/res/layout/fragment_file_list.xml
@@ -63,7 +63,7 @@
-
@@ -80,6 +80,7 @@
android:contentDescription="@string/createAFolder"
android:visibility="invisible"
android:layout_marginEnd="16dp"
+ android:accessibilityTraversalAfter="@id/addFileFab"
app:elevation="4dp"
app:srcCompat="@drawable/ic_files" />
@@ -95,6 +96,7 @@
android:contentDescription="@string/createAFile"
android:visibility="invisible"
android:layout_marginEnd="16dp"
+ android:accessibilityTraversalAfter="@id/addFab"
app:elevation="4dp"
app:srcCompat="@drawable/ic_file_upload" />
diff --git a/apps/student/src/main/res/layout/fragment_file_search.xml b/apps/student/src/main/res/layout/fragment_file_search.xml
index 075688d684..d736bf7ef4 100644
--- a/apps/student/src/main/res/layout/fragment_file_search.xml
+++ b/apps/student/src/main/res/layout/fragment_file_search.xml
@@ -92,7 +92,7 @@
android:layout_width="match_parent"
android:layout_height="match_parent"/>
-
diff --git a/apps/student/src/main/res/layout/fragment_inbox_compose_message.xml b/apps/student/src/main/res/layout/fragment_inbox_compose_message.xml
index 2dce8df43e..6c5b7549c0 100644
--- a/apps/student/src/main/res/layout/fragment_inbox_compose_message.xml
+++ b/apps/student/src/main/res/layout/fragment_inbox_compose_message.xml
@@ -67,6 +67,7 @@
android:layout_alignParentStart="true"
android:layout_centerVertical="true"
android:layout_marginEnd="8dp"
+ android:contentDescription="@string/selectCourse"
android:minHeight="48dp"/>
@@ -143,6 +144,8 @@
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:ellipsize="end"
+ android:importantForAccessibility="yes"
+ android:contentDescription="@string/a11y_content_description_inbox_subject"
android:hint="@string/noSubject"
android:lines="1"
android:padding="16dp"
diff --git a/apps/student/src/main/res/layout/fragment_inbox_recipients.xml b/apps/student/src/main/res/layout/fragment_inbox_recipients.xml
index 20ed9563c9..a0b067b733 100644
--- a/apps/student/src/main/res/layout/fragment_inbox_recipients.xml
+++ b/apps/student/src/main/res/layout/fragment_inbox_recipients.xml
@@ -42,7 +42,7 @@
-
diff --git a/apps/student/src/main/res/layout/fragment_mastery_paths_options.xml b/apps/student/src/main/res/layout/fragment_mastery_paths_options.xml
index 5b8d73840d..274af70006 100644
--- a/apps/student/src/main/res/layout/fragment_mastery_paths_options.xml
+++ b/apps/student/src/main/res/layout/fragment_mastery_paths_options.xml
@@ -37,7 +37,7 @@
-
-
+ android:layout_marginBottom="16dp"
+ android:layout_marginEnd="16dp" />
diff --git a/apps/student/src/main/res/layout/fragment_picker_submission_upload.xml b/apps/student/src/main/res/layout/fragment_picker_submission_upload.xml
index ec5c841ffc..f6f30127ed 100644
--- a/apps/student/src/main/res/layout/fragment_picker_submission_upload.xml
+++ b/apps/student/src/main/res/layout/fragment_picker_submission_upload.xml
@@ -42,7 +42,7 @@
app:layout_constraintTop_toBottomOf="@id/toolbar"
tools:listitem="@layout/viewholder_file_upload"/>
-
-
\ No newline at end of file
diff --git a/apps/student/src/main/res/layout/fragment_unsupported_feature.xml b/apps/student/src/main/res/layout/fragment_unsupported_feature.xml
index 4ec7c9a460..54cce10f9d 100644
--- a/apps/student/src/main/res/layout/fragment_unsupported_feature.xml
+++ b/apps/student/src/main/res/layout/fragment_unsupported_feature.xml
@@ -42,6 +42,7 @@
android:layout_marginTop="32dp"
android:layout_marginEnd="24dp"
android:layout_marginStart="24dp"
+ android:gravity="center_horizontal"
android:layout_gravity="center_horizontal"/>