diff --git a/PULL_REQUEST_TEMPLATE b/PULL_REQUEST_TEMPLATE
new file mode 100644
index 0000000000..b56e60264e
--- /dev/null
+++ b/PULL_REQUEST_TEMPLATE
@@ -0,0 +1,16 @@
+--- edit or delete this section ---
+## Screenshots
+
+
+Before | After |
+
+ |
+ |
+
+
+
+## Checklist
+
+- [ ] Follow-up e2e test ticket created or not needed
+- [ ] A11y checked
+- [ ] Approve from product or not needed
diff --git a/apps/flutter_parent/android/app/build.gradle b/apps/flutter_parent/android/app/build.gradle
index ea3bc8685a..532b872ab9 100644
--- a/apps/flutter_parent/android/app/build.gradle
+++ b/apps/flutter_parent/android/app/build.gradle
@@ -78,6 +78,19 @@ android {
shrinkResources false // Must be false, otherwise resources we need are erroneously stripped out
proguardFiles 'proguard-rules.pro'
}
+ applicationVariants.all{
+ variant ->
+ variant.outputs.each{
+ output->
+ project.ext { appName = 'parent' }
+ def dateTimeStamp = new Date().format('yyyy-MM-dd-HH-mm-ss')
+ def newName = output.outputFile.name
+ newName = newName.replace("app-", "$project.ext.appName-")
+ newName = newName.replace("-debug", "-dev-debug-" + dateTimeStamp)
+ newName = newName.replace("-release", "-prod-release")
+ output.outputFileName = newName
+ }
+ }
}
}
diff --git a/apps/flutter_parent/android/app/src/main/AndroidManifest.xml b/apps/flutter_parent/android/app/src/main/AndroidManifest.xml
index eb3ec7352a..88c5a4cae2 100644
--- a/apps/flutter_parent/android/app/src/main/AndroidManifest.xml
+++ b/apps/flutter_parent/android/app/src/main/AndroidManifest.xml
@@ -31,7 +31,8 @@
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
- android:windowSoftInputMode="adjustResize">
+ android:windowSoftInputMode="adjustResize"
+ android:exported="true">
diff --git a/apps/flutter_parent/lib/network/api/enrollments_api.dart b/apps/flutter_parent/lib/network/api/enrollments_api.dart
index 848d579e30..c373b780fe 100644
--- a/apps/flutter_parent/lib/network/api/enrollments_api.dart
+++ b/apps/flutter_parent/lib/network/api/enrollments_api.dart
@@ -41,7 +41,7 @@ class EnrollmentsApi {
final dio = canvasDio(forceRefresh: forceRefresh);
final params = {
'state[]': ['active', 'completed'], // current_and_concluded state not supported for observers
- //'user_id': studentId, <-- add this back when the api is fixed
+ 'user_id': studentId,
if (gradingPeriodId?.isNotEmpty == true)
'grading_period_id': gradingPeriodId,
};
diff --git a/apps/flutter_parent/lib/network/utils/analytics.dart b/apps/flutter_parent/lib/network/utils/analytics.dart
index 4e92501eba..5564eb8147 100644
--- a/apps/flutter_parent/lib/network/utils/analytics.dart
+++ b/apps/flutter_parent/lib/network/utils/analytics.dart
@@ -12,7 +12,6 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
import 'package:device_info/device_info.dart';
-import 'package:firebase_analytics/firebase_analytics.dart';
import 'package:firebase_crashlytics/firebase_crashlytics.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_parent/utils/debug_flags.dart';
@@ -79,12 +78,11 @@ class AnalyticsParamConstants {
}
class Analytics {
- FirebaseAnalytics get _analytics => FirebaseAnalytics();
- /// Set the current screen in Firebase Analytics
+ /// Set the current screen in analytics
void setCurrentScreen(String screenName) async {
if (kReleaseMode) {
- await _analytics.setCurrentScreen(screenName: screenName);
+
}
if (DebugFlags.isDebug) {
@@ -92,7 +90,7 @@ class Analytics {
}
}
- /// Log an event to Firebase analytics (only in release mode).
+ /// Log an event to analytics (only in release mode).
/// If isDebug, it will also print to the console
///
/// Params
@@ -100,7 +98,7 @@ class Analytics {
/// * [extras] a map of keys [AnalyticsParamConstants] to values. Use sparingly, we only get 25 unique parameters
void logEvent(String event, {Map extras = const {}}) async {
if (kReleaseMode) {
- await _analytics.logEvent(name: event, parameters: extras);
+
}
if (DebugFlags.isDebug) {
@@ -122,14 +120,6 @@ class Analytics {
/// Sets environment properties such as the build type and SDK int. This only needs to be called once per session.
void setEnvironmentProperties() async {
- var androidInfo = await DeviceInfoPlugin().androidInfo;
- await _analytics.setUserProperty(
- name: AnalyticsEventConstants.USER_PROPERTY_BUILD_TYPE,
- value: kReleaseMode ? 'release' : 'debug',
- );
- await _analytics.setUserProperty(
- name: AnalyticsEventConstants.USER_PROPERTY_OS_VERSION,
- value: androidInfo.version.sdkInt.toString(),
- );
+
}
}
diff --git a/apps/flutter_parent/lib/screens/courses/details/course_details_model.dart b/apps/flutter_parent/lib/screens/courses/details/course_details_model.dart
index 163457ffcb..60eea98f46 100644
--- a/apps/flutter_parent/lib/screens/courses/details/course_details_model.dart
+++ b/apps/flutter_parent/lib/screens/courses/details/course_details_model.dart
@@ -98,8 +98,6 @@ class CourseDetailsModel extends BaseModel {
final enrollmentsFuture = _interactor()
.loadEnrollmentsForGradingPeriod(courseId, student.id, _nextGradingPeriod?.id, forceRefresh: forceRefresh)
?.then((enrollments) {
- enrollments = enrollments
- .where((element) => element.userId == student.id).toList();
return enrollments.length > 0 ? enrollments.first : null;
})?.catchError((_) => null); // Some 'legacy' parents can't read grades for students, so catch and return null
diff --git a/apps/flutter_parent/lib/utils/crash_utils.dart b/apps/flutter_parent/lib/utils/crash_utils.dart
index 1d75550be9..d3dfb4e50f 100644
--- a/apps/flutter_parent/lib/utils/crash_utils.dart
+++ b/apps/flutter_parent/lib/utils/crash_utils.dart
@@ -25,8 +25,8 @@ class CrashUtils {
FirebaseCrashlytics firebase = locator();
FlutterError.onError = (error) async {
- await firebase
- .setUserIdentifier('domain: ${ApiPrefs.getDomain() ?? 'null'} user_id: ${ApiPrefs.getUser()?.id ?? 'null'}');
+ // We don't know how the crashlytics stores the userId so we just set it to empty to make sure we don't log it.
+ await firebase.setUserIdentifier('');
firebase.recordFlutterError(error);
};
diff --git a/apps/flutter_parent/pubspec.lock b/apps/flutter_parent/pubspec.lock
index 3def51903f..fc0fa4e97d 100644
--- a/apps/flutter_parent/pubspec.lock
+++ b/apps/flutter_parent/pubspec.lock
@@ -337,34 +337,6 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "4.2.0"
- firebase:
- dependency: transitive
- description:
- name: firebase
- url: "https://pub.dartlang.org"
- source: hosted
- version: "9.0.2"
- firebase_analytics:
- dependency: "direct main"
- description:
- name: firebase_analytics
- url: "https://pub.dartlang.org"
- source: hosted
- version: "8.3.4"
- firebase_analytics_platform_interface:
- dependency: transitive
- description:
- name: firebase_analytics_platform_interface
- url: "https://pub.dartlang.org"
- source: hosted
- version: "2.0.1"
- firebase_analytics_web:
- dependency: transitive
- description:
- name: firebase_analytics_web
- url: "https://pub.dartlang.org"
- source: hosted
- version: "0.3.0+1"
firebase_core:
dependency: "direct main"
description:
diff --git a/apps/flutter_parent/pubspec.yaml b/apps/flutter_parent/pubspec.yaml
index 98699a76f3..0b2485a37e 100644
--- a/apps/flutter_parent/pubspec.yaml
+++ b/apps/flutter_parent/pubspec.yaml
@@ -39,7 +39,6 @@ dependencies:
sdk: flutter
flutter_localizations:
sdk: flutter
- firebase_analytics: ^8.3.4
firebase_remote_config: ^0.11.0+2
firebase_core: ^1.8.0
firebase_crashlytics: ^2.2.4
diff --git a/apps/flutter_parent/test/screens/courses/course_details_model_test.dart b/apps/flutter_parent/test/screens/courses/course_details_model_test.dart
index 34749806dd..3285c6bf1f 100644
--- a/apps/flutter_parent/test/screens/courses/course_details_model_test.dart
+++ b/apps/flutter_parent/test/screens/courses/course_details_model_test.dart
@@ -150,8 +150,7 @@ void main() {
// Initial setup
final termEnrollment = Enrollment((b) => b
..id = '10'
- ..enrollmentState = 'active'
- ..userId = _studentId);
+ ..enrollmentState = 'active');
final gradingPeriods = [
GradingPeriod((b) => b
..id = '123'
diff --git a/apps/flutter_parent/test/screens/courses/course_grades_screen_test.dart b/apps/flutter_parent/test/screens/courses/course_grades_screen_test.dart
index db0ae0ef27..7e03ef1cc9 100644
--- a/apps/flutter_parent/test/screens/courses/course_grades_screen_test.dart
+++ b/apps/flutter_parent/test/screens/courses/course_grades_screen_test.dart
@@ -326,8 +326,7 @@ void main() {
];
final enrollment = Enrollment((b) => b
..enrollmentState = 'active'
- ..grades = _mockGrade(currentScore: 1.2345)
- ..userId = _studentId);
+ ..grades = _mockGrade(currentScore: 1.2345));
final model = CourseDetailsModel(_student, _courseId);
model.course = _mockCourse();
@@ -351,8 +350,7 @@ void main() {
];
final enrollment = Enrollment((b) => b
..enrollmentState = 'active'
- ..grades = _mockGrade(currentGrade: grade)
- ..userId = _studentId);
+ ..grades = _mockGrade(currentGrade: grade));
final model = CourseDetailsModel(_student, _courseId);
model.course = _mockCourse();
when(interactor.loadAssignmentGroups(_courseId, _studentId, null)).thenAnswer((_) async => groups);
diff --git a/apps/student/build.gradle b/apps/student/build.gradle
index 525fcedb68..10be6c70f8 100644
--- a/apps/student/build.gradle
+++ b/apps/student/build.gradle
@@ -54,8 +54,8 @@ android {
applicationId "com.instructure.candroid"
minSdkVersion Versions.MIN_SDK
targetSdkVersion Versions.TARGET_SDK
- versionCode = 242
- versionName = '6.19.0'
+ versionCode = 243
+ versionName = '6.20.0'
vectorDrawables.useSupportLibrary = true
multiDexEnabled = true
@@ -271,12 +271,11 @@ dependencies {
testImplementation Libs.ANDROIDX_CORE_TESTING
/* Firebase */
- implementation platform(Libs.FIREBASE_BOM)
+ implementation platform(Libs.FIREBASE_BOM) {
+ exclude group: 'com.google.firebase', module: 'firebase-analytics'
+ }
implementation Libs.FIREBASE_MESSAGING
implementation Libs.FIREBASE_CRASHLYTICS_NDK
- implementation (Libs.FIREBASE_ANALYTICS) {
- transitive = true
- }
implementation (Libs.FIREBASE_CRASHLYTICS) {
transitive = true
}
@@ -308,7 +307,6 @@ dependencies {
implementation Libs.ANDROIDX_CONSTRAINT_LAYOUT
implementation Libs.ANDROIDX_DESIGN
implementation Libs.ANDROIDX_RECYCLERVIEW
- implementation Libs.PLAY_SERVICES_ANALYTICS
implementation Libs.ANDROIDX_PALETTE
implementation Libs.PLAY_CORE
@@ -332,6 +330,10 @@ dependencies {
kaptAndroidTestQa Libs.HILT_TESTING_COMPILER
androidTestImplementation Libs.UI_AUTOMATOR
+
+ /* WorkManager */
+ implementation Libs.ANDROIDX_WORK_MANAGER
+ implementation Libs.ANDROIDX_WORK_MANAGER_KTX
}
// Comment out this line if the reporting logic starts going wonky.
diff --git a/apps/student/flank_e2e_lowres.yml b/apps/student/flank_e2e_lowres.yml
new file mode 100644
index 0000000000..d7862027cb
--- /dev/null
+++ b/apps/student/flank_e2e_lowres.yml
@@ -0,0 +1,26 @@
+gcloud:
+ project: delta-essence-114723
+# Use the next two lines to run locally
+# app: ./build/outputs/apk/qa/debug/student-qa-debug.apk
+# test: ./build/outputs/apk/androidTest/qa/debug/student-qa-debug-androidTest.apk
+ app: ./apps/student/build/outputs/apk/qa/debug/student-qa-debug.apk
+ test: ./apps/student/build/outputs/apk/androidTest/qa/debug/student-qa-debug-androidTest.apk
+ results-bucket: android-student
+ auto-google-login: true
+ use-orchestrator: true
+ performance-metrics: false
+ record-video: true
+ timeout: 60m
+ test-targets:
+ - annotation com.instructure.canvas.espresso.E2E
+ - notAnnotation com.instructure.canvas.espresso.Stub, com.instructure.canvas.espresso.FlakyE2E, com.instructure.canvas.espresso.KnownBug
+ device:
+ - model: NexusLowRes
+ version: 26
+ locale: en_US
+ orientation: portrait
+
+flank:
+ testShards: 1
+ testRuns: 1
+
diff --git a/apps/student/flank_multi_api_level.yml b/apps/student/flank_multi_api_level.yml
index cf63ab1f30..dd25260b89 100644
--- a/apps/student/flank_multi_api_level.yml
+++ b/apps/student/flank_multi_api_level.yml
@@ -12,17 +12,17 @@ gcloud:
record-video: true
timeout: 60m
test-targets:
- - notAnnotation com.instructure.canvas.espresso.E2E, com.instructure.canvas.espresso.Stub
+ - notAnnotation com.instructure.canvas.espresso.E2E, com.instructure.canvas.espresso.Stub, com.instructure.canvas.espresso.StubMultiAPILevel, com.instructure.canvas.espresso.FlakyE2E, com.instructure.canvas.espresso.KnownBug
device:
- - model: Nexus6P
+ - model: NexusLowRes
version: 27
locale: en_US
orientation: portrait
- - model: Nexus6P
+ - model: NexusLowRes
version: 28
locale: en_US
orientation: portrait
- - model: Nexus6P
+ - model: NexusLowRes
version: 29
locale: en_US
orientation: portrait
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/AssignmentsE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/AssignmentsE2ETest.kt
index 77561fa26e..871eca2058 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/AssignmentsE2ETest.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/AssignmentsE2ETest.kt
@@ -404,13 +404,9 @@ class AssignmentsE2ETest: StudentTest() {
assignmentDetailsPage.goToSubmissionDetails()
submissionDetailsPage.openComments()
- // MBL-13604: This does not work on FTL, so we're commenting it out for now.
- // You could also break this out to a separate E2E test and annotate it with
- // @Stub, so that we can run it locally but it doesn't run as part of our CI suite.
- // send video comment
- //submissionDetailsPage.addAndSendVideoComment()
- //sleep(3000) // wait for video comment submission to propagate
- //submissionDetailsPage.assertVideoCommentDisplayed()
+ submissionDetailsPage.addAndSendVideoComment()
+ sleep(3000) // wait for video comment submission to propagate
+ submissionDetailsPage.assertVideoCommentDisplayed()
Log.d(STEP_TAG,"Send an audio comment.")
submissionDetailsPage.addAndSendAudioComment()
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/ConferencesE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/ConferencesE2ETest.kt
index a100993eaf..8beae2e7d6 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/ConferencesE2ETest.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/ConferencesE2ETest.kt
@@ -2,12 +2,12 @@ package com.instructure.student.ui.e2e
import android.util.Log
import com.instructure.canvas.espresso.E2E
-import com.instructure.canvas.espresso.Stub
+import com.instructure.canvas.espresso.refresh
+import com.instructure.dataseeding.api.ConferencesApi
import com.instructure.panda_annotations.FeatureCategory
import com.instructure.panda_annotations.Priority
import com.instructure.panda_annotations.TestCategory
import com.instructure.panda_annotations.TestMetaData
-import com.instructure.student.ui.pages.ConferencesPage
import com.instructure.student.ui.utils.StudentTest
import com.instructure.student.ui.utils.seedData
import com.instructure.student.ui.utils.tokenLogin
@@ -32,15 +32,15 @@ class ConferencesE2ETest: StudentTest() {
// Re-stubbing for now because the interface has changed from webview to native
// and this test no longer passes. MBL-14127 is being tracked to re-write this
// test against the new native interface.
- @Stub
@E2E
@Test
- @TestMetaData(Priority.MANDATORY, FeatureCategory.CONFERENCES, TestCategory.E2E, true)
+ @TestMetaData(Priority.MANDATORY, FeatureCategory.CONFERENCES, TestCategory.E2E)
fun testConferencesE2E() {
Log.d(PREPARATION_TAG,"Seeding data.")
val data = seedData(students = 1, teachers = 1, courses = 1)
val student = data.studentsList[0]
+ val teacher = data.teachersList[0]
val course = data.coursesList[0]
Log.d(STEP_TAG,"Login with user: ${student.name}, login id: ${student.loginId} , password: ${student.password}")
@@ -51,13 +51,38 @@ class ConferencesE2ETest: StudentTest() {
dashboardPage.selectCourse(course)
courseBrowserPage.selectConferences()
- val title = "Awesome Conference!"
- var description = "Awesome! Spectacular! Mind-blowing!"
- Log.d(STEP_TAG,"Create a new conference with $title title and $description description.")
- ConferencesPage.createConference(title, description)
+ Log.d(STEP_TAG,"Assert that the empty view is displayed since we did not make any conference yet.")
+ conferenceListPage.assertEmptyView()
+
+ val testConferenceTitle = "E2E test conference"
+ val testConferenceDescription = "Nightly E2E Test conference description"
+ Log.d(PREPARATION_TAG,"Create a conference with '$testConferenceTitle' title and '$testConferenceDescription' description.")
+ ConferencesApi.createCourseConference(teacher.token,
+ testConferenceTitle, testConferenceDescription,"BigBlueButton",false,70,
+ listOf(student.id),course.id)
+
+ val testConferenceTitle2 = "E2E test conference 2"
+ val testConferenceDescription2 = "Nightly E2E Test conference description 2"
+ ConferencesApi.createCourseConference(teacher.token,
+ testConferenceTitle2, testConferenceDescription2,"BigBlueButton",true,120,
+ listOf(student.id),course.id)
+
+ Log.d(STEP_TAG,"Refresh the page. Assert that $testConferenceTitle conference is displayed on the Conference List Page with the corresponding status.")
+ refresh()
+ conferenceListPage.assertConferenceDisplayed(testConferenceTitle)
+ conferenceListPage.assertConferenceStatus(testConferenceTitle,"Not Started")
+
+ Log.d(STEP_TAG,"Assert that $testConferenceTitle2 conference is displayed on the Conference List Page with the corresponding status.")
+ conferenceListPage.assertConferenceDisplayed(testConferenceTitle2)
+ conferenceListPage.assertConferenceStatus(testConferenceTitle2,"Not Started")
+
+ Log.d(STEP_TAG,"Open '$testConferenceTitle' conference details page.")
+ conferenceListPage.openConferenceDetails(testConferenceTitle)
+
+ Log.d(STEP_TAG,"Assert that the proper conference title '$testConferenceTitle', status and description '$testConferenceDescription' are displayed.")
+ conferenceDetailsPage.assertConferenceTitleDisplayed()
+ conferenceDetailsPage.assertConferenceStatus("Not Started")
+ conferenceDetailsPage.assertDescription(testConferenceDescription)
- Log.d(STEP_TAG,"Assert that the previously created conference is displayed with $title title and $description description.")
- ConferencesPage.assertConferenceTitlePresent(title)
- ConferencesPage.assertConferenceDescriptionPresent(description)
}
}
\ No newline at end of file
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/LoginE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/LoginE2ETest.kt
index 62d522e87a..b0b9bb3748 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/LoginE2ETest.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/LoginE2ETest.kt
@@ -17,7 +17,6 @@
package com.instructure.student.ui.e2e
import android.util.Log
-import androidx.test.espresso.Espresso
import com.instructure.canvas.espresso.E2E
import com.instructure.dataseeding.api.CoursesApi
import com.instructure.dataseeding.api.EnrollmentsApi
@@ -28,8 +27,12 @@ import com.instructure.dataseeding.model.CourseApiModel
import com.instructure.dataseeding.model.EnrollmentTypes.STUDENT_ENROLLMENT
import com.instructure.dataseeding.model.EnrollmentTypes.TEACHER_ENROLLMENT
import com.instructure.dataseeding.util.CanvasNetworkAdapter
-import com.instructure.panda_annotations.*
+import com.instructure.panda_annotations.FeatureCategory
+import com.instructure.panda_annotations.Priority
+import com.instructure.panda_annotations.TestCategory
+import com.instructure.panda_annotations.TestMetaData
import com.instructure.student.ui.utils.StudentTest
+import com.instructure.student.ui.utils.ViewUtils
import com.instructure.student.ui.utils.seedData
import dagger.hilt.android.testing.HiltAndroidTest
import org.junit.Test
@@ -54,38 +57,20 @@ class LoginE2ETest : StudentTest() {
val student1 = data.studentsList[0]
val student2 = data.studentsList[1]
- Log.d(STEP_TAG,"Click 'Find My School' button.")
- loginLandingPage.clickFindMySchoolButton()
-
- Log.d(STEP_TAG,"Enter domain: ${student1.domain}.")
- loginFindSchoolPage.enterDomain(student1.domain)
-
- Log.d(STEP_TAG,"Click on 'Next' button on the Toolbar.")
- loginFindSchoolPage.clickToolbarNextMenuItem()
-
Log.d(STEP_TAG,"Login with user: ${student1.name}, login id: ${student1.loginId} , password: ${student1.password}")
- loginSignInPage.loginAs(student1)
+ loginWithUser(student1)
Log.d(STEP_TAG,"Assert that the Dashboard Page is the landing page and it is loaded successfully.")
- verifyDashboardPage(student1)
+ assertDashboardPageDisplayed(student1)
Log.d(STEP_TAG,"Log out with ${student1.name} student.")
dashboardPage.logOut()
- Log.d(STEP_TAG,"Click 'Find My School' button.")
- loginLandingPage.clickFindMySchoolButton()
-
- Log.d(STEP_TAG,"Enter domain: ${student2.domain}.")
- loginFindSchoolPage.enterDomain(student2.domain)
-
- Log.d(STEP_TAG,"Click on 'Next' button on the Toolbar.")
- loginFindSchoolPage.clickToolbarNextMenuItem()
-
Log.d(STEP_TAG,"Login with user: ${student2.name}, login id: ${student2.loginId} , password: ${student2.password}")
- loginSignInPage.loginAs(student2)
+ loginWithUser(student2)
Log.d(STEP_TAG,"Assert that the Dashboard Page is the landing page and it is loaded successfully.")
- verifyDashboardPage(student2)
+ assertDashboardPageDisplayed(student2)
Log.d(STEP_TAG,"Click on 'Change User' button on the left-side menu.")
dashboardPage.pressChangeUser()
@@ -93,32 +78,44 @@ class LoginE2ETest : StudentTest() {
Log.d(STEP_TAG,"Assert that the previously logins has been displayed.")
loginLandingPage.assertDisplaysPreviousLogins()
- Log.d(STEP_TAG,"Login MANUALLY. Click 'Find My School' button.")
- loginLandingPage.clickFindMySchoolButton()
-
- Log.d(STEP_TAG,"Enter domain: ${student1.domain}.")
- loginFindSchoolPage.enterDomain(student1.domain)
-
- Log.d(STEP_TAG,"Click on 'Next' button on the Toolbar.")
- loginFindSchoolPage.clickToolbarNextMenuItem()
-
Log.d(STEP_TAG,"Login with user: ${student1.name}, login id: ${student1.loginId} , password: ${student1.password}")
- loginSignInPage.loginAs(student1)
+ loginWithUser(student1)
Log.d(STEP_TAG,"Assert that the Dashboard Page is the landing page and it is loaded successfully.")
- verifyDashboardPage(student1)
+ assertDashboardPageDisplayed(student1)
Log.d(STEP_TAG,"Click on 'Change User' button on the left-side menu.")
dashboardPage.pressChangeUser()
- Log.d(STEP_TAG,"Assert that the previously logins has been displayed.")
+ Log.d(STEP_TAG,"Assert that the previously logins has been displayed. Assert that ${student1.name} and ${student2.name} students are displayed within the previous login section.")
loginLandingPage.assertDisplaysPreviousLogins()
+ loginLandingPage.assertPreviousLoginUserDisplayed(student1.name)
+ loginLandingPage.assertPreviousLoginUserDisplayed(student2.name)
+
+ Log.d(STEP_TAG,"Remove ${student1.name} student from the previous login section.")
+ loginLandingPage.removeUserFromPreviousLogins(student1.name)
Log.d(STEP_TAG,"Login with the previous user, ${student2.name}, with one click, by clicking on the user's name on the bottom.")
loginLandingPage.loginWithPreviousUser(student2)
Log.d(STEP_TAG,"Assert that the Dashboard Page is the landing page and it is loaded successfully.")
- verifyDashboardPage(student2)
+ assertDashboardPageDisplayed(student2)
+
+ Log.d(STEP_TAG,"Click on 'Change User' button on the left-side menu.")
+ dashboardPage.pressChangeUser()
+
+ Log.d(STEP_TAG,"Assert that the previously logins has been displayed. Assert that ${student1.name} and ${student2.name} students are displayed within the previous login section.")
+ loginLandingPage.assertDisplaysPreviousLogins()
+ loginLandingPage.assertPreviousLoginUserDisplayed(student2.name)
+
+ Log.d(STEP_TAG,"Remove ${student2.name} student from the previous login section.")
+ loginLandingPage.removeUserFromPreviousLogins(student2.name)
+
+ Log.d(STEP_TAG,"Assert that none of the students, ${student1.name} and ${student2.name} are displayed and not even the 'Previous Logins' label is displayed.")
+ loginLandingPage.assertPreviousLoginUserNotExist(student1.name)
+ loginLandingPage.assertPreviousLoginUserNotExist(student2.name)
+ loginLandingPage.assertNotDisplaysPreviousLogins()
+
}
@E2E
@@ -137,34 +134,51 @@ class LoginE2ETest : StudentTest() {
val teacher = data.teachersList[0]
val ta = data.taList[0]
val course = data.coursesList[0]
+ val parent = parentData.parentsList[0] //Test with Parent user. parents don't show up in the "People" page so we can't verify their role.
+
+ Log.d(STEP_TAG,"Login with user: ${student.name}, login id: ${student.loginId} , password: ${student.password}")
+ loginWithUser(student)
Log.d(STEP_TAG,"Validate ${student.name} user's role as a Student.")
validateUserAndRole(student, course, "Student")
+ Log.d(STEP_TAG,"Navigate back to Dashboard Page.")
+ ViewUtils.pressBackButton(2)
+
+ Log.d(STEP_TAG,"Log out with ${student.name} student.")
+ dashboardPage.logOut()
+
+ Log.d(STEP_TAG,"Login with user: ${teacher.name}, login id: ${teacher.loginId} , password: ${teacher.password}")
+ loginWithUser(teacher)
+
Log.d(STEP_TAG,"Validate ${teacher.name} user's role as a Teacher.")
validateUserAndRole(teacher, course, "Teacher")
+ Log.d(STEP_TAG,"Navigate back to Dashboard Page.")
+ ViewUtils.pressBackButton(2)
+
+ Log.d(STEP_TAG,"Log out with ${teacher.name} teacher.")
+ dashboardPage.logOut()
+
+ Log.d(STEP_TAG,"Login with user: ${ta.name}, login id: ${ta.loginId} , password: ${ta.password}")
+ loginWithUser(ta)
+
Log.d(STEP_TAG,"Validate ${ta.name} user's role as a TA.")
validateUserAndRole(ta, course, "TA")
- // Test with Parent user. parents don't show up in the "People" page so we can't verify their role.
- val parent = parentData.parentsList[0]
- Log.d(STEP_TAG,"Click 'Find My School' button.")
- loginLandingPage.clickFindMySchoolButton()
-
- Log.d(STEP_TAG,"Enter domain: ${parent.domain}.")
- loginFindSchoolPage.enterDomain(parent.domain)
+ Log.d(STEP_TAG,"Navigate back to Dashboard Page.")
+ ViewUtils.pressBackButton(2)
- Log.d(STEP_TAG,"Enter domain: ${parent.domain}.")
- loginFindSchoolPage.clickToolbarNextMenuItem()
+ Log.d(STEP_TAG,"Log out with ${ta.name} teacher assistant.")
+ dashboardPage.logOut()
Log.d(STEP_TAG,"Login with user: ${parent.name}, login id: ${parent.loginId} , password: ${parent.password}")
- loginSignInPage.loginAs(parent)
+ loginWithUser(parent)
Log.d(STEP_TAG,"Assert that the Dashboard Page is the landing page and it is loaded successfully.")
- verifyDashboardPage(parent)
+ assertDashboardPageDisplayed(parent)
- Log.d(STEP_TAG,"Log out with ${parent.name} student.")
+ Log.d(STEP_TAG,"Log out with ${parent.name} parent.")
dashboardPage.logOut()
}
@@ -203,34 +217,46 @@ class LoginE2ETest : StudentTest() {
enrollmentService = enrollmentsService
)
+ Log.d(STEP_TAG,"Login with user: ${student.name}, login id: ${student.loginId} , password: ${student.password}")
+ loginWithUser(student)
+
Log.d(STEP_TAG,"Attempt to sign into our vanity domain, and validate ${student.name} user's role as a Student.")
validateUserAndRole(student, course,"Student" )
+
+ Log.d(STEP_TAG,"Navigate back to Dashboard Page.")
+ ViewUtils.pressBackButton(2)
+
+ Log.d(STEP_TAG,"Log out with ${student.name} student.")
+ dashboardPage.logOut()
}
- // Repeated logic from the testUserRolesLoginE2E test.
- // Assumes that you start at the login landing page, and logs you out before completing.
- private fun validateUserAndRole(user: CanvasUserApiModel, course: CourseApiModel, role: String) {
+ private fun loginWithUser(user: CanvasUserApiModel) {
+ Log.d(STEP_TAG,"Click 'Find My School' button.")
loginLandingPage.clickFindMySchoolButton()
+
+ Log.d(STEP_TAG,"Enter domain: ${user.domain}.")
loginFindSchoolPage.enterDomain(user.domain)
+
+ Log.d(STEP_TAG,"Click on 'Next' button on the Toolbar.")
loginFindSchoolPage.clickToolbarNextMenuItem()
loginSignInPage.loginAs(user)
+ }
+
+ private fun validateUserAndRole(user: CanvasUserApiModel, course: CourseApiModel, role: String) {
- // Verify that we are signed in as the user
- verifyDashboardPage(user)
+ Log.d(STEP_TAG,"Assert that the Dashboard Page is the landing page and it is loaded successfully.")
+ assertDashboardPageDisplayed(user)
- // Verify that our role is correct
+ Log.d(STEP_TAG,"Navigate to 'People' Page of ${course.name} course.")
dashboardPage.selectCourse(course)
courseBrowserPage.selectPeople()
- peopleListPage.assertPersonListed(user, role)
- Espresso.pressBack() // to course browser page
- Espresso.pressBack() // to dashboard page
- // Sign the user out
- dashboardPage.logOut()
+ Log.d(STEP_TAG,"Assert that ${user.name} user's role is: $role.")
+ peopleListPage.assertPersonListed(user, role)
}
- private fun verifyDashboardPage(user: CanvasUserApiModel)
+ private fun assertDashboardPageDisplayed(user: CanvasUserApiModel)
{
dashboardPage.waitForRender()
dashboardPage.assertUserLoggedIn(user)
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/k5/ScheduleE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/k5/ScheduleE2ETest.kt
index 586620f868..f32ae3784d 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/k5/ScheduleE2ETest.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/k5/ScheduleE2ETest.kt
@@ -190,7 +190,7 @@ class ScheduleE2ETest : StudentTest() {
schedulePage.scrollToItem(R.id.title, assignmentName, schedulePage.withAncestor(R.id.plannerItems))
schedulePage.assertMarkedAsDoneNotShown()
schedulePage.clickDoneCheckbox()
- schedulePage.swipeDown()
+ Thread.sleep(2000)
schedulePage.assertMarkedAsDoneShown()
}
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/DiscussionsInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/DiscussionsInteractionTest.kt
index 4b28e5fc11..d40f45ac7c 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/DiscussionsInteractionTest.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/DiscussionsInteractionTest.kt
@@ -19,17 +19,8 @@ package com.instructure.student.ui.interaction
import android.os.SystemClock.sleep
import androidx.test.espresso.Espresso
import androidx.test.espresso.web.webdriver.Locator
-import com.instructure.canvas.espresso.mockCanvas.MockCanvas
-import com.instructure.canvas.espresso.mockCanvas.addAssignment
-import com.instructure.canvas.espresso.mockCanvas.addDiscussionTopicToCourse
-import com.instructure.canvas.espresso.mockCanvas.addFileToCourse
-import com.instructure.canvas.espresso.mockCanvas.addReplyToDiscussion
-import com.instructure.canvas.espresso.mockCanvas.init
-import com.instructure.canvasapi2.models.Assignment
-import com.instructure.canvasapi2.models.CanvasContextPermission
-import com.instructure.canvasapi2.models.DiscussionEntry
-import com.instructure.canvasapi2.models.RemoteFile
-import com.instructure.canvasapi2.models.Tab
+import com.instructure.canvas.espresso.mockCanvas.*
+import com.instructure.canvasapi2.models.*
import com.instructure.panda_annotations.FeatureCategory
import com.instructure.panda_annotations.Priority
import com.instructure.panda_annotations.TestCategory
@@ -475,7 +466,8 @@ class DiscussionsInteractionTest : StudentTest() {
val attachment = createHtmlAttachment(data, attachmentHtml)
discussionEntry.attachments = mutableListOf(attachment)
- discussionDetailsPage.refresh() // To pick up updated reply
+ discussionDetailsPage.refresh()
+ Thread.sleep(3000) //allow some time to the reply to propagate
discussionDetailsPage.assertReplyDisplayed(discussionEntry)
discussionDetailsPage.assertReplyAttachment(discussionEntry)
discussionDetailsPage.previewAndCheckReplyAttachment(discussionEntry,
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ModuleInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ModuleInteractionTest.kt
index b1a6092ddb..6e84a2e81e 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ModuleInteractionTest.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ModuleInteractionTest.kt
@@ -18,34 +18,12 @@ package com.instructure.student.ui.interaction
import android.text.Html
import androidx.test.espresso.Espresso
import androidx.test.espresso.web.webdriver.Locator
-import com.instructure.canvas.espresso.Stub
-import com.instructure.canvas.espresso.mockCanvas.MockCanvas
-import com.instructure.canvas.espresso.mockCanvas.addAssignment
-import com.instructure.canvas.espresso.mockCanvas.addDiscussionTopicToCourse
-import com.instructure.canvas.espresso.mockCanvas.addFileToCourse
-import com.instructure.canvas.espresso.mockCanvas.addItemToModule
-import com.instructure.canvas.espresso.mockCanvas.addModuleToCourse
-import com.instructure.canvas.espresso.mockCanvas.addPageToCourse
-import com.instructure.canvas.espresso.mockCanvas.addQuestionToQuiz
-import com.instructure.canvas.espresso.mockCanvas.addQuizToCourse
-import com.instructure.canvas.espresso.mockCanvas.init
-import com.instructure.canvasapi2.models.Assignment
-import com.instructure.canvasapi2.models.DiscussionTopicHeader
-import com.instructure.canvasapi2.models.LockInfo
-import com.instructure.canvasapi2.models.LockedModule
-import com.instructure.canvasapi2.models.ModuleObject
-import com.instructure.canvasapi2.models.Page
-import com.instructure.canvasapi2.models.Quiz
-import com.instructure.canvasapi2.models.QuizAnswer
-import com.instructure.canvasapi2.models.Tab
+import com.instructure.canvas.espresso.mockCanvas.*
+import com.instructure.canvasapi2.models.*
import com.instructure.dataseeding.util.days
import com.instructure.dataseeding.util.fromNow
import com.instructure.dataseeding.util.iso8601
-import com.instructure.panda_annotations.FeatureCategory
-import com.instructure.panda_annotations.Priority
-import com.instructure.panda_annotations.SecondaryFeatureCategory
-import com.instructure.panda_annotations.TestCategory
-import com.instructure.panda_annotations.TestMetaData
+import com.instructure.panda_annotations.*
import com.instructure.student.R
import com.instructure.student.ui.pages.WebViewTextCheck
import com.instructure.student.ui.utils.StudentTest
@@ -101,12 +79,16 @@ class ModuleInteractionTest : StudentTest() {
discussionDetailsPage.assertTopicInfoShowing(topicHeader!!)
}
- // I'm punting on LTI testing for now. But MBL-13517 captures this work.
- @Stub
@Test
- @TestMetaData(Priority.MANDATORY, FeatureCategory.MODULES, TestCategory.INTERACTION, true)
+ @TestMetaData(Priority.MANDATORY, FeatureCategory.MODULES, TestCategory.INTERACTION)
fun testModules_launchesIntoExternalTool() {
// Tapping an ExternalTool module item should navigate to that item's detail page
+ val data = getToCourseModules(studentCount = 1, courseCount = 1)
+ val course1 = data.courses.values.first()
+ val module = data.courseModules[course1.id]!!.first()
+
+ modulesPage.clickModuleItem(module, "Google Drive")
+ canvasWebViewPage.assertTitle("Google Drive")
}
// Tapping an ExternalURL module item should navigate to that item's detail page
@@ -122,6 +104,7 @@ class ModuleInteractionTest : StudentTest() {
modulesPage.clickModuleItem(module,externalUrl)
// Not much we can test here, as it is an external URL, but testModules_navigateToNextAndPreviousModuleItems
// will test that the module name and module item name are displayed correctly.
+ canvasWebViewPage.checkWebViewURL("https://www.google.com")
}
// Tapping a File module item should navigate to that item's detail page
@@ -487,6 +470,12 @@ class ModuleInteractionTest : StudentTest() {
item = quiz!!
)
+ val ltiTool = data.addLTITool("Google Drive", "http://google.com", course1, 1234L)
+ data.addItemToModule(
+ course = course1,
+ moduleId = module.id,
+ item = ltiTool!!
+ )
// Sign in
val student = data.students[0]
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/NavigationDrawerInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/NavigationDrawerInteractionTest.kt
index 020c69234d..0ce11e4de2 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/NavigationDrawerInteractionTest.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/NavigationDrawerInteractionTest.kt
@@ -34,9 +34,9 @@ import com.instructure.panda_annotations.FeatureCategory
import com.instructure.panda_annotations.Priority
import com.instructure.panda_annotations.TestCategory
import com.instructure.panda_annotations.TestMetaData
+import com.instructure.student.R
import com.instructure.student.ui.utils.StudentTest
import com.instructure.student.ui.utils.tokenLogin
-import com.instructure.student.R
import dagger.hilt.android.testing.HiltAndroidTest
import org.hamcrest.CoreMatchers
import org.junit.Before
@@ -164,7 +164,7 @@ class NavigationDrawerInteractionTest : StudentTest() {
dashboardPage.goToHelp()
helpPage.launchGuides()
- canvasWebViewPage.verifyTitle(R.string.searchGuides)
+ canvasWebViewPage.assertTitle(R.string.searchGuides)
}
// Should send an error report
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ShareExtensionInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ShareExtensionInteractionTest.kt
new file mode 100644
index 0000000000..7bae222dd1
--- /dev/null
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ShareExtensionInteractionTest.kt
@@ -0,0 +1,311 @@
+/*
+ * Copyright (C) 2022 - present Instructure, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+package com.instructure.student.ui.interaction
+
+import android.app.Activity
+import android.app.Instrumentation
+import android.content.Intent
+import android.net.Uri
+import androidx.core.content.FileProvider
+import androidx.test.espresso.intent.Intents
+import androidx.test.espresso.intent.matcher.IntentMatchers
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.uiautomator.UiDevice
+import androidx.test.uiautomator.UiSelector
+import com.instructure.canvas.espresso.Stub
+import com.instructure.canvas.espresso.mockCanvas.MockCanvas
+import com.instructure.canvas.espresso.mockCanvas.addAssignment
+import com.instructure.canvas.espresso.mockCanvas.init
+import com.instructure.canvasapi2.models.Assignment
+import com.instructure.canvasapi2.models.User
+import com.instructure.pandautils.utils.Const
+import com.instructure.student.ui.utils.StudentTest
+import com.instructure.student.ui.utils.tokenLogin
+import dagger.hilt.android.testing.HiltAndroidTest
+import org.hamcrest.core.AllOf
+import org.junit.Test
+import java.io.File
+
+@HiltAndroidTest
+class ShareExtensionInteractionTest : StudentTest() {
+
+ override fun displaysPageObjects() = Unit
+
+ @Test
+ fun shareExtensionShowsUpCorrectlyWhenSharingFileFromExternalSource() {
+ val data = createMockData()
+ val student = data.students[0]
+ val uri = setupFileOnDevice("sample.jpg")
+ val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
+
+ login(student)
+ device.pressHome()
+
+ shareExternalFile(uri)
+
+ device.findObject(UiSelector().text("Canvas")).click()
+ device.waitForIdle()
+
+ shareExtensionTargetPage.assertPageObjects()
+ shareExtensionTargetPage.assertFilesCheckboxIsSelected()
+ shareExtensionTargetPage.assertUserName(student.name)
+ }
+
+ @Test
+ fun fileUploadDialogShowsCorrectlyForMyFilesUpload() {
+ val data = createMockData()
+ val student = data.students[0]
+ val uri = setupFileOnDevice("sample.jpg")
+ val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
+
+ login(student)
+ device.pressHome()
+
+ shareExternalFile(uri)
+
+ device.findObject(UiSelector().text("Canvas")).click()
+ device.waitForIdle()
+
+ shareExtensionTargetPage.pressNext()
+
+ fileUploadPage.assertPageObjects()
+ fileUploadPage.assertDialogTitle("Upload To My Files")
+ fileUploadPage.assertFileDisplayed("sample.jpg")
+ }
+
+ @Test
+ fun addAndRemoveFileFromFileUploadDialog() {
+ val data = createMockData()
+ val student = data.students[0]
+ val uri = setupFileOnDevice("sample.jpg")
+ setupFileOnDevice("samplepdf.pdf")
+ val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
+
+ login(student)
+ device.pressHome()
+
+ shareExternalFile(uri)
+
+ device.findObject(UiSelector().text("Canvas")).click()
+ device.waitForIdle()
+
+ shareExtensionTargetPage.pressNext()
+
+ fileUploadPage.assertPageObjects()
+ fileUploadPage.assertFileDisplayed("sample.jpg")
+
+ fileUploadPage.removeFile("sample.jpg")
+
+ // Add new file
+ Intents.init()
+ try {
+ stubFilePickerIntent("samplepdf.pdf")
+ fileUploadPage.chooseDevice()
+ }
+ finally {
+ Intents.release()
+ }
+
+ fileUploadPage.assertFileNotDisplayed("sample.jpg")
+ fileUploadPage.assertFileDisplayed("samplepdf.pdf")
+ }
+
+ @Test
+ fun fileUploadDialogShowsCorrectlyForAssignmentSubmission() {
+ val data = createMockData()
+ val student = data.students[0]
+ val uri = setupFileOnDevice("sample.jpg")
+ val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
+
+ val assignment = data.addAssignment(data.courses.values.first().id, submissionType = Assignment.SubmissionType.ONLINE_UPLOAD)
+
+ login(student)
+ device.pressHome()
+
+ shareExternalFile(uri)
+
+ device.findObject(UiSelector().text("Canvas")).click()
+ device.waitForIdle()
+
+ shareExtensionTargetPage.selectSubmission()
+ shareExtensionTargetPage.assertCourseSelectorDisplayedWithCourse(data.courses.values.first().name)
+ shareExtensionTargetPage.assertAssignmentSelectorDisplayedWithAssignment(assignment.name!!)
+ shareExtensionTargetPage.pressNext()
+
+ fileUploadPage.assertPageObjects()
+ fileUploadPage.assertDialogTitle("Submission")
+ fileUploadPage.assertFileDisplayed("sample.jpg")
+ }
+
+ // Clicking spinner item not working.
+ @Test
+ @Stub
+ fun changeTargetAssignment() {
+ val data = createMockData()
+ val student = data.students[0]
+ val uri = setupFileOnDevice("sample.jpg")
+ val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
+
+ data.addAssignment(data.courses.values.first().id, submissionType = Assignment.SubmissionType.ONLINE_UPLOAD)
+ val assignment2 = data.addAssignment(data.courses.values.first().id, submissionType = Assignment.SubmissionType.ONLINE_UPLOAD)
+
+ login(student)
+ device.pressHome()
+
+ shareExternalFile(uri)
+
+ device.findObject(UiSelector().text("Canvas")).click()
+ device.waitForIdle()
+
+ shareExtensionTargetPage.selectSubmission()
+ shareExtensionTargetPage.selectAssignment(assignment2.name!!)
+
+ shareExtensionTargetPage.pressNext()
+
+ fileUploadPage.assertPageObjects()
+ fileUploadPage.assertDialogTitle("Submission")
+ fileUploadPage.assertFileDisplayed("sample.jpg")
+ }
+
+ @Test
+ fun shareExtensionShowsUpCorrectlyWhenSharingMultipleFiles() {
+ val data = createMockData()
+ val student = data.students[0]
+ val uri = setupFileOnDevice("sample.jpg")
+ val uri2 = setupFileOnDevice("samplepdf.pdf")
+ val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
+
+ login(student)
+ device.pressHome()
+
+ shareMultipleFiles(arrayListOf(uri, uri2))
+
+ device.findObject(UiSelector().text("Canvas")).click()
+ device.waitForIdle()
+
+ shareExtensionTargetPage.assertPageObjects()
+ shareExtensionTargetPage.assertFilesCheckboxIsSelected()
+ shareExtensionTargetPage.assertUserName(student.name)
+
+ shareExtensionTargetPage.pressNext()
+
+ fileUploadPage.assertPageObjects()
+ fileUploadPage.assertFileDisplayed("sample.jpg")
+ fileUploadPage.assertFileDisplayed("samplepdf.pdf")
+ }
+
+ @Test
+ fun testFileAssignmentSubmission() {
+ val data = createMockData()
+ val student = data.students[0]
+ val uri = setupFileOnDevice("sample.jpg")
+ val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
+
+ data.addAssignment(data.courses.values.first().id, submissionType = Assignment.SubmissionType.ONLINE_UPLOAD)
+
+ login(student)
+ device.pressHome()
+
+ shareExternalFile(uri)
+
+ device.findObject(UiSelector().text("Canvas")).click()
+ device.waitForIdle()
+
+ shareExtensionTargetPage.selectSubmission()
+ shareExtensionTargetPage.pressNext()
+ fileUploadPage.clickTurnIn()
+
+ shareExtensionStatusPage.assertPageObjects()
+ shareExtensionStatusPage.assertAssignemntSubmissionSuccess()
+ }
+
+ private fun createMockData(): MockCanvas {
+
+ val data = MockCanvas.init(
+ studentCount = 1,
+ teacherCount = 1,
+ courseCount = 1,
+ favoriteCourseCount = 1
+ )
+
+ return data
+ }
+
+ private fun login(student: User) {
+ val token = MockCanvas.data.tokenFor(student)
+ tokenLogin(MockCanvas.data.domain, token!!, student)
+ }
+
+ private fun setupFileOnDevice(fileName: String): Uri {
+ copyAssetFileToExternalCache(activityRule.activity, fileName)
+
+ val dir = activityRule.activity.externalCacheDir
+ val file = File(dir?.path, fileName)
+
+ val instrumentationContext = InstrumentationRegistry.getInstrumentation().context
+ return FileProvider.getUriForFile(
+ instrumentationContext,
+ "com.instructure.candroid" + Const.FILE_PROVIDER_AUTHORITY,
+ file
+ )
+ }
+
+ private fun shareExternalFile(uri: Uri) {
+ val intent = Intent().apply {
+ action = Intent.ACTION_SEND
+ putExtra(Intent.EXTRA_STREAM, uri)
+ type = "image/jpg"
+ }
+
+ val chooser = Intent.createChooser(intent, null)
+ chooser.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+
+ InstrumentationRegistry.getInstrumentation().context.startActivity(chooser)
+ }
+
+ private fun shareMultipleFiles(uris: ArrayList) {
+ val intent = Intent().apply {
+ action = Intent.ACTION_SEND_MULTIPLE
+ putParcelableArrayListExtra(Intent.EXTRA_STREAM, uris)
+ type = "*/*"
+ }
+
+ val chooser = Intent.createChooser(intent, null)
+ chooser.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+
+ InstrumentationRegistry.getInstrumentation().context.startActivity(chooser)
+ }
+
+ private fun stubFilePickerIntent(fileName: String) {
+ val resultData = Intent()
+ val dir = activityRule.activity.externalCacheDir
+ val file = File(dir?.path, fileName)
+ val newFileUri = FileProvider.getUriForFile(
+ activityRule.activity,
+ "com.instructure.candroid" + Const.FILE_PROVIDER_AUTHORITY,
+ file
+ )
+ resultData.data = newFileUri
+
+ Intents.intending(
+ AllOf.allOf(
+ IntentMatchers.hasAction(Intent.ACTION_GET_CONTENT),
+ IntentMatchers.hasType("*/*"),
+ )
+ ).respondWith(Instrumentation.ActivityResult(Activity.RESULT_OK, resultData))
+ }
+}
\ No newline at end of file
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/UserFilesInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/UserFilesInteractionTest.kt
index 17cbc628d6..4aacb3f12e 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/UserFilesInteractionTest.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/UserFilesInteractionTest.kt
@@ -20,6 +20,7 @@ import android.app.Instrumentation
import android.content.Intent
import android.net.Uri
import android.provider.MediaStore
+import androidx.core.content.FileProvider
import androidx.test.espresso.intent.ActivityResultFunction
import androidx.test.espresso.intent.Intents
import androidx.test.espresso.intent.Intents.intending
@@ -34,6 +35,7 @@ import com.instructure.panda_annotations.FeatureCategory
import com.instructure.panda_annotations.Priority
import com.instructure.panda_annotations.TestCategory
import com.instructure.panda_annotations.TestMetaData
+import com.instructure.pandautils.utils.Const
import com.instructure.student.ui.utils.StudentTest
import com.instructure.student.ui.utils.tokenLogin
import dagger.hilt.android.testing.HiltAndroidTest
@@ -70,7 +72,11 @@ class UserFilesInteractionTest : StudentTest() {
val resultData = Intent()
val dir = activity.externalCacheDir
val file = File(dir?.path, "sample.jpg")
- val uri = Uri.fromFile(file)
+ val uri = FileProvider.getUriForFile(
+ activityRule.activity,
+ "com.instructure.candroid" + Const.FILE_PROVIDER_AUTHORITY,
+ file
+ )
resultData.data = uri
activityResult = Instrumentation.ActivityResult(Activity.RESULT_OK, resultData)
}
@@ -96,8 +102,7 @@ class UserFilesInteractionTest : StudentTest() {
intending(
allOf(
hasAction(Intent.ACTION_GET_CONTENT),
- hasType("*/*"),
- hasFlag(Intent.FLAG_GRANT_READ_URI_PERMISSION)
+ hasType("*/*")
)
).respondWith(activityResult)
fileUploadPage.chooseDevice()
@@ -167,8 +172,8 @@ class UserFilesInteractionTest : StudentTest() {
// Set up the "from gallery" mock result, then press "from gallery"
intending(
allOf(
- hasAction(Intent.ACTION_PICK),
- hasFlag(Intent.FLAG_GRANT_READ_URI_PERMISSION)
+ hasAction(Intent.ACTION_GET_CONTENT),
+ hasType("image/*")
)
).respondWith(activityResult)
fileUploadPage.chooseGallery()
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/CanvasWebViewPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/CanvasWebViewPage.kt
index d30fa91f02..64aa9bc941 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/CanvasWebViewPage.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/CanvasWebViewPage.kt
@@ -21,7 +21,10 @@ import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.web.assertion.WebViewAssertions.webMatches
import androidx.test.espresso.web.model.Atoms.getCurrentUrl
import androidx.test.espresso.web.sugar.Web.onWebView
-import androidx.test.espresso.web.webdriver.DriverAtoms.*
+import androidx.test.espresso.web.webdriver.DriverAtoms.findElement
+import androidx.test.espresso.web.webdriver.DriverAtoms.getText
+import androidx.test.espresso.web.webdriver.DriverAtoms.webClick
+import androidx.test.espresso.web.webdriver.DriverAtoms.webScrollIntoView
import androidx.test.espresso.web.webdriver.Locator
import com.instructure.canvas.espresso.withElementRepeat
import com.instructure.espresso.assertVisible
@@ -35,7 +38,11 @@ import org.hamcrest.Matchers.containsString
*/
open class CanvasWebViewPage : BasePage(R.id.canvasWebView) {
- fun verifyTitle(@StringRes title: Int) {
+ fun assertTitle(@StringRes title: Int) {
+ onView(withAncestor(R.id.toolbar) + withText(title)).assertVisible()
+ }
+
+ fun assertTitle(title: String) {
onView(withAncestor(R.id.toolbar) + withText(title)).assertVisible()
}
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/ConferenceDetailsPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/ConferenceDetailsPage.kt
index 000c33630b..556eee1ea6 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/ConferenceDetailsPage.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/ConferenceDetailsPage.kt
@@ -16,9 +16,23 @@
*/
package com.instructure.student.ui.pages
-import com.instructure.espresso.page.BasePage
+import androidx.test.espresso.matcher.ViewMatchers.hasSibling
+import com.instructure.espresso.assertDisplayed
+import com.instructure.espresso.page.*
import com.instructure.student.R
+import org.hamcrest.CoreMatchers.allOf
open class ConferenceDetailsPage : BasePage(R.id.conferenceDetailsPage) {
- // For future use
+
+ fun assertConferenceTitleDisplayed() {
+ onView(allOf(withId(R.id.title), hasSibling(withId(R.id.statusDetails)))).assertDisplayed()
+ }
+
+ fun assertConferenceStatus(expectedStatus: String) {
+ onView(allOf(withId(R.id.status), withText(expectedStatus), withParent(R.id.statusDetails))).assertDisplayed()
+ }
+
+ fun assertDescription(expectedDescription: String) {
+ onView(allOf(withId(R.id.description) + withText(expectedDescription), hasSibling(withId(R.id.statusDetails)))).assertDisplayed()
+ }
}
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/ConferenceListPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/ConferenceListPage.kt
index 976da7af61..e41269af5c 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/ConferenceListPage.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/ConferenceListPage.kt
@@ -16,9 +16,44 @@
*/
package com.instructure.student.ui.pages
+import androidx.test.espresso.assertion.ViewAssertions.doesNotExist
+import androidx.test.espresso.matcher.ViewMatchers.hasSibling
+import com.instructure.espresso.assertDisplayed
+import com.instructure.espresso.click
import com.instructure.espresso.page.BasePage
+import com.instructure.espresso.page.onView
+import com.instructure.espresso.page.withId
+import com.instructure.espresso.page.withText
import com.instructure.student.R
+import org.hamcrest.CoreMatchers.allOf
open class ConferenceListPage : BasePage(R.id.conferenceListPage) {
- // For future use
+
+ fun assertEmptyView() {
+ onView(withId(R.id.conferenceListEmptyView)).assertDisplayed()
+ onView(allOf(withId(R.id.emptyTitle), withText(R.string.noConferencesTitle))).assertDisplayed()
+ onView(allOf(withId(R.id.emptyMessage), withText(R.string.noConferencesMessage))).assertDisplayed()
+
+ }
+
+ fun assertConferenceStatus(conferenceTitle: String, expectedStatus: String) {
+ onView(allOf(withId(R.id.statusLabel), withText(expectedStatus), hasSibling(allOf(withId(R.id.title), withText(conferenceTitle)))))
+ }
+
+ fun assertConferenceDisplayed(conferenceTitle: String) {
+ onView(allOf(withId(R.id.title), withText(conferenceTitle))).assertDisplayed()
+ }
+
+ fun clickOnOpenExternallyButton() {
+ onView(withId(R.id.openExternallyButton)).click()
+ }
+
+ fun assertOpenExternallyButtonNotDisplayed() {
+ onView(withId(R.id.openExternallyButton)).check(doesNotExist())
+ }
+
+ fun openConferenceDetails(conferenceTitle: String) {
+ onView(withText(conferenceTitle)).click()
+ }
+
}
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/FileUploadPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/FileUploadPage.kt
index 39b4c4fbdf..71a5238bee 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/FileUploadPage.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/FileUploadPage.kt
@@ -18,15 +18,21 @@ package com.instructure.student.ui.pages
import android.widget.Button
import androidx.test.espresso.Espresso.onView
+import androidx.test.espresso.assertion.ViewAssertions.doesNotExist
+import androidx.test.espresso.matcher.ViewMatchers
import androidx.test.espresso.matcher.ViewMatchers.isAssignableFrom
-import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.withId
import com.instructure.canvas.espresso.containsTextCaseInsensitive
-import com.instructure.canvas.espresso.scrollRecyclerView
import com.instructure.espresso.OnViewWithId
+import com.instructure.espresso.assertDisplayed
import com.instructure.espresso.click
import com.instructure.espresso.page.BasePage
-import com.instructure.espresso.page.scrollTo
+import com.instructure.espresso.page.onViewWithText
+import com.instructure.espresso.page.plus
+import com.instructure.espresso.page.waitForViewWithId
+import com.instructure.espresso.page.withAncestor
+import com.instructure.espresso.page.withDescendant
+import com.instructure.espresso.page.withText
import com.instructure.espresso.scrollTo
import com.instructure.student.R
import org.hamcrest.core.AllOf.allOf
@@ -35,6 +41,8 @@ class FileUploadPage : BasePage() {
private val cameraButton by OnViewWithId(R.id.fromCamera)
private val galleryButton by OnViewWithId(R.id.fromGallery)
private val deviceButton by OnViewWithId(R.id.fromDevice)
+ private val chooseFileTitle by OnViewWithId(R.id.chooseFileTitle)
+ private val chooseFileSubtitle by OnViewWithId(R.id.chooseFileSubtitle)
fun chooseCamera() {
cameraButton.scrollTo().click()
@@ -51,4 +59,27 @@ class FileUploadPage : BasePage() {
fun clickUpload() {
onView(allOf(isAssignableFrom(Button::class.java),containsTextCaseInsensitive("upload"))).click()
}
+
+ fun clickTurnIn() {
+ onView(containsTextCaseInsensitive("turn in")).click()
+ }
+
+ fun removeFile(filename: String) {
+ val fileItemMatcher = withId(R.id.fileItem) + withDescendant(withId(R.id.fileName) + withText(filename))
+
+ onView(withId(R.id.removeFile) + ViewMatchers.isDescendantOfA(fileItemMatcher))
+ .click()
+ }
+
+ fun assertDialogTitle(title: String) {
+ onViewWithText(title).assertDisplayed()
+ }
+
+ fun assertFileDisplayed(filename: String) {
+ onView(withId(R.id.fileName) + withText(filename))
+ }
+
+ fun assertFileNotDisplayed(filename: String) {
+ onView(withId(R.id.fileName) + withText(filename)).check(doesNotExist())
+ }
}
\ No newline at end of file
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/LoginLandingPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/LoginLandingPage.kt
index 19c5b5e2e8..1fe4cc35ad 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/LoginLandingPage.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/LoginLandingPage.kt
@@ -16,16 +16,18 @@
*/
package com.instructure.student.ui.pages
+import androidx.test.espresso.assertion.ViewAssertions.doesNotExist
+import androidx.test.espresso.matcher.ViewMatchers.hasSibling
+import androidx.test.espresso.matcher.ViewMatchers.withChild
import com.instructure.canvasapi2.models.User
-import com.instructure.canvasapi2.utils.RemoteConfigParam
-import com.instructure.canvasapi2.utils.RemoteConfigUtils
import com.instructure.dataseeding.model.CanvasUserApiModel
import com.instructure.espresso.OnViewWithId
import com.instructure.espresso.assertDisplayed
+import com.instructure.espresso.assertNotDisplayed
import com.instructure.espresso.click
-import com.instructure.espresso.page.BasePage
-import com.instructure.espresso.page.onViewWithText
+import com.instructure.espresso.page.*
import com.instructure.student.R
+import org.hamcrest.CoreMatchers.allOf
@Suppress("unused")
class LoginLandingPage : BasePage() {
@@ -65,6 +67,22 @@ class LoginLandingPage : BasePage() {
previousLoginTitleText.assertDisplayed()
}
+ fun assertNotDisplaysPreviousLogins() {
+ previousLoginTitleText.assertNotDisplayed()
+ }
+
+ fun assertPreviousLoginUserDisplayed(userName: String) {
+ onView(withText(userName)).assertDisplayed()
+ }
+
+ fun assertPreviousLoginUserNotExist(userName: String) {
+ onView(withText(userName)).check(doesNotExist())
+ }
+
+ fun removeUserFromPreviousLogins(userName: String) {
+ onView(allOf(withId(R.id.removePreviousUser), hasSibling(withChild(withText(userName))))).click()
+ }
+
fun loginWithPreviousUser(previousUser: CanvasUserApiModel) {
onViewWithText(previousUser.name).click()
}
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/ShareExtensionStatusPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/ShareExtensionStatusPage.kt
new file mode 100644
index 0000000000..0e94d5a7cb
--- /dev/null
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/ShareExtensionStatusPage.kt
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2022 - present Instructure, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+package com.instructure.student.ui.pages
+
+import com.instructure.espresso.WaitForViewWithId
+import com.instructure.espresso.assertHasText
+import com.instructure.espresso.page.BasePage
+import com.instructure.student.R
+
+class ShareExtensionStatusPage : BasePage() {
+
+ private val dialogTitle by WaitForViewWithId(R.id.dialogTitle)
+ private val subtitle by WaitForViewWithId(R.id.subtitle)
+ private val description by WaitForViewWithId(R.id.description)
+
+ fun assertAssignemntSubmissionSuccess() {
+ dialogTitle.assertHasText(R.string.submission)
+ subtitle.assertHasText(R.string.submissionSuccessTitle)
+ description.assertHasText(R.string.submissionSuccessMessage)
+ }
+}
\ No newline at end of file
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/ShareExtensionTargetPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/ShareExtensionTargetPage.kt
new file mode 100644
index 0000000000..042a4e2262
--- /dev/null
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/ShareExtensionTargetPage.kt
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2022 - present Instructure, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+package com.instructure.student.ui.pages
+
+import androidx.test.espresso.Espresso.onData
+import androidx.test.espresso.ViewAssertion
+import androidx.test.espresso.assertion.ViewAssertions
+import androidx.test.espresso.matcher.RootMatchers.*
+import androidx.test.espresso.matcher.ViewMatchers
+import com.instructure.espresso.OnViewWithId
+import com.instructure.espresso.WaitForViewWithId
+import com.instructure.espresso.WaitForViewWithStringTextIgnoreCase
+import com.instructure.espresso.WaitForViewWithText
+import com.instructure.espresso.assertDisplayed
+import com.instructure.espresso.assertHasText
+import com.instructure.espresso.assertSelected
+import com.instructure.espresso.click
+import com.instructure.espresso.page.BasePage
+import com.instructure.espresso.page.onView
+import com.instructure.espresso.page.onViewWithId
+import com.instructure.espresso.page.onViewWithSpinnerText
+import com.instructure.espresso.page.onViewWithText
+import com.instructure.espresso.page.plus
+import com.instructure.espresso.page.withAncestor
+import com.instructure.espresso.page.withId
+import com.instructure.espresso.page.withText
+import com.instructure.student.R
+import org.hamcrest.Matchers.anything
+
+class ShareExtensionTargetPage : BasePage() {
+
+ private val avatar by WaitForViewWithId(R.id.avatar)
+ private val dialogTitle by WaitForViewWithId(R.id.dialogTitle)
+ private val userName by WaitForViewWithId(R.id.userName)
+ private val selectionWrapper by WaitForViewWithId(R.id.selectionWrapper)
+ private val filesCheckbox by WaitForViewWithId(R.id.filesCheckBox)
+ private val assignmentCheckbox by WaitForViewWithId(R.id.assignmentCheckBox)
+ private val nextButton by WaitForViewWithStringTextIgnoreCase("next")
+ private val cancelButton by WaitForViewWithStringTextIgnoreCase("cancel")
+
+ fun assertFilesCheckboxIsSelected() {
+ filesCheckbox.check(ViewAssertions.matches(ViewMatchers.isChecked()))
+ }
+
+ fun assertUserName(username: String) {
+ userName.assertHasText(username)
+ }
+
+ fun assertCourseSelectorDisplayedWithCourse(courseName: String) {
+ onViewWithId(R.id.studentCourseSpinner).assertDisplayed()
+ onView(withText(courseName) + withAncestor(R.id.studentCourseSpinner)).assertDisplayed()
+ }
+
+ fun assertAssignmentSelectorDisplayedWithAssignment(assignmentName: String) {
+ onViewWithId(R.id.assignmentSpinner).assertDisplayed()
+ onView(withText(assignmentName) + withAncestor(R.id.assignmentSpinner)).assertDisplayed()
+ }
+
+ fun selectAssignment(assignmentName: String) {
+ onViewWithId(R.id.assignmentSpinner).click()
+ onData(anything()).inRoot(isDialog()).atPosition(1)
+ }
+
+ fun selectSubmission() {
+ assignmentCheckbox.click()
+ }
+
+ fun pressNext() {
+ nextButton.click()
+ }
+}
\ No newline at end of file
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/AssignmentDetailsRenderTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/AssignmentDetailsRenderTest.kt
index 4b468fe200..9d66d7546f 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/AssignmentDetailsRenderTest.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/AssignmentDetailsRenderTest.kt
@@ -76,7 +76,8 @@ class AssignmentDetailsRenderTest : StudentRenderTest() {
fun displaysTitleDataNotSubmitted() {
val assignment = Assignment(
name = "Test Assignment",
- pointsPossible = 35.0
+ pointsPossible = 35.0,
+ submissionTypesRaw = listOf("online_text_entry")
)
val model = baseModel.copy(assignmentResult = DataResult.Success(assignment))
loadPageWithModel(model)
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/SyllabusRenderTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/SyllabusRenderTest.kt
index aa83cfa912..66b6355a37 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/SyllabusRenderTest.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/SyllabusRenderTest.kt
@@ -29,6 +29,7 @@ import dagger.hilt.android.testing.HiltAndroidTest
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
+import java.lang.Thread.sleep
@HiltAndroidTest
@RunWith(AndroidJUnit4::class)
@@ -140,6 +141,7 @@ class SyllabusRenderTest : StudentRenderTest() {
loopMod = { it.effectRunner { emptyEffectRunner } }
}
activityRule.activity.loadFragment(fragment)
+ sleep(3000) // Need to wait here a bit because loadFragment needs some time.
}
}
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/utils/StudentTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/utils/StudentTest.kt
index 1ca87195e4..1583572955 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/utils/StudentTest.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/utils/StudentTest.kt
@@ -68,6 +68,8 @@ abstract class StudentTest : CanvasTest() {
val calendarEventPage = CalendarEventPage()
val canvasWebViewPage = CanvasWebViewPage()
val courseBrowserPage = CourseBrowserPage()
+ val conferenceListPage = ConferenceListPage()
+ val conferenceDetailsPage = ConferenceDetailsPage()
val elementaryCoursePage = ElementaryCoursePage()
val courseGradesPage = CourseGradesPage()
val dashboardPage = DashboardPage()
@@ -110,6 +112,8 @@ abstract class StudentTest : CanvasTest() {
val gradesPage = GradesPage()
val resourcesPage = ResourcesPage()
val importantDatesPage = ImportantDatesPage()
+ val shareExtensionTargetPage = ShareExtensionTargetPage()
+ val shareExtensionStatusPage = ShareExtensionStatusPage()
// A no-op interaction to afford us an easy, harmless way to get a11y checking to trigger.
fun meaninglessSwipe() {
diff --git a/apps/student/src/main/AndroidManifest.xml b/apps/student/src/main/AndroidManifest.xml
index 0a83d35912..888e536236 100644
--- a/apps/student/src/main/AndroidManifest.xml
+++ b/apps/student/src/main/AndroidManifest.xml
@@ -81,7 +81,8 @@
android:clearTaskOnLaunch="true"
android:launchMode="singleTop"
android:configChanges="keyboardHidden|orientation|screenSize"
- android:theme="@style/LoginFlowTheme.Splash_Student">
+ android:theme="@style/LoginFlowTheme.Splash_Student"
+ android:exported="true">
@@ -106,12 +107,6 @@
android:windowSoftInputMode="adjustResize"
android:label="@string/canvas"
android:theme="@style/CanvasMaterialTheme_Default">
-
-
-
-
-
-
+ android:configChanges="keyboardHidden|orientation"
+ android:exported="true">
+ android:theme="@style/CanvasMaterialTheme_Default.Translucent"
+ android:exported="true">
+
@@ -238,7 +235,8 @@
+ android:name=".activity.WidgetSetupActivity"
+ android:exported="false">
@@ -248,7 +246,8 @@
android:name=".activity.BookmarkShortcutActivity"
android:icon="@drawable/ic_bookmark_shortcut"
android:label="@string/student_app_name"
- android:theme="@style/CanvasMaterialTheme_DefaultNoTransparency">
+ android:theme="@style/CanvasMaterialTheme_DefaultNoTransparency"
+ android:exported="true">
@@ -259,7 +258,8 @@
+ android:launchMode="singleTask"
+ android:exported="true">
@@ -280,7 +280,8 @@
android:name=".util.FileDownloadJobIntentService"
android:permission="android.permission.BIND_JOB_SERVICE" />
-
+
@@ -301,7 +302,8 @@
+ android:label="@string/todoWidgetTitleLong"
+ android:exported="false">
@@ -321,7 +323,8 @@
+ android:label="@string/gradesWidgetTitleLong"
+ android:exported="false">
@@ -341,7 +344,8 @@
+ android:label="@string/notificationWidgetTitleLong"
+ android:exported="false">
diff --git a/apps/student/src/main/java/com/instructure/student/activity/BaseRouterActivity.kt b/apps/student/src/main/java/com/instructure/student/activity/BaseRouterActivity.kt
index 37454de549..0bd5ea46d0 100644
--- a/apps/student/src/main/java/com/instructure/student/activity/BaseRouterActivity.kt
+++ b/apps/student/src/main/java/com/instructure/student/activity/BaseRouterActivity.kt
@@ -205,7 +205,7 @@ abstract class BaseRouterActivity : CallbackActivity(), FullScreenInteractions {
}
fun openMedia(canvasContext: CanvasContext?, url: String) {
- openMediaBundle = OpenMediaAsyncTaskLoader.createBundle(canvasContext, url, null)
+ openMediaBundle = OpenMediaAsyncTaskLoader.createBundle(url, null, canvasContext)
LoaderUtils.restartLoaderWithBundle>(
LoaderManager.getInstance(this), openMediaBundle, loaderCallbacks, R.id.openMediaLoaderID)
}
diff --git a/apps/student/src/main/java/com/instructure/student/activity/CallbackActivity.kt b/apps/student/src/main/java/com/instructure/student/activity/CallbackActivity.kt
index 4386a7ba62..ff7e145dd8 100644
--- a/apps/student/src/main/java/com/instructure/student/activity/CallbackActivity.kt
+++ b/apps/student/src/main/java/com/instructure/student/activity/CallbackActivity.kt
@@ -60,8 +60,6 @@ abstract class CallbackActivity : ParentActivity(), InboxFragment.OnUnreadCountI
private fun loadInitialData() {
loadInitialDataJob = tryWeave {
- val crashlytics = FirebaseCrashlytics.getInstance();
-
// Determine if user can masquerade
if (ApiPrefs.canBecomeUser == null) {
if (ApiPrefs.domain.startsWith("siteadmin", true)) {
@@ -117,14 +115,9 @@ abstract class CallbackActivity : ParentActivity(), InboxFragment.OnUnreadCountI
}
if (!ApiPrefs.isMasquerading) {
- // Set logged user details
- if (Logger.canLogUserDetails()) {
- Logger.d("User detail logging allowed. Setting values.")
- crashlytics.setUserId("UserID: ${ApiPrefs.user?.id.toString()} User Domain: ${ApiPrefs.domain}")
- } else {
- Logger.d("User detail logging disallowed. Clearing values.")
- crashlytics.setUserId("")
- }
+ // We don't know how the crashlytics stores the userId so we just set it to empty to make sure we don't log it.
+ val crashlytics = FirebaseCrashlytics.getInstance();
+ crashlytics.setUserId("")
}
// get unread count of conversations
diff --git a/apps/student/src/main/java/com/instructure/student/activity/CandroidPSPDFActivity.kt b/apps/student/src/main/java/com/instructure/student/activity/CandroidPSPDFActivity.kt
index cc5a2b290f..8d5f1f8e66 100644
--- a/apps/student/src/main/java/com/instructure/student/activity/CandroidPSPDFActivity.kt
+++ b/apps/student/src/main/java/com/instructure/student/activity/CandroidPSPDFActivity.kt
@@ -25,16 +25,16 @@ import android.util.LayoutDirection
import android.util.TypedValue
import android.view.Menu
import android.view.MenuItem
-import android.view.View
import androidx.annotation.ColorInt
import androidx.core.text.TextUtilsCompat
import com.instructure.annotations.CanvasPdfMenuGrouping
import com.instructure.pandautils.analytics.SCREEN_VIEW_PSPDFKIT
import com.instructure.pandautils.analytics.ScreenView
+import com.instructure.pandautils.features.shareextension.ShareFileSubmissionTarget
import com.instructure.pandautils.utils.Const
-import com.instructure.pandautils.utils.ThemePrefs
import com.instructure.pandautils.utils.ViewStyler
import com.instructure.student.R
+import com.instructure.student.features.shareextension.StudentShareExtensionActivity
import com.pspdfkit.document.processor.PdfProcessorTask
import com.pspdfkit.document.sharing.DefaultDocumentSharingController
import com.pspdfkit.document.sharing.DocumentSharingIntentHelper
@@ -132,7 +132,7 @@ class CandroidPSPDFActivity : PdfActivity(), ToolbarCoordinatorLayout.OnContextu
) : DefaultDocumentSharingController(mContext) {
override fun onDocumentPrepared(shareUri: Uri) {
- val intent = Intent(mContext, ShareFileUploadActivity::class.java)
+ val intent = Intent(mContext, StudentShareExtensionActivity::class.java)
intent.type = DocumentSharingIntentHelper.MIME_TYPE_PDF
intent.putExtra(Intent.EXTRA_STREAM, shareUri)
intent.putExtra(Const.SUBMISSION_TARGET, submissionTarget)
diff --git a/apps/student/src/main/java/com/instructure/student/activity/NavigationActivity.kt b/apps/student/src/main/java/com/instructure/student/activity/NavigationActivity.kt
index 768f7c9380..ed14703185 100644
--- a/apps/student/src/main/java/com/instructure/student/activity/NavigationActivity.kt
+++ b/apps/student/src/main/java/com/instructure/student/activity/NavigationActivity.kt
@@ -26,7 +26,6 @@ import android.graphics.Color
import android.graphics.Typeface
import android.os.Bundle
import android.os.Handler
-import android.view.LayoutInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
@@ -48,8 +47,6 @@ import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
import com.airbnb.lottie.LottieAnimationView
import com.bumptech.glide.Glide
-import com.google.android.material.bottomnavigation.BottomNavigationItemView
-import com.google.android.material.bottomnavigation.BottomNavigationMenuView
import com.google.android.material.bottomnavigation.BottomNavigationView
import com.instructure.canvasapi2.CanvasRestAdapter
import com.instructure.canvasapi2.managers.CourseManager
@@ -68,15 +65,17 @@ import com.instructure.interactions.router.RouterParams
import com.instructure.loginapi.login.dialog.ErrorReportDialog
import com.instructure.loginapi.login.dialog.MasqueradingDialog
import com.instructure.loginapi.login.tasks.LogoutTask
-import com.instructure.pandautils.dialogs.UploadFilesDialog
import com.instructure.pandautils.features.help.HelpDialogFragment
-import com.instructure.pandautils.features.notification.preferences.NotificationPreferencesFragment
+import com.instructure.pandautils.features.notification.preferences.PushNotificationPreferencesFragment
import com.instructure.pandautils.features.themeselector.ThemeSelectorBottomSheet
import com.instructure.pandautils.models.PushNotification
import com.instructure.pandautils.receivers.PushExternalReceiver
import com.instructure.pandautils.typeface.TypefaceBehavior
import com.instructure.pandautils.update.UpdateManager
import com.instructure.pandautils.utils.*
+import com.instructure.pandautils.utils.RequestCodes.CAMERA_PIC_REQUEST
+import com.instructure.pandautils.utils.RequestCodes.PICK_FILE_FROM_DEVICE
+import com.instructure.pandautils.utils.RequestCodes.PICK_IMAGE_GALLERY
import com.instructure.student.R
import com.instructure.student.dialog.BookmarkCreationDialog
import com.instructure.student.events.*
@@ -105,7 +104,6 @@ import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode
import java.util.*
import javax.inject.Inject
-import kotlin.collections.ArrayList
private const val BOTTOM_NAV_SCREEN = "bottomNavScreen"
private const val BOTTOM_SCREENS_BUNDLE_KEY = "bottomScreens"
@@ -342,9 +340,9 @@ class NavigationActivity : BaseRouterActivity(), Navigation, MasqueradingDialog.
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
- if (requestCode == UploadFilesDialog.CAMERA_PIC_REQUEST ||
- requestCode == UploadFilesDialog.PICK_FILE_FROM_DEVICE ||
- requestCode == UploadFilesDialog.PICK_IMAGE_GALLERY ||
+ if (requestCode == CAMERA_PIC_REQUEST ||
+ requestCode == PICK_FILE_FROM_DEVICE ||
+ requestCode == PICK_IMAGE_GALLERY ||
PickerSubmissionUploadEffectHandler.isPickerRequest(requestCode) ||
AssignmentDetailsFragment.isFileRequest(requestCode) ||
SubmissionDetailsEmptyContentFragment.isFileRequest(requestCode)
@@ -751,8 +749,8 @@ class NavigationActivity : BaseRouterActivity(), Navigation, MasqueradingDialog.
}
}
RouteContext.NOTIFICATION_PREFERENCES == route.routeContext -> {
- Analytics.trackAppFlow(this@NavigationActivity, NotificationPreferencesFragment::class.java)
- RouteMatcher.route(this@NavigationActivity, Route(NotificationPreferencesFragment::class.java, null))
+ Analytics.trackAppFlow(this@NavigationActivity, PushNotificationPreferencesFragment::class.java)
+ RouteMatcher.route(this@NavigationActivity, Route(PushNotificationPreferencesFragment::class.java, null))
}
else -> {
//fetch the CanvasContext
diff --git a/apps/student/src/main/java/com/instructure/student/activity/ShareFileUploadActivity.kt b/apps/student/src/main/java/com/instructure/student/activity/ShareFileUploadActivity.kt
deleted file mode 100644
index 5872617ad1..0000000000
--- a/apps/student/src/main/java/com/instructure/student/activity/ShareFileUploadActivity.kt
+++ /dev/null
@@ -1,253 +0,0 @@
-/*
- * Copyright (C) 2016 - present Instructure, Inc.
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, version 3 of the License.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
- *
- */
-
-package com.instructure.student.activity
-
-import android.animation.Animator
-import android.animation.AnimatorListenerAdapter
-import android.animation.ArgbEvaluator
-import android.animation.ValueAnimator
-import android.content.DialogInterface
-import android.content.Intent
-import android.net.Uri
-import android.os.Bundle
-import android.os.Parcelable
-import android.text.TextUtils
-import android.view.ViewTreeObserver
-import android.widget.Toast
-import androidx.appcompat.app.AppCompatActivity
-import androidx.core.app.ActivityCompat
-import androidx.core.content.ContextCompat
-import androidx.fragment.app.DialogFragment
-import com.instructure.canvasapi2.managers.CourseManager
-import com.instructure.canvasapi2.models.Assignment
-import com.instructure.canvasapi2.models.CanvasContext
-import com.instructure.canvasapi2.models.Course
-import com.instructure.canvasapi2.models.StorageQuotaExceededError
-import com.instructure.canvasapi2.utils.ApiPrefs
-import com.instructure.canvasapi2.utils.isNotDeleted
-import com.instructure.canvasapi2.utils.weave.awaitApi
-import com.instructure.canvasapi2.utils.weave.catch
-import com.instructure.canvasapi2.utils.weave.tryWeave
-import com.instructure.pandautils.dialogs.UploadFilesDialog
-import com.instructure.pandautils.utils.*
-import com.instructure.student.R
-import com.instructure.student.dialog.ShareFileDestinationDialog
-import com.instructure.student.util.Analytics
-import com.instructure.student.util.AnimationHelpers
-import kotlinx.android.parcel.Parcelize
-import kotlinx.android.synthetic.main.activity_share_file.*
-import kotlinx.coroutines.Job
-import org.greenrobot.eventbus.EventBus
-import org.greenrobot.eventbus.Subscribe
-import org.greenrobot.eventbus.ThreadMode
-import java.util.*
-
-@Parcelize
-data class ShareFileSubmissionTarget(
- val course: Course,
- val assignment: Assignment
-) : Parcelable
-
-class ShareFileUploadActivity : AppCompatActivity(), ShareFileDestinationDialog.DialogCloseListener {
-
- private val PERMISSION_REQUEST_WRITE_STORAGE = 0
-
- private var loadCoursesJob: Job? = null
- private var uploadFileSourceFragment: DialogFragment? = null
- private var courses: ArrayList? = null
-
- private val submissionTarget: ShareFileSubmissionTarget? by lazy {
- intent?.extras?.getParcelable(Const.SUBMISSION_TARGET)
- }
-
- private var sharedURI: Uri? = null
-
- public override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- setContentView(R.layout.activity_share_file)
- ViewStyler.setStatusBarDark(this, ContextCompat.getColor(this, R.color.studentDocumentSharingColor))
- if (checkLoggedIn()) {
- revealBackground()
- Analytics.trackAppFlow(this)
- sharedURI = parseIntentType()
- if (submissionTarget != null) {
- // If targeted for submission, skip the picker and go immediately to the submission workflow
- val bundle = UploadFilesDialog.createAssignmentBundle(
- sharedURI,
- submissionTarget!!.course,
- submissionTarget!!.assignment
- )
- onNext(bundle)
- } else {
- getCourses()
- }
- askForStoragePermissionIfNecessary()
- }
- }
-
- private fun askForStoragePermissionIfNecessary() {
- if ((sharedURI?.scheme?.equals("file") == true || sharedURI?.scheme?.equals("content") == true) && !PermissionUtils.hasPermissions(this, PermissionUtils.WRITE_EXTERNAL_STORAGE)) {
- ActivityCompat.requestPermissions(this, PermissionUtils.makeArray(PermissionUtils.WRITE_EXTERNAL_STORAGE), PERMISSION_REQUEST_WRITE_STORAGE)
- }
- }
-
- override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
- super.onActivityResult(requestCode, resultCode, data)
-
- if (requestCode == UploadFilesDialog.CAMERA_PIC_REQUEST ||
- requestCode == UploadFilesDialog.PICK_FILE_FROM_DEVICE ||
- requestCode == UploadFilesDialog.PICK_IMAGE_GALLERY) {
- //File Dialog Fragment will not be notified of onActivityResult(), alert manually
- OnActivityResults(ActivityResult(requestCode, resultCode, data), null).postSticky()
- }
- }
-
- override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) {
- super.onRequestPermissionsResult(requestCode, permissions, grantResults)
- when(requestCode) {
- PERMISSION_REQUEST_WRITE_STORAGE -> {
- if (!PermissionUtils.allPermissionsGrantedResultSummary(grantResults)) {
- Toast.makeText(this, R.string.permissionDenied, Toast.LENGTH_LONG).show()
- finish()
- }
- }
- }
- }
-
- private fun getCourses() {
- loadCoursesJob = tryWeave {
- val courses = awaitApi> { CourseManager.getCourses(true, it) }
- if (courses.isNotEmpty()) {
- this@ShareFileUploadActivity.courses = ArrayList(courses)
- if (uploadFileSourceFragment == null) showDestinationDialog()
- } else {
- Toast.makeText(applicationContext, R.string.uploadingFromSourceFailed, Toast.LENGTH_LONG).show()
- exitActivity()
- }
- } catch {
- Toast.makeText(this@ShareFileUploadActivity, R.string.uploadingFromSourceFailed, Toast.LENGTH_LONG).show()
- exitActivity()
- }
- }
-
- private fun revealBackground() {
- rootView.viewTreeObserver.addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener {
- override fun onGlobalLayout() {
- AnimationHelpers.removeGlobalLayoutListeners(rootView, this)
- AnimationHelpers.createRevealAnimator(rootView).start()
- }
- })
- }
-
- private fun checkLoggedIn(): Boolean {
- return if (TextUtils.isEmpty(ApiPrefs.getValidToken())) {
- exitActivity()
- false
- } else {
- true
- }
- }
-
- private fun exitActivity() {
- val intent = LoginActivity.createIntent(this)
- startActivity(intent)
- finish()
- }
-
- override fun onBackPressed() {
- uploadFileSourceFragment?.dismissAllowingStateLoss()
- super.onBackPressed()
- }
-
- override fun onDestroy() {
- uploadFileSourceFragment?.dismissAllowingStateLoss()
- loadCoursesJob?.cancel()
- super.onDestroy()
- }
-
- override fun onStart() {
- super.onStart()
- EventBus.getDefault().register(this)
- }
-
- override fun onStop() {
- super.onStop()
- EventBus.getDefault().unregister(this)
- }
-
- private fun showDestinationDialog() {
- if (sharedURI == null) {
- Toast.makeText(applicationContext, R.string.uploadingFromSourceFailed, Toast.LENGTH_LONG).show()
- } else {
- uploadFileSourceFragment = ShareFileDestinationDialog.newInstance(ShareFileDestinationDialog.createBundle(sharedURI!!, courses!!))
- uploadFileSourceFragment!!.show(supportFragmentManager, ShareFileDestinationDialog.TAG)
- }
- }
-
- private fun parseIntentType(): Uri? {
- // Get intent, action and MIME type
- val intent = intent
- val action = intent.action
- val type = intent.type
-
- return if (Intent.ACTION_SEND == action && type != null) {
- intent.getParcelableExtra(Intent.EXTRA_STREAM)
- } else null
-
- }
-
- override fun onCancel(dialog: DialogInterface?) {
- finish()
- }
-
-
- @Suppress("unused", "UNUSED_PARAMETER")
- @Subscribe(threadMode = ThreadMode.MAIN)
- fun onQuotaExceeded(errorCode: StorageQuotaExceededError) {
- toast(R.string.fileQuotaExceeded)
- }
-
- private fun getColor(bundle: Bundle?): Int {
- return if(bundle != null && bundle.containsKey(Const.CANVAS_CONTEXT)) {
- val color = ColorKeeper.getOrGenerateColor(bundle.getParcelable(Const.CANVAS_CONTEXT) as CanvasContext)
- ViewStyler.setStatusBarDark(this, color)
- color
- } else {
- val color = ContextCompat.getColor(this, R.color.login_studentAppTheme)
- ViewStyler.setStatusBarDark(this, color)
- color
- }
- }
-
- override fun onNext(bundle: Bundle) {
- ValueAnimator.ofObject(ArgbEvaluator(), ContextCompat.getColor(this, R.color.login_studentAppTheme), getColor(bundle)).let {
- it.addUpdateListener { animation -> rootView!!.setBackgroundColor(animation.animatedValue as Int) }
- it.duration = 500
- it.addListener(object : AnimatorListenerAdapter() {
- override fun onAnimationStart(animation: Animator) {
- UploadFilesDialog.show(supportFragmentManager, bundle) { event ->
- if(event == UploadFilesDialog.EVENT_ON_UPLOAD_BEGIN || event == UploadFilesDialog.EVENT_DIALOG_CANCELED) {
- finish()
- }
- }
- }
- })
- it.start()
- }
- }
-}
diff --git a/apps/student/src/main/java/com/instructure/student/di/ShareExtensionModule.kt b/apps/student/src/main/java/com/instructure/student/di/ShareExtensionModule.kt
new file mode 100644
index 0000000000..237905ecbb
--- /dev/null
+++ b/apps/student/src/main/java/com/instructure/student/di/ShareExtensionModule.kt
@@ -0,0 +1,20 @@
+package com.instructure.student.di
+
+import com.instructure.pandautils.features.shareextension.ShareExtensionRouter
+import com.instructure.student.features.shareextension.StudentShareExtensionRouter
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.components.SingletonComponent
+import javax.inject.Singleton
+
+@Module
+@InstallIn(SingletonComponent::class)
+class ShareExtensionModule {
+
+ @Provides
+ @Singleton
+ fun provideShareExtensionRouter(): ShareExtensionRouter {
+ return StudentShareExtensionRouter()
+ }
+}
\ No newline at end of file
diff --git a/apps/student/src/main/java/com/instructure/student/dialog/ShareFileDestinationDialog.kt b/apps/student/src/main/java/com/instructure/student/dialog/ShareFileDestinationDialog.kt
deleted file mode 100644
index 4d09f4da63..0000000000
--- a/apps/student/src/main/java/com/instructure/student/dialog/ShareFileDestinationDialog.kt
+++ /dev/null
@@ -1,328 +0,0 @@
-/*
- * Copyright (C) 2016 - present Instructure, Inc.
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, version 3 of the License.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
- *
- */
-package com.instructure.student.dialog
-
-import android.animation.Animator
-import android.animation.AnimatorListenerAdapter
-import android.annotation.SuppressLint
-import android.app.Dialog
-import android.content.DialogInterface
-import android.net.Uri
-import android.os.Bundle
-import android.os.Handler
-import android.view.*
-import android.view.animation.AnimationUtils
-import android.widget.AdapterView
-import android.widget.Toast
-import androidx.appcompat.app.AlertDialog
-import androidx.fragment.app.DialogFragment
-import com.instructure.canvasapi2.managers.AssignmentManager.getAllAssignments
-import com.instructure.canvasapi2.models.Assignment
-import com.instructure.canvasapi2.models.Course
-import com.instructure.canvasapi2.models.User
-import com.instructure.canvasapi2.utils.ApiPrefs
-import com.instructure.canvasapi2.utils.Pronouns.span
-import com.instructure.canvasapi2.utils.weave.awaitApi
-import com.instructure.canvasapi2.utils.weave.catch
-import com.instructure.canvasapi2.utils.weave.tryWeave
-import com.instructure.pandautils.dialogs.UploadFilesDialog
-import com.instructure.pandautils.dialogs.UploadFilesDialog.Companion.createAssignmentBundle
-import com.instructure.pandautils.dialogs.UploadFilesDialog.Companion.createFilesBundle
-import com.instructure.pandautils.utils.Const
-import com.instructure.pandautils.utils.ParcelableArg
-import com.instructure.pandautils.utils.ParcelableArrayListArg
-import com.instructure.pandautils.utils.ThemePrefs.buttonColor
-import com.instructure.pandautils.utils.setVisible
-import com.instructure.student.R
-import com.instructure.student.adapter.FileUploadAssignmentsAdapter
-import com.instructure.student.adapter.FileUploadAssignmentsAdapter.Companion.getOnlineUploadAssignmentsList
-import com.instructure.student.adapter.FileUploadCoursesAdapter
-import com.instructure.student.adapter.FileUploadCoursesAdapter.Companion.getFilteredCourseList
-import com.instructure.student.util.AnimationHelpers.createRevealAnimator
-import com.instructure.student.util.AnimationHelpers.removeGlobalLayoutListeners
-import com.instructure.student.util.UploadCheckboxManager
-import com.instructure.student.util.UploadCheckboxManager.OnOptionCheckedListener
-import kotlinx.android.synthetic.main.upload_file_destination.*
-import kotlinx.coroutines.Job
-import java.util.*
-
-@SuppressLint("InflateParams")
-class ShareFileDestinationDialog : DialogFragment(), OnOptionCheckedListener {
- // Dismiss interface
- interface DialogCloseListener {
- fun onCancel(dialog: DialogInterface?)
- fun onNext(bundle: Bundle)
- }
-
- private var uri: Uri by ParcelableArg(key = Const.URI)
- private var courses: ArrayList by ParcelableArrayListArg(key = Const.COURSES)
- private var user: User = ApiPrefs.user!!
-
- private lateinit var checkboxManager: UploadCheckboxManager
- private lateinit var rootView: View
-
- private var assignmentJob: Job? = null
-
- private var selectedAssignment: Assignment? = null
- private var studentEnrollmentsAdapter: FileUploadCoursesAdapter? = null
-
- override fun onStart() {
- super.onStart()
- // Don't dim the background when the dialog is created.
- dialog?.window?.apply {
- val params = attributes
- params.dimAmount = 0f
- params.flags = params.flags or WindowManager.LayoutParams.FLAG_DIM_BEHIND
- attributes = params
- }
- }
-
- override fun onActivityCreated(savedInstanceState: Bundle?) {
- super.onActivityCreated(savedInstanceState)
- dialog?.window?.let {
- it.attributes.windowAnimations = R.style.FileDestinationDialogAnimation
- it.setWindowAnimations(R.style.FileDestinationDialogAnimation)
- }
- }
-
- override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
- rootView = LayoutInflater.from(activity).inflate(R.layout.upload_file_destination, null)
- val alertDialog = AlertDialog.Builder(requireContext())
- .setView(rootView)
- .setPositiveButton(R.string.next) { _, _ -> validateAndShowNext() }
- .setNegativeButton(R.string.cancel) { _, _ -> dismissAllowingStateLoss() }
- .setCancelable(true)
- .create()
-
- alertDialog.setOnShowListener {
- alertDialog.getButton(AlertDialog.BUTTON_POSITIVE).setTextColor(buttonColor)
- alertDialog.getButton(AlertDialog.BUTTON_NEGATIVE).setTextColor(buttonColor)
- }
-
- return alertDialog
- }
-
- override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
- return rootView
- }
-
- override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
- userName.text = span(user.name, user.pronouns)
-
- // Init checkboxes
- checkboxManager = UploadCheckboxManager(this, selectionIndicator)
- checkboxManager.add(myFilesCheckBox)
- checkboxManager.add(assignmentCheckBox)
-
- setRevealContentsListener()
- assignmentContainer.setVisible()
- }
-
- override fun onCancel(dialog: DialogInterface) {
- (activity as? DialogCloseListener)?.onCancel(dialog)
- }
-
- override fun onDestroyView() {
- if (retainInstance) dialog?.dismiss()
- super.onDestroyView()
- }
-
- private fun validateAndShowNext() {
- // Validate selections
- val errorString = validateForm()
- if (errorString.isNotEmpty()) {
- Toast.makeText(activity, errorString, Toast.LENGTH_SHORT).show()
- } else {
- (activity as? DialogCloseListener)?.onNext(uploadBundle)
- dismiss()
- }
- }
-
- /**
- * Checks if user has filled out form completely.
- * @return Returns an error string if the form is not valid.
- */
- private fun validateForm(): String {
- // Make sure the user has selected a course and an assignment
- val uploadType = checkboxManager.selectedType
-
- // Make sure an assignment & course was selected if FileUploadType.Assignment
- if (uploadType == UploadFilesDialog.FileUploadType.ASSIGNMENT) {
- if (studentCourseSpinner.selectedItem == null) {
- return getString(R.string.noCourseSelected)
- } else if (assignmentSpinner.selectedItem == null || (assignmentSpinner.selectedItem as? Assignment)?.id == Long.MIN_VALUE) {
- return getString(R.string.noAssignmentSelected)
- }
- }
- return ""
- }
-
- private val uploadBundle: Bundle
- get() = when (checkboxManager.selectedCheckBox!!.id) {
- R.id.myFilesCheckBox -> createFilesBundle(uri, null)
- R.id.assignmentCheckBox -> createAssignmentBundle(
- uri,
- (studentCourseSpinner.selectedItem as Course),
- (assignmentSpinner.selectedItem as Assignment)
- )
- else -> createFilesBundle(uri, null)
- }
-
- private fun setAssignmentsSpinnerToLoading() {
- val loading = Assignment()
- val courseAssignments = ArrayList()
- loading.name = getString(R.string.loadingAssignments)
- loading.id = Long.MIN_VALUE
- courseAssignments.add(loading)
- assignmentSpinner.adapter = FileUploadAssignmentsAdapter(requireContext(), courseAssignments)
- }
-
- fun fetchAssignments(courseId: Long) {
- assignmentJob?.cancel()
- assignmentJob = tryWeave {
- val assignments = awaitApi> { getAllAssignments(courseId, false, it) }
- if (assignments.isNotEmpty() && courseSelectionChanged(assignments[0].courseId)) return@tryWeave
- val courseAssignments = getOnlineUploadAssignmentsList(requireContext(), assignments)
-
- // Init assignment spinner
- val adapter = FileUploadAssignmentsAdapter(requireContext(), courseAssignments)
- assignmentSpinner.adapter = adapter
- if (selectedAssignment != null) {
- // Prevent listener from firing the when selection is placed
- assignmentSpinner.onItemSelectedListener = null
- val position = adapter.getPosition(selectedAssignment)
- if (position >= 0) {
- // Prevents the network callback from replacing what the user selected while cache was being displayed
- assignmentSpinner.setSelection(position, false)
- }
- }
- assignmentSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
- override fun onItemSelected(parent: AdapterView<*>?, view: View, position: Int, id: Long) {
- if (position < 0) return
- if (position < adapter.count) {
- selectedAssignment = adapter.getItem(position)
- }
- }
-
- override fun onNothingSelected(parent: AdapterView<*>?) {}
- }
- } catch {
- // Do nothing
- }
- }
-
- private fun setupCourseSpinners() {
- if (activity?.isFinishing != false) return
- if (studentEnrollmentsAdapter == null) {
- studentEnrollmentsAdapter = FileUploadCoursesAdapter(
- requireContext(),
- requireActivity().layoutInflater,
- getFilteredCourseList(courses, FileUploadCoursesAdapter.Type.STUDENT)
- )
- studentCourseSpinner.adapter = studentEnrollmentsAdapter
- } else {
- studentEnrollmentsAdapter?.setCourses(getFilteredCourseList(courses, FileUploadCoursesAdapter.Type.STUDENT))
- }
- studentCourseSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
- override fun onItemSelected(parent: AdapterView<*>, view: View, position: Int, id: Long) {
- // Make the allowed extensions disappear
- val (courseId) = parent.adapter.getItem(position) as Course
- // If the user is a teacher, let them know and don't let them select an assignment
- if (courseId > 0) {
- setAssignmentsSpinnerToLoading()
- fetchAssignments(courseId)
- }
- }
-
- override fun onNothingSelected(parent: AdapterView<*>?) {}
- }
- }
-
- private fun courseSelectionChanged(newCourseId: Long): Boolean {
- return checkboxManager.selectedCheckBox!!.id == R.id.assignmentCheckBox && newCourseId != (studentCourseSpinner.selectedItem as Course).id
- }
-
- private fun setRevealContentsListener() {
- val avatarAnimation = AnimationUtils.loadAnimation(activity, R.anim.ease_in_shrink)
- val titleAnimation = AnimationUtils.loadAnimation(activity, R.anim.ease_in_bottom)
- avatar.viewTreeObserver.addOnGlobalLayoutListener(
- object : ViewTreeObserver.OnGlobalLayoutListener {
- override fun onGlobalLayout() {
- removeGlobalLayoutListeners(avatar, this)
- avatar.startAnimation(avatarAnimation)
- userName.startAnimation(titleAnimation)
- dialogTitle.startAnimation(titleAnimation)
- }
- }
- )
- dialogContents.viewTreeObserver.addOnGlobalLayoutListener(
- object : ViewTreeObserver.OnGlobalLayoutListener {
- override fun onGlobalLayout() {
- removeGlobalLayoutListeners(dialogContents, this)
- val revealAnimator = createRevealAnimator(dialogContents)
- Handler().postDelayed({
- if (!isAdded) return@postDelayed
- dialogContents.visibility = View.VISIBLE
- revealAnimator.addListener(
- object : AnimatorListenerAdapter() {
- override fun onAnimationEnd(animation: Animator) {
- setupCourseSpinners()
- }
- }
- )
- revealAnimator.start()
- }, 600)
- }
- }
- )
- }
-
- private fun enableStudentSpinners(isEnabled: Boolean) {
- assignmentSpinner.isEnabled = isEnabled
- studentCourseSpinner.isEnabled = isEnabled
- }
-
- override fun onUserFilesSelected() {
- enableStudentSpinners(false)
- }
-
- override fun onAssignmentFilesSelected() {
- enableStudentSpinners(true)
- }
-
- override fun onDestroy() {
- assignmentJob?.cancel()
- super.onDestroy()
- }
-
- companion object {
- const val TAG = "uploadFileSourceFragment"
-
- fun newInstance(bundle: Bundle): ShareFileDestinationDialog {
- val uploadFileSourceFragment = ShareFileDestinationDialog()
- uploadFileSourceFragment.arguments = bundle
- return uploadFileSourceFragment
- }
-
- fun createBundle(uri: Uri, courses: ArrayList): Bundle {
- val bundle = Bundle()
- bundle.putParcelable(Const.URI, uri)
- bundle.putParcelableArrayList(Const.COURSES, courses)
- return bundle
- }
- }
-}
diff --git a/apps/student/src/main/java/com/instructure/student/features/shareextension/StudentShareExtensionActivity.kt b/apps/student/src/main/java/com/instructure/student/features/shareextension/StudentShareExtensionActivity.kt
new file mode 100644
index 0000000000..6cd9ede4b9
--- /dev/null
+++ b/apps/student/src/main/java/com/instructure/student/features/shareextension/StudentShareExtensionActivity.kt
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2022 - present Instructure, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.instructure.student.features.shareextension
+
+import android.os.Bundle
+import com.instructure.pandautils.features.shareextension.ShareExtensionActivity
+import com.instructure.student.activity.LoginActivity
+import com.instructure.student.util.Analytics
+
+class StudentShareExtensionActivity : ShareExtensionActivity() {
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ Analytics.trackAppFlow(this)
+ }
+
+ override fun exitActivity() {
+ val intent = LoginActivity.createIntent(this)
+ startActivity(intent)
+ finish()
+ }
+}
\ No newline at end of file
diff --git a/apps/student/src/main/java/com/instructure/student/features/shareextension/StudentShareExtensionRouter.kt b/apps/student/src/main/java/com/instructure/student/features/shareextension/StudentShareExtensionRouter.kt
new file mode 100644
index 0000000000..2b3c1e3c9e
--- /dev/null
+++ b/apps/student/src/main/java/com/instructure/student/features/shareextension/StudentShareExtensionRouter.kt
@@ -0,0 +1,15 @@
+package com.instructure.student.features.shareextension
+
+import android.content.Context
+import android.content.Intent
+import com.instructure.pandautils.features.shareextension.ShareExtensionRouter
+import com.instructure.pandautils.features.shareextension.WORKER_ID
+import java.util.*
+
+class StudentShareExtensionRouter : ShareExtensionRouter {
+ override fun routeToProgressScreen(context: Context, workerId: UUID): Intent {
+ val intent = Intent(context, StudentShareExtensionActivity::class.java)
+ intent.putExtra(WORKER_ID, workerId)
+ return intent
+ }
+}
\ No newline at end of file
diff --git a/apps/student/src/main/java/com/instructure/student/fragment/ApplicationSettingsFragment.kt b/apps/student/src/main/java/com/instructure/student/fragment/ApplicationSettingsFragment.kt
index 5e9131f1f8..8279d05085 100644
--- a/apps/student/src/main/java/com/instructure/student/fragment/ApplicationSettingsFragment.kt
+++ b/apps/student/src/main/java/com/instructure/student/fragment/ApplicationSettingsFragment.kt
@@ -28,7 +28,8 @@ import com.instructure.canvasapi2.utils.pageview.PageView
import com.instructure.loginapi.login.dialog.NoInternetConnectionDialog
import com.instructure.pandautils.analytics.SCREEN_VIEW_APPLICATION_SETTINGS
import com.instructure.pandautils.analytics.ScreenView
-import com.instructure.pandautils.features.notification.preferences.NotificationPreferencesFragment
+import com.instructure.pandautils.features.notification.preferences.EmailNotificationPreferencesFragment
+import com.instructure.pandautils.features.notification.preferences.PushNotificationPreferencesFragment
import com.instructure.pandautils.fragments.RemoteConfigParamsFragment
import com.instructure.pandautils.utils.*
import com.instructure.student.BuildConfig
@@ -94,7 +95,11 @@ class ApplicationSettingsFragment : ParentFragment() {
}
pushNotifications.onClick {
- addFragment(NotificationPreferencesFragment.newInstance())
+ addFragment(PushNotificationPreferencesFragment.newInstance())
+ }
+
+ emailNotifications.onClick {
+ addFragment(EmailNotificationPreferencesFragment.newInstance())
}
about.onClick {
diff --git a/apps/student/src/main/java/com/instructure/student/fragment/CalendarEventFragment.kt b/apps/student/src/main/java/com/instructure/student/fragment/CalendarEventFragment.kt
index 78548c9873..fe139b7280 100644
--- a/apps/student/src/main/java/com/instructure/student/fragment/CalendarEventFragment.kt
+++ b/apps/student/src/main/java/com/instructure/student/fragment/CalendarEventFragment.kt
@@ -237,7 +237,7 @@ class CalendarEventFragment : ParentFragment() {
private fun loadCalendarHtml(html: String, contentDescription: String?) {
calendarEventWebView.setVisible()
calendarEventWebView.setBackgroundColor(ContextCompat.getColor(requireContext(), R.color.backgroundLightest))
- calendarEventWebView.loadHtml(html, contentDescription)
+ calendarEventWebView.loadHtml(html, contentDescription, baseUrl = scheduleItem?.htmlUrl)
}
private fun setUpCallback() {
diff --git a/apps/student/src/main/java/com/instructure/student/fragment/DiscussionDetailsFragment.kt b/apps/student/src/main/java/com/instructure/student/fragment/DiscussionDetailsFragment.kt
index 43529465ca..fdc6ba1ea4 100644
--- a/apps/student/src/main/java/com/instructure/student/fragment/DiscussionDetailsFragment.kt
+++ b/apps/student/src/main/java/com/instructure/student/fragment/DiscussionDetailsFragment.kt
@@ -544,7 +544,7 @@ class DiscussionDetailsFragment : ParentFragment(), Bookmarkable {
//region Loading
private fun loadHTMLTopic(html: String, contentDescription: String?) {
setupHeaderWebView()
- discussionTopicHeaderWebView.loadHtml(html, contentDescription)
+ discussionTopicHeaderWebView.loadHtml(html, contentDescription, baseUrl = discussionTopicHeader.htmlUrl)
}
private fun loadHTMLReplies(html: String, contentDescription: String? = null) {
diff --git a/apps/student/src/main/java/com/instructure/student/fragment/DiscussionsReplyFragment.kt b/apps/student/src/main/java/com/instructure/student/fragment/DiscussionsReplyFragment.kt
index f352041ff8..809228f1aa 100644
--- a/apps/student/src/main/java/com/instructure/student/fragment/DiscussionsReplyFragment.kt
+++ b/apps/student/src/main/java/com/instructure/student/fragment/DiscussionsReplyFragment.kt
@@ -35,8 +35,8 @@ import com.instructure.interactions.router.Route
import com.instructure.loginapi.login.dialog.NoInternetConnectionDialog
import com.instructure.pandautils.analytics.SCREEN_VIEW_DISCUSSIONS_REPLY
import com.instructure.pandautils.analytics.ScreenView
-import com.instructure.pandautils.dialogs.UploadFilesDialog
import com.instructure.pandautils.discussions.DiscussionCaching
+import com.instructure.pandautils.features.file.upload.FileUploadDialogFragment
import com.instructure.pandautils.utils.*
import com.instructure.pandautils.views.AttachmentView
import com.instructure.student.R
@@ -76,12 +76,12 @@ class DiscussionsReplyFragment : ParentFragment() {
val attachments = ArrayList()
if (attachment != null) attachments.add(attachment!!)
- val bundle = UploadFilesDialog.createDiscussionsBundle(attachments)
- UploadFilesDialog.show(fragmentManager, bundle) { event, attachment ->
- if (event == UploadFilesDialog.EVENT_ON_FILE_SELECTED) {
+ val bundle = FileUploadDialogFragment.createDiscussionsBundle(attachments)
+ FileUploadDialogFragment.newInstance(bundle, pickerCallback = { event, attachment ->
+ if (event == FileUploadDialogFragment.EVENT_ON_FILE_SELECTED) {
handleAttachment(attachment)
}
- }
+ }).show(childFragmentManager, FileUploadDialogFragment.TAG)
} else {
NoInternetConnectionDialog.show(requireFragmentManager())
}
diff --git a/apps/student/src/main/java/com/instructure/student/fragment/FileListFragment.kt b/apps/student/src/main/java/com/instructure/student/fragment/FileListFragment.kt
index 198877c353..e8589d77f1 100644
--- a/apps/student/src/main/java/com/instructure/student/fragment/FileListFragment.kt
+++ b/apps/student/src/main/java/com/instructure/student/fragment/FileListFragment.kt
@@ -29,6 +29,8 @@ import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.widget.PopupMenu
import androidx.fragment.app.DialogFragment
+import androidx.lifecycle.LiveData
+import androidx.work.WorkInfo
import com.instructure.canvasapi2.managers.FileFolderManager
import com.instructure.canvasapi2.models.*
import com.instructure.canvasapi2.utils.ApiPrefs
@@ -44,7 +46,7 @@ import com.instructure.interactions.router.Route
import com.instructure.interactions.router.RouterParams
import com.instructure.pandautils.analytics.SCREEN_VIEW_FILE_LIST
import com.instructure.pandautils.analytics.ScreenView
-import com.instructure.pandautils.dialogs.UploadFilesDialog
+import com.instructure.pandautils.features.file.upload.FileUploadDialogFragment
import com.instructure.pandautils.utils.*
import com.instructure.student.R
import com.instructure.student.adapter.FileFolderCallback
@@ -58,6 +60,7 @@ import kotlinx.android.synthetic.main.fragment_file_list.*
import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode
+import java.util.*
@ScreenView(SCREEN_VIEW_FILE_LIST)
@PageView
@@ -406,8 +409,19 @@ class FileListFragment : ParentFragment(), Bookmarkable {
private fun uploadFile() {
folder?.let {
- val bundle = UploadFilesDialog.createContextBundle(null, canvasContext, it.id)
- UploadFilesDialog.show(fragmentManager, bundle) { _ -> }
+ val bundle = FileUploadDialogFragment.createContextBundle(null, canvasContext, it.id)
+ FileUploadDialogFragment.newInstance(bundle, workerLiveDataCallback = this::workInfoLiveDataCallback).show(childFragmentManager, FileUploadDialogFragment.TAG)
+ }
+ }
+
+ private fun workInfoLiveDataCallback(uuid: UUID, workInfoLiveData: LiveData) {
+ workInfoLiveData.observe(viewLifecycleOwner) {
+ if (it.state == WorkInfo.State.SUCCEEDED) {
+ recyclerAdapter?.refresh()
+ folder?.let {
+ StudentPrefs.staleFolderIds = StudentPrefs.staleFolderIds + it.id
+ }
+ }
}
}
diff --git a/apps/student/src/main/java/com/instructure/student/fragment/InboxComposeMessageFragment.kt b/apps/student/src/main/java/com/instructure/student/fragment/InboxComposeMessageFragment.kt
index 2af3db8363..efa7092d0e 100644
--- a/apps/student/src/main/java/com/instructure/student/fragment/InboxComposeMessageFragment.kt
+++ b/apps/student/src/main/java/com/instructure/student/fragment/InboxComposeMessageFragment.kt
@@ -24,6 +24,8 @@ import android.view.ViewTreeObserver
import android.widget.AdapterView
import android.widget.Toast
import androidx.annotation.StringRes
+import androidx.lifecycle.LiveData
+import androidx.work.WorkInfo
import com.instructure.canvasapi2.managers.CourseManager
import com.instructure.canvasapi2.managers.GroupManager
import com.instructure.canvasapi2.managers.InboxManager
@@ -35,8 +37,9 @@ import com.instructure.canvasapi2.utils.weave.*
import com.instructure.interactions.router.Route
import com.instructure.pandautils.analytics.SCREEN_VIEW_INBOX_COMPOSE
import com.instructure.pandautils.analytics.ScreenView
-import com.instructure.pandautils.dialogs.UploadFilesDialog
-import com.instructure.pandautils.services.FileUploadService
+import com.instructure.pandautils.features.file.upload.FileUploadDialogFragment
+import com.instructure.pandautils.features.file.upload.worker.FileUploadWorker
+import com.instructure.pandautils.utils.fromJson
import com.instructure.pandautils.utils.*
import com.instructure.student.R
import com.instructure.student.adapter.CanvasContextSpinnerAdapter
@@ -51,7 +54,8 @@ import kotlinx.android.synthetic.main.fragment_inbox_compose_message.*
import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode
-import java.util.ArrayList
+import java.util.*
+import kotlin.collections.ArrayList
@ScreenView(SCREEN_VIEW_INBOX_COMPOSE)
class InboxComposeMessageFragment : ParentFragment() {
@@ -279,8 +283,8 @@ class InboxComposeMessageFragment : ParentFragment() {
sendMessage()
}
R.id.menu_attachment -> {
- val bundle = UploadFilesDialog.createMessageAttachmentsBundle(arrayListOf())
- UploadFilesDialog.show(fragmentManager, bundle, { _ -> })
+ val bundle = FileUploadDialogFragment.createMessageAttachmentsBundle(arrayListOf())
+ FileUploadDialogFragment.newInstance(bundle, workerLiveDataCallback = this::fileUploadLiveDataCallback).show(childFragmentManager, FileUploadDialogFragment.TAG)
}
else -> return@setOnMenuItemClickListener false
}
@@ -438,16 +442,15 @@ class InboxComposeMessageFragment : ParentFragment() {
}
}
- @Suppress("unused")
- @Subscribe(threadMode = ThreadMode.MAIN)
- fun onFileUploadedEvent(event: FileUploadEvent) {
- event.get {
- event.remove()
- if(it.intent?.action == FileUploadService.ALL_UPLOADS_COMPLETED) {
- it.attachments.forEach {
- attachments += it
- }
- refreshAttachments()
+ private fun fileUploadLiveDataCallback(uuid: UUID, workInfoLiveData: LiveData) {
+ workInfoLiveData.observe(viewLifecycleOwner) {
+ if (it.state == WorkInfo.State.SUCCEEDED) {
+ it.outputData.getStringArray(FileUploadWorker.RESULT_ATTACHMENTS)
+ ?.map { it.fromJson() }
+ ?.let {
+ this.attachments.addAll(it)
+ refreshAttachments()
+ } ?: toast(R.string.errorUploadingFile)
}
}
}
diff --git a/apps/student/src/main/java/com/instructure/student/fragment/PageDetailsFragment.kt b/apps/student/src/main/java/com/instructure/student/fragment/PageDetailsFragment.kt
index a5633d7aaa..f590d6eb01 100644
--- a/apps/student/src/main/java/com/instructure/student/fragment/PageDetailsFragment.kt
+++ b/apps/student/src/main/java/com/instructure/student/fragment/PageDetailsFragment.kt
@@ -218,8 +218,8 @@ class PageDetailsFragment : InternalWebviewFragment(), Bookmarkable {
checkCanEdit()
}
- private fun loadPageHtml(html: String, contentDescrption: String?) {
- canvasWebView.loadHtml(html, contentDescrption)
+ private fun loadPageHtml(html: String, contentDescription: String?) {
+ canvasWebView.loadHtml(html, contentDescription, baseUrl = page.htmlUrl)
}
/**
diff --git a/apps/student/src/main/java/com/instructure/student/fragment/ParentFragment.kt b/apps/student/src/main/java/com/instructure/student/fragment/ParentFragment.kt
index 321c79d4d3..ab6cd62326 100644
--- a/apps/student/src/main/java/com/instructure/student/fragment/ParentFragment.kt
+++ b/apps/student/src/main/java/com/instructure/student/fragment/ParentFragment.kt
@@ -508,7 +508,7 @@ abstract class ParentFragment : DialogFragment(), FragmentInteractions {
fun openMedia(canvasContext: CanvasContext, url: String, filename: String?) {
val owner = activity ?: return
onMainThread {
- openMediaBundle = OpenMediaAsyncTaskLoader.createBundle(canvasContext, url, filename)
+ openMediaBundle = OpenMediaAsyncTaskLoader.createBundle(url, filename, canvasContext)
LoaderUtils.restartLoaderWithBundle>(LoaderManager.getInstance(owner), openMediaBundle, loaderCallbacks, R.id.openMediaLoaderID)
}
}
diff --git a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/AssignmentDetailsPresenter.kt b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/AssignmentDetailsPresenter.kt
index fef59aa445..4635932219 100644
--- a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/AssignmentDetailsPresenter.kt
+++ b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/AssignmentDetailsPresenter.kt
@@ -248,7 +248,8 @@ object AssignmentDetailsPresenter : Presenter loadDescriptionHtml(html, contentDescription, state.htmlUrl) }, {
+ val args = LtiLaunchFragment.makeLTIBundle(
+ URLDecoder.decode(it, "utf-8"), context.getString(R.string.utils_externalToolTitle), true
+ )
+ RouteMatcher.route(context, Route(LtiLaunchFragment::class.java, canvasContext, args))
+ }, state.assignmentName
+ )
}
if(state.visibilities.quizDetails) renderQuizDetails(state.quizDescriptionViewState!!)
if(state.visibilities.discussionTopicHeader) renderDiscussionTopicHeader(state.discussionHeaderViewState!!)
@@ -230,8 +233,8 @@ class AssignmentDetailsView(
submissionAndRubricLabel.text = context.getText(submissionAndRubricText)
}
- private fun loadDescriptionHtml(html: String, contentDescrption: String?) {
- descriptionWebView.loadHtml(html, contentDescrption)
+ private fun loadDescriptionHtml(html: String, contentDescription: String?, baseUrl: String?) {
+ descriptionWebView.loadHtml(html, contentDescription, baseUrl = baseUrl)
}
private fun renderQuizDetails(quizDescriptionViewState: QuizDescriptionViewState) {
diff --git a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/ui/AssignmentDetailsViewState.kt b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/ui/AssignmentDetailsViewState.kt
index 4b3a2aed76..14dd89aad5 100644
--- a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/ui/AssignmentDetailsViewState.kt
+++ b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/ui/AssignmentDetailsViewState.kt
@@ -46,7 +46,8 @@ sealed class AssignmentDetailsViewState(val visibilities: AssignmentDetailsVisib
val isExternalToolSubmission: Boolean = false,
val quizDescriptionViewState: QuizDescriptionViewState? = null,
val discussionHeaderViewState: DiscussionHeaderViewState? = null,
- val showSubmissionsAndRubric: Boolean = true
+ val showSubmissionsAndRubric: Boolean = true,
+ val htmlUrl: String? = null
) : AssignmentDetailsViewState(assignmentDetailsVisibilities)
}
diff --git a/apps/student/src/main/java/com/instructure/student/mobius/common/ui/SubmissionService.kt b/apps/student/src/main/java/com/instructure/student/mobius/common/ui/SubmissionService.kt
index 3ef22d2e26..959dd19af1 100644
--- a/apps/student/src/main/java/com/instructure/student/mobius/common/ui/SubmissionService.kt
+++ b/apps/student/src/main/java/com/instructure/student/mobius/common/ui/SubmissionService.kt
@@ -550,7 +550,7 @@ class SubmissionService : IntentService(SubmissionService::class.java.simpleName
putExtra(PushNotification.HTML_URL, path)
}
- return PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
+ return PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
}
private fun showErrorNotification(
diff --git a/apps/student/src/main/java/com/instructure/student/router/RouteMatcher.kt b/apps/student/src/main/java/com/instructure/student/router/RouteMatcher.kt
index 96a48a6a7c..a0a6d15a50 100644
--- a/apps/student/src/main/java/com/instructure/student/router/RouteMatcher.kt
+++ b/apps/student/src/main/java/com/instructure/student/router/RouteMatcher.kt
@@ -38,6 +38,7 @@ import com.instructure.canvasapi2.utils.Logger
import com.instructure.interactions.router.*
import com.instructure.pandautils.activities.BaseViewMediaActivity
import com.instructure.pandautils.features.discussion.details.DiscussionDetailsWebViewFragment
+import com.instructure.pandautils.features.shareextension.ShareFileSubmissionTarget
import com.instructure.pandautils.loaders.OpenMediaAsyncTaskLoader
import com.instructure.pandautils.utils.Const
import com.instructure.pandautils.utils.LoaderUtils
diff --git a/apps/student/src/main/java/com/instructure/student/router/RouteResolver.kt b/apps/student/src/main/java/com/instructure/student/router/RouteResolver.kt
index 0fabd50ace..360efc5f8a 100644
--- a/apps/student/src/main/java/com/instructure/student/router/RouteResolver.kt
+++ b/apps/student/src/main/java/com/instructure/student/router/RouteResolver.kt
@@ -4,7 +4,8 @@ import androidx.fragment.app.Fragment
import com.instructure.canvasapi2.models.CanvasContext
import com.instructure.interactions.router.Route
import com.instructure.pandautils.features.discussion.details.DiscussionDetailsWebViewFragment
-import com.instructure.pandautils.features.notification.preferences.NotificationPreferencesFragment
+import com.instructure.pandautils.features.notification.preferences.EmailNotificationPreferencesFragment
+import com.instructure.pandautils.features.notification.preferences.PushNotificationPreferencesFragment
import com.instructure.pandautils.utils.Const
import com.instructure.student.AnnotationComments.AnnotationCommentListFragment
import com.instructure.student.activity.NothingToSeeHereFragment
@@ -121,7 +122,8 @@ object RouteResolver {
cls.isA() -> AnnotationCommentListFragment.newInstance(route)
cls.isA() -> NothingToSeeHereFragment.newInstance()
cls.isA() -> AnnotationSubmissionUploadFragment.newInstance(route)
- cls.isA() -> NotificationPreferencesFragment.newInstance()
+ cls.isA() -> PushNotificationPreferencesFragment.newInstance()
+ cls.isA() -> EmailNotificationPreferencesFragment.newInstance()
cls.isA() -> DiscussionDetailsWebViewFragment.newInstance(route)
cls.isA() -> InternalWebviewFragment.newInstance(route) // Keep this at the end
else -> null
diff --git a/apps/student/src/main/java/com/instructure/student/util/BaseAppManager.kt b/apps/student/src/main/java/com/instructure/student/util/BaseAppManager.kt
index 17cb0fbe34..07d301fec1 100644
--- a/apps/student/src/main/java/com/instructure/student/util/BaseAppManager.kt
+++ b/apps/student/src/main/java/com/instructure/student/util/BaseAppManager.kt
@@ -20,9 +20,6 @@ import android.os.Build
import android.webkit.WebView
import androidx.appcompat.app.AppCompatDelegate
import androidx.core.content.ContextCompat
-import com.google.android.gms.analytics.GoogleAnalytics
-import com.google.android.gms.analytics.HitBuilders
-import com.google.android.gms.analytics.Tracker
import com.google.android.play.core.missingsplits.MissingSplitsManagerFactory
import com.google.firebase.crashlytics.FirebaseCrashlytics
import com.instructure.canvasapi2.utils.*
@@ -53,12 +50,6 @@ import javax.inject.Inject
open class BaseAppManager : com.instructure.canvasapi2.AppManager(), AnalyticsEventHandling {
- // To enable debug logging use: adb shell setprop log.tag.GAv4 DEBUG
- private val defaultTracker: Tracker by lazy {
- val analytics = GoogleAnalytics.getInstance(this)
- analytics.newTracker(R.xml.analytics)
- }
-
override fun onCreate() {
if (MissingSplitsManagerFactory.create(this).disableAppIfMissingRequiredSplits()) {
// Skip app initialization.
@@ -118,94 +109,31 @@ open class BaseAppManager : com.instructure.canvasapi2.AppManager(), AnalyticsEv
override fun onCanvasTokenRefreshed() = FlutterComm.sendUpdatedLogin()
override fun trackButtonPressed(buttonName: String?, buttonValue: Long?) {
- if (buttonName == null) return
-
- if (buttonValue == null) {
- defaultTracker.send(
- HitBuilders.EventBuilder()
- .setCategory("UI Actions")
- .setAction("Button Pressed")
- .setLabel(buttonName)
- .build()
- )
- } else {
- defaultTracker.send(
- HitBuilders.EventBuilder()
- .setCategory("UI Actions")
- .setAction("Button Pressed")
- .setLabel(buttonName)
- .setValue(buttonValue)
- .build()
- )
- }
+
}
override fun trackScreen(screenName: String?) {
- if (screenName == null) return
- val tracker = defaultTracker
- tracker.setScreenName(screenName)
- tracker.send(HitBuilders.ScreenViewBuilder().build())
}
override fun trackEnrollment(enrollmentType: String?) {
- if (enrollmentType == null) return
- defaultTracker.send(
- HitBuilders.AppViewBuilder()
- .setCustomDimension(1, enrollmentType)
- .build()
- )
}
override fun trackDomain(domain: String?) {
- if (domain == null) return
- defaultTracker.send(
- HitBuilders.AppViewBuilder()
- .setCustomDimension(2, domain)
- .build()
- )
}
override fun trackEvent(category: String?, action: String?, label: String?, value: Long) {
- if (category == null || action == null || label == null) return
-
- val tracker = defaultTracker
- tracker.send(
- HitBuilders.EventBuilder()
- .setCategory(category)
- .setAction(action)
- .setLabel(label)
- .setValue(value)
- .build()
- )
+
}
override fun trackUIEvent(action: String?, label: String?, value: Long) {
- if (action == null || label == null) return
-
- defaultTracker.send(
- HitBuilders.EventBuilder()
- .setAction(action)
- .setLabel(label)
- .setValue(value)
- .build()
- )
+
}
override fun trackTiming(category: String?, name: String?, label: String?, duration: Long) {
- if (category == null || name == null || label == null) return
-
- val tracker = defaultTracker
- tracker.send(
- HitBuilders.TimingBuilder()
- .setCategory(category)
- .setLabel(label)
- .setVariable(name)
- .setValue(duration)
- .build()
- )
+
}
private fun initPSPDFKit() {
diff --git a/apps/student/src/main/java/com/instructure/student/util/FileDownloadJobIntentService.kt b/apps/student/src/main/java/com/instructure/student/util/FileDownloadJobIntentService.kt
index 66cdf77db8..6570caec0d 100644
--- a/apps/student/src/main/java/com/instructure/student/util/FileDownloadJobIntentService.kt
+++ b/apps/student/src/main/java/com/instructure/student/util/FileDownloadJobIntentService.kt
@@ -23,7 +23,6 @@ import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
-import android.os.Build
import android.os.Bundle
import android.os.Environment
import android.util.Log
@@ -32,7 +31,6 @@ import androidx.core.app.NotificationCompat
import com.google.firebase.crashlytics.FirebaseCrashlytics
import com.instructure.canvasapi2.models.Attachment
import com.instructure.canvasapi2.models.FileFolder
-import com.instructure.pandautils.services.FileUploadService.Companion.CHANNEL_ID
import com.instructure.student.R
import okhttp3.OkHttpClient
import okhttp3.Request
@@ -55,7 +53,7 @@ class FileDownloadJobIntentService : JobIntentService() {
// Tell Android where to send the user if they click on the notification
val viewDownloadIntent = Intent(DownloadManager.ACTION_VIEW_DOWNLOADS)
- val pendingIntent = PendingIntent.getActivity(this, 0, viewDownloadIntent, 0)
+ val pendingIntent = PendingIntent.getActivity(this, 0, viewDownloadIntent, PendingIntent.FLAG_IMMUTABLE)
// Setup a notification
val notification = NotificationCompat.Builder(this, CHANNEL_ID)
@@ -191,6 +189,8 @@ class FileDownloadJobIntentService : JobIntentService() {
val NOTIFICATION_ID = "notificationid"
val USE_HTTPURLCONNECTION = "usehttpurlconnection"
+ const val CHANNEL_ID = "uploadChannel"
+
// Notification ID is passed into the extras of the job, make sure to use that for any notification updates inside the job
var notificationId = 1
get() = ++field
diff --git a/apps/student/src/main/java/com/instructure/student/util/FileUtils.kt b/apps/student/src/main/java/com/instructure/student/util/FileUtils.kt
index 026a917d4c..428ccbd03d 100644
--- a/apps/student/src/main/java/com/instructure/student/util/FileUtils.kt
+++ b/apps/student/src/main/java/com/instructure/student/util/FileUtils.kt
@@ -24,7 +24,7 @@ import com.instructure.canvasapi2.utils.ApiPrefs
import com.instructure.pandautils.loaders.OpenMediaAsyncTaskLoader
import com.instructure.student.R
import com.instructure.student.activity.CandroidPSPDFActivity
-import com.instructure.student.activity.ShareFileSubmissionTarget
+import com.instructure.pandautils.features.shareextension.ShareFileSubmissionTarget
import com.pspdfkit.PSPDFKit
import com.pspdfkit.annotations.AnnotationType
import com.pspdfkit.configuration.activity.PdfActivityConfiguration
diff --git a/apps/student/src/main/java/com/instructure/student/util/ShortcutUtils.kt b/apps/student/src/main/java/com/instructure/student/util/ShortcutUtils.kt
index ed7cd43934..b7fafb2893 100644
--- a/apps/student/src/main/java/com/instructure/student/util/ShortcutUtils.kt
+++ b/apps/student/src/main/java/com/instructure/student/util/ShortcutUtils.kt
@@ -62,7 +62,7 @@ object ShortcutUtils {
.build()
val successIntent = shortcutManager.createShortcutResultIntent(pinShortcutInfo)
- val pendingIntent = PendingIntent.getBroadcast(context, 0, successIntent, 0)
+ val pendingIntent = PendingIntent.getBroadcast(context, 0, successIntent, PendingIntent.FLAG_IMMUTABLE)
shortcutManager.requestPinShortcut(pinShortcutInfo, pendingIntent.intentSender)
return true
}
diff --git a/apps/student/src/main/java/com/instructure/student/util/UploadCheckboxManager.kt b/apps/student/src/main/java/com/instructure/student/util/UploadCheckboxManager.kt
deleted file mode 100644
index 60f37ac536..0000000000
--- a/apps/student/src/main/java/com/instructure/student/util/UploadCheckboxManager.kt
+++ /dev/null
@@ -1,143 +0,0 @@
-/*
- * Copyright (C) 2016 - present Instructure, Inc.
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, version 3 of the License.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
- *
- */
-package com.instructure.student.util
-
-import android.view.View
-import android.view.ViewTreeObserver
-import android.view.animation.*
-import android.widget.CheckedTextView
-import com.instructure.pandautils.dialogs.UploadFilesDialog
-import com.instructure.student.R
-import java.util.*
-
-class UploadCheckboxManager(private val listener: OnOptionCheckedListener, private val selectionIndicator: View) {
- interface OnOptionCheckedListener {
- fun onUserFilesSelected()
- fun onAssignmentFilesSelected()
- }
-
- private var checkBoxes: MutableList = ArrayList()
-
- var selectedCheckBox: CheckedTextView? = null
- private set
-
- private var isAnimating = false
-
- fun add(checkBox: CheckedTextView) {
- if (checkBoxes.size == 0) {
- selectedCheckBox = checkBox
- setInitialIndicatorHeight()
- }
- checkBoxes.add(checkBox)
- checkBox.setOnClickListener(destinationClickListener)
- }
-
- val selectedType: UploadFilesDialog.FileUploadType
- get() = when (selectedCheckBox?.id) {
- R.id.myFilesCheckBox -> UploadFilesDialog.FileUploadType.USER
- R.id.assignmentCheckBox -> UploadFilesDialog.FileUploadType.ASSIGNMENT
- else -> UploadFilesDialog.FileUploadType.USER
- }
-
- private fun setInitialIndicatorHeight() {
- selectionIndicator.viewTreeObserver.addOnGlobalLayoutListener(
- object : ViewTreeObserver.OnGlobalLayoutListener {
- override fun onGlobalLayout() {
- selectionIndicator.viewTreeObserver.removeOnGlobalLayoutListener(this)
- if (selectedCheckBox != null) {
- selectionIndicator.layoutParams.height = (selectedCheckBox!!.parent as View).height
- selectionIndicator.layoutParams = selectionIndicator.layoutParams
- }
- listener.onUserFilesSelected()
- }
- }
- )
- }
-
- private fun moveIndicator(newCurrentCheckBox: CheckedTextView) {
- val moveAnimation: Animation = getAnimation(newCurrentCheckBox)
- selectionIndicator.startAnimation(moveAnimation)
- moveAnimation.setAnimationListener(object : Animation.AnimationListener {
- override fun onAnimationStart(animation: Animation) {
- isAnimating = true
- }
-
- override fun onAnimationEnd(animation: Animation) {
- selectedCheckBox = newCurrentCheckBox
- isAnimating = false
- }
-
- override fun onAnimationRepeat(animation: Animation) {}
- })
- }
-
- private fun getAnimation(toCheckBox: CheckedTextView): AnimationSet {
- val toView = toCheckBox.parent as View
- val fromView = selectedCheckBox!!.parent as View
-
- // get ratio between current height and new height
- val toRatio =
- toView.height.toFloat() / selectionIndicator.height.toFloat()
- val fromRatio =
- fromView.height.toFloat() / selectionIndicator.height.toFloat()
- val scaleAnimation = ScaleAnimation(
- 1f, // fromXType
- 1f, // toX
- fromRatio, // fromY
- toRatio, // toY
- .5f, // pivotX
- 0.0f
- ) // pivotY
- val translateAnimation = TranslateAnimation(
- Animation.RELATIVE_TO_SELF, 0.0f, // fromXType, fromXValue
- Animation.RELATIVE_TO_SELF, 0.0f, // toXType, toXValue
- Animation.ABSOLUTE, fromView.top.toFloat(), // fromYType, fromYValue
- Animation.ABSOLUTE, toView.top.toFloat()
- ) // toYTyp\e, toYValue
- translateAnimation.interpolator = AccelerateDecelerateInterpolator()
- translateAnimation.fillAfter = true
- val animSet = AnimationSet(true)
- animSet.addAnimation(scaleAnimation)
- animSet.addAnimation(translateAnimation)
- animSet.fillAfter = true
- animSet.duration = 200
- return animSet
- }
-
- private val destinationClickListener = View.OnClickListener { v: View ->
- if (isAnimating) return@OnClickListener
- val checkedTextView = v as CheckedTextView
- if (!checkedTextView.isChecked) {
- checkedTextView.isChecked = true
- notifyListener(checkedTextView)
- moveIndicator(checkedTextView)
- for (checkBox in checkBoxes) {
- if (checkBox.id != checkedTextView.id) {
- checkBox.isChecked = false
- }
- }
- }
- }
-
- private fun notifyListener(checkedTextView: CheckedTextView) {
- when (checkedTextView.id) {
- R.id.myFilesCheckBox -> listener.onUserFilesSelected()
- R.id.assignmentCheckBox -> listener.onAssignmentFilesSelected()
- }
- }
-
-}
diff --git a/apps/student/src/main/java/com/instructure/student/widget/CanvasWidgetProvider.kt b/apps/student/src/main/java/com/instructure/student/widget/CanvasWidgetProvider.kt
index c76f038d01..a03c429ca7 100644
--- a/apps/student/src/main/java/com/instructure/student/widget/CanvasWidgetProvider.kt
+++ b/apps/student/src/main/java/com/instructure/student/widget/CanvasWidgetProvider.kt
@@ -92,7 +92,7 @@ abstract class CanvasWidgetProvider : AppWidgetProvider() {
// Tapping on the logo or title should open the app
val launchMain = Intent(context, LoginActivity::class.java)
- val pendingMainIntent = PendingIntent.getActivity(context, 0, launchMain, 0)
+ val pendingMainIntent = PendingIntent.getActivity(context, 0, launchMain, PendingIntent.FLAG_IMMUTABLE)
remoteViews.setOnClickPendingIntent(R.id.widget_title, pendingMainIntent)
remoteViews.setOnClickPendingIntent(R.id.widget_logo, pendingMainIntent)
diff --git a/apps/student/src/main/java/com/instructure/student/widget/CanvasWidgetRowFactory.kt b/apps/student/src/main/java/com/instructure/student/widget/CanvasWidgetRowFactory.kt
index a4c1f00d7b..0fbdaea5b9 100644
--- a/apps/student/src/main/java/com/instructure/student/widget/CanvasWidgetRowFactory.kt
+++ b/apps/student/src/main/java/com/instructure/student/widget/CanvasWidgetRowFactory.kt
@@ -100,7 +100,7 @@ abstract class CanvasWidgetRowFactory : RemoteViewsService.RemoteViewsFactory
//create log in intent
val intent = LoginActivity.createIntent(ContextKeeper.appContext)
val pendingIntent = PendingIntent.getActivity(
- ContextKeeper.appContext, CanvasWidgetProvider.cycleBit++, intent, PendingIntent.FLAG_UPDATE_CURRENT)
+ ContextKeeper.appContext, CanvasWidgetProvider.cycleBit++, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
row.setOnClickPendingIntent(R.id.widget_root, pendingIntent)
true
} else {
diff --git a/apps/student/src/main/java/com/instructure/student/widget/GradesWidgetProvider.kt b/apps/student/src/main/java/com/instructure/student/widget/GradesWidgetProvider.kt
index 1456b160e9..031e2bf43c 100644
--- a/apps/student/src/main/java/com/instructure/student/widget/GradesWidgetProvider.kt
+++ b/apps/student/src/main/java/com/instructure/student/widget/GradesWidgetProvider.kt
@@ -44,9 +44,22 @@ class GradesWidgetProvider : CanvasWidgetProvider() {
remoteViews.setTextColor(R.id.widget_title, textColor)
val listViewItemIntent = Intent(context, InterwebsToApplication::class.java)
- remoteViews.setPendingIntentTemplate(R.id.contentList, PendingIntent.getActivity(context, CanvasWidgetProvider.cycleBit++, listViewItemIntent, PendingIntent.FLAG_UPDATE_CURRENT))
+ remoteViews.setPendingIntentTemplate(
+ R.id.contentList,
+ PendingIntent.getActivity(
+ context,
+ CanvasWidgetProvider.cycleBit++,
+ listViewItemIntent,
+ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE
+ )
+ )
- val pendingRefreshIntent = PendingIntent.getBroadcast(context, refreshIntentID, getRefreshIntent(appWidgetManager), PendingIntent.FLAG_UPDATE_CURRENT)
+ val pendingRefreshIntent = PendingIntent.getBroadcast(
+ context,
+ refreshIntentID,
+ getRefreshIntent(appWidgetManager),
+ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
+ )
remoteViews.setOnClickPendingIntent(R.id.widget_refresh, pendingRefreshIntent)
}
diff --git a/apps/student/src/main/java/com/instructure/student/widget/NotificationViewWidgetService.kt b/apps/student/src/main/java/com/instructure/student/widget/NotificationViewWidgetService.kt
index a529985323..a0c8107b21 100644
--- a/apps/student/src/main/java/com/instructure/student/widget/NotificationViewWidgetService.kt
+++ b/apps/student/src/main/java/com/instructure/student/widget/NotificationViewWidgetService.kt
@@ -141,7 +141,7 @@ class NotificationViewWidgetService : BaseRemoteViewsService(), Serializable {
val courses = CourseManager.getCoursesSynchronous(true)
.filter { it.isFavorite && !it.accessRestrictedByDate && !it.isInvited() }
val groups = GroupManager.getFavoriteGroupsSynchronous(false)
- val userStream = StreamManager.getUserStreamSynchronous(25, false).toMutableList()
+ val userStream = StreamManager.getUserStreamSynchronous(25, true).toMutableList()
userStream.sort()
userStream.reverse()
diff --git a/apps/student/src/main/java/com/instructure/student/widget/NotificationWidgetProvider.kt b/apps/student/src/main/java/com/instructure/student/widget/NotificationWidgetProvider.kt
index c1e6a45e17..bcb9e67a45 100644
--- a/apps/student/src/main/java/com/instructure/student/widget/NotificationWidgetProvider.kt
+++ b/apps/student/src/main/java/com/instructure/student/widget/NotificationWidgetProvider.kt
@@ -45,9 +45,22 @@ class NotificationWidgetProvider : CanvasWidgetProvider() {
remoteViews.setTextColor(R.id.widget_title, textColor)
val listViewItemIntent = Intent(context, NotificationWidgetRouter::class.java)
- remoteViews.setPendingIntentTemplate(R.id.contentList, PendingIntent.getActivity(context, CanvasWidgetProvider.cycleBit++, listViewItemIntent, PendingIntent.FLAG_UPDATE_CURRENT))
+ remoteViews.setPendingIntentTemplate(
+ R.id.contentList,
+ PendingIntent.getActivity(
+ context,
+ CanvasWidgetProvider.cycleBit++,
+ listViewItemIntent,
+ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE
+ )
+ )
- val pendingRefreshIntent = PendingIntent.getBroadcast(context, refreshIntentID, getRefreshIntent(appWidgetManager), PendingIntent.FLAG_UPDATE_CURRENT)
+ val pendingRefreshIntent = PendingIntent.getBroadcast(
+ context,
+ refreshIntentID,
+ getRefreshIntent(appWidgetManager),
+ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
+ )
remoteViews.setOnClickPendingIntent(R.id.widget_refresh, pendingRefreshIntent)
}
diff --git a/apps/student/src/main/java/com/instructure/student/widget/TodoWidgetProvider.kt b/apps/student/src/main/java/com/instructure/student/widget/TodoWidgetProvider.kt
index 841cb89370..2163f0883f 100644
--- a/apps/student/src/main/java/com/instructure/student/widget/TodoWidgetProvider.kt
+++ b/apps/student/src/main/java/com/instructure/student/widget/TodoWidgetProvider.kt
@@ -45,9 +45,22 @@ class TodoWidgetProvider : CanvasWidgetProvider() {
remoteViews.setTextColor(R.id.widget_title, textColor)
val listViewItemIntent = Intent(context, InterwebsToApplication::class.java)
- remoteViews.setPendingIntentTemplate(R.id.contentList, PendingIntent.getActivity(context, CanvasWidgetProvider.cycleBit++, listViewItemIntent, PendingIntent.FLAG_UPDATE_CURRENT))
+ remoteViews.setPendingIntentTemplate(
+ R.id.contentList,
+ PendingIntent.getActivity(
+ context,
+ CanvasWidgetProvider.cycleBit++,
+ listViewItemIntent,
+ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE
+ )
+ )
- val pendingRefreshIntent = PendingIntent.getBroadcast(context, refreshIntentID, getRefreshIntent(appWidgetManager), PendingIntent.FLAG_UPDATE_CURRENT)
+ val pendingRefreshIntent = PendingIntent.getBroadcast(
+ context,
+ refreshIntentID,
+ getRefreshIntent(appWidgetManager),
+ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
+ )
remoteViews.setOnClickPendingIntent(R.id.widget_refresh, pendingRefreshIntent)
}
diff --git a/apps/student/src/main/res/layout-sw720dp/fragment_elementary_course.xml b/apps/student/src/main/res/layout-sw720dp/fragment_elementary_course.xml
index 997415dc81..c1cd5d7bfe 100644
--- a/apps/student/src/main/res/layout-sw720dp/fragment_elementary_course.xml
+++ b/apps/student/src/main/res/layout-sw720dp/fragment_elementary_course.xml
@@ -103,8 +103,7 @@
android:background="@{ColorUtils.parseColor(course.courseColor)}"
android:importantForAccessibility="no"
android:scaleType="centerCrop"
- app:imageUrl="@{course.imageUrl}"
- app:overlayColor="@{ColorUtils.parseColor(course.courseColor)}"
+ app:imageUrl="@{course.bannerImageUrl != null ? course.bannerImageUrl : course.imageUrl}"
tools:background="@color/backgroundWarning" />
+
+
+ android:orientation="horizontal"
+ android:id="@+id/statusDetails">
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/apps/student/src/main/res/values/styles.xml b/apps/student/src/main/res/values/styles.xml
index 94fd5a9aa0..0575a7b29c 100644
--- a/apps/student/src/main/res/values/styles.xml
+++ b/apps/student/src/main/res/values/styles.xml
@@ -110,7 +110,7 @@