diff --git a/PULL_REQUEST_TEMPLATE b/PULL_REQUEST_TEMPLATE new file mode 100644 index 0000000000..b56e60264e --- /dev/null +++ b/PULL_REQUEST_TEMPLATE @@ -0,0 +1,16 @@ +--- edit or delete this section --- +## Screenshots + + + + + + + +
BeforeAfter
+ +## Checklist + +- [ ] Follow-up e2e test ticket created or not needed +- [ ] A11y checked +- [ ] Approve from product or not needed diff --git a/apps/flutter_parent/android/app/build.gradle b/apps/flutter_parent/android/app/build.gradle index ea3bc8685a..532b872ab9 100644 --- a/apps/flutter_parent/android/app/build.gradle +++ b/apps/flutter_parent/android/app/build.gradle @@ -78,6 +78,19 @@ android { shrinkResources false // Must be false, otherwise resources we need are erroneously stripped out proguardFiles 'proguard-rules.pro' } + applicationVariants.all{ + variant -> + variant.outputs.each{ + output-> + project.ext { appName = 'parent' } + def dateTimeStamp = new Date().format('yyyy-MM-dd-HH-mm-ss') + def newName = output.outputFile.name + newName = newName.replace("app-", "$project.ext.appName-") + newName = newName.replace("-debug", "-dev-debug-" + dateTimeStamp) + newName = newName.replace("-release", "-prod-release") + output.outputFileName = newName + } + } } } diff --git a/apps/flutter_parent/android/app/src/main/AndroidManifest.xml b/apps/flutter_parent/android/app/src/main/AndroidManifest.xml index eb3ec7352a..f4245b5921 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"> @@ -88,5 +89,15 @@ + + + + diff --git a/apps/flutter_parent/lib/l10n/app_localizations.dart b/apps/flutter_parent/lib/l10n/app_localizations.dart index 2d06702a2d..f8c753fd9d 100644 --- a/apps/flutter_parent/lib/l10n/app_localizations.dart +++ b/apps/flutter_parent/lib/l10n/app_localizations.dart @@ -296,6 +296,12 @@ class AppLocalizations { desc: 'Text for the find-my-school button', ); + String get findAnotherSchool => Intl.message( + 'Find another school', + name: 'findAnotherSchool', + desc: 'Text for the find-another-school button', + ); + /// Domain search screen String get domainSearchInputHint => Intl.message( diff --git a/apps/flutter_parent/lib/l10n/res/intl_en.arb b/apps/flutter_parent/lib/l10n/res/intl_en.arb index a7ce42583b..e2b0149f61 100644 --- a/apps/flutter_parent/lib/l10n/res/intl_en.arb +++ b/apps/flutter_parent/lib/l10n/res/intl_en.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2022-01-28T12:37:40.360857", + "@@last_modified": "2022-10-06T10:11:41.858017", "alertsLabel": "Alerts", "@alertsLabel": { "description": "The label for the Alerts tab", @@ -259,6 +259,13 @@ "placeholders_order": [], "placeholders": {} }, + "findAnotherSchool": "Find another school", + "@findAnotherSchool": { + "description": "Text for the find-another-school button", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, "domainSearchInputHint": "Enter school name or district…", "@domainSearchInputHint": { "description": "Input hint for the text box on the domain search screen", diff --git a/apps/flutter_parent/lib/l10n/res/intl_en_GB_instukhe.arb b/apps/flutter_parent/lib/l10n/res/intl_en_GB_instukhe.arb index 72519075a7..2e89e929cc 100644 --- a/apps/flutter_parent/lib/l10n/res/intl_en_GB_instukhe.arb +++ b/apps/flutter_parent/lib/l10n/res/intl_en_GB_instukhe.arb @@ -2660,4 +2660,4 @@ "placeholders_order": [], "placeholders": {} } -} \ No newline at end of file +} diff --git a/apps/flutter_parent/lib/l10n/res/intl_messages.arb b/apps/flutter_parent/lib/l10n/res/intl_messages.arb index a7ce42583b..e2b0149f61 100644 --- a/apps/flutter_parent/lib/l10n/res/intl_messages.arb +++ b/apps/flutter_parent/lib/l10n/res/intl_messages.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2022-01-28T12:37:40.360857", + "@@last_modified": "2022-10-06T10:11:41.858017", "alertsLabel": "Alerts", "@alertsLabel": { "description": "The label for the Alerts tab", @@ -259,6 +259,13 @@ "placeholders_order": [], "placeholders": {} }, + "findAnotherSchool": "Find another school", + "@findAnotherSchool": { + "description": "Text for the find-another-school button", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, "domainSearchInputHint": "Enter school name or district…", "@domainSearchInputHint": { "description": "Input hint for the text box on the domain search screen", diff --git a/apps/flutter_parent/lib/main.dart b/apps/flutter_parent/lib/main.dart index 1140648ed2..102b50fc81 100644 --- a/apps/flutter_parent/lib/main.dart +++ b/apps/flutter_parent/lib/main.dart @@ -14,6 +14,8 @@ import 'dart:async'; import 'dart:io'; +import 'dart:isolate'; +import 'dart:ui'; import 'package:device_info/device_info.dart'; import 'package:firebase_core/firebase_core.dart'; @@ -49,6 +51,8 @@ void main() async { ]); PandaRouter.init(); + await FlutterDownloader.registerCallback(downloadCallback); + // This completer waits for the app to be built before allowing the notificationUtil to handle notifications final Completer _appCompleter = Completer(); NotificationUtil.init(_appCompleter); @@ -68,3 +72,10 @@ void main() async { runApp(ParentApp(_appCompleter)); }, FirebaseCrashlytics.instance.recordError); } + +@pragma('vm:entry-point') +void downloadCallback(String id, DownloadTaskStatus status, int progress) { + final SendPort send = + IsolateNameServer.lookupPortByName('downloader_send_port'); + send.send([id, status, progress]); +} 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/network/utils/api_prefs.dart b/apps/flutter_parent/lib/network/utils/api_prefs.dart index 1b44ba5dc4..a4fb8d0a99 100644 --- a/apps/flutter_parent/lib/network/utils/api_prefs.dart +++ b/apps/flutter_parent/lib/network/utils/api_prefs.dart @@ -19,9 +19,11 @@ import 'dart:ui' as ui; import 'package:encrypted_shared_preferences/encrypted_shared_preferences.dart'; import 'package:flutter/material.dart'; import 'package:flutter_parent/models/login.dart'; +import 'package:flutter_parent/models/school_domain.dart'; import 'package:flutter_parent/models/serializers.dart'; import 'package:flutter_parent/models/user.dart'; import 'package:flutter_parent/network/api/auth_api.dart'; +import 'package:flutter_parent/screens/web_login/web_login_screen.dart'; import 'package:flutter_parent/utils/db/calendar_filter_db.dart'; import 'package:flutter_parent/utils/db/reminder_db.dart'; import 'package:flutter_parent/utils/notification_util.dart'; @@ -29,6 +31,7 @@ import 'package:flutter_parent/utils/service_locator.dart'; import 'package:intl/intl.dart'; import 'package:package_info/package_info.dart'; import 'package:shared_preferences/shared_preferences.dart'; +import 'package:tuple/tuple.dart'; import 'dio_config.dart'; @@ -42,6 +45,8 @@ class ApiPrefs { static const String KEY_LOGINS = 'logins'; static const String KEY_RATING_DONT_SHOW_AGAIN = 'dont_show_again'; static const String KEY_RATING_NEXT_SHOW_DATE = 'next_show_date'; + static const String KEY_LAST_ACCOUNT = 'last_account'; + static const String KEY_LAST_ACCOUNT_LOGIN_FLOW = 'last_account_login_flow'; static EncryptedSharedPreferences _prefs; static PackageInfo _packageInfo; @@ -167,6 +172,26 @@ class ApiPrefs { return _prefs.getStringList(KEY_LOGINS)?.map((it) => deserialize(json.decode(it)))?.toList() ?? []; } + static setLastAccount(SchoolDomain lastAccount, LoginFlow loginFlow) { + _checkInit(); + final lastAccountJson = json.encode(serialize(lastAccount)); + _prefs.setString(KEY_LAST_ACCOUNT, lastAccountJson); + _prefs.setInt(KEY_LAST_ACCOUNT_LOGIN_FLOW, loginFlow.index); + } + + static Tuple2 getLastAccount() { + _checkInit(); + if (!_prefs.containsKey(KEY_LAST_ACCOUNT)) return null; + + final accountJson = _prefs.getString(KEY_LAST_ACCOUNT); + if (accountJson == null || accountJson.isEmpty) return null; + + final lastAccount = deserialize(json.decode(accountJson)); + final loginFlow = _prefs.containsKey(KEY_LAST_ACCOUNT_LOGIN_FLOW) ? LoginFlow.values[_prefs.getInt(KEY_LAST_ACCOUNT_LOGIN_FLOW)] : LoginFlow.normal; + + return Tuple2(lastAccount, loginFlow); + } + static Future removeLogin(Login login) => removeLoginByUuid(login.uuid); static Future removeLoginByUuid(String uuid) async { diff --git a/apps/flutter_parent/lib/router/panda_router.dart b/apps/flutter_parent/lib/router/panda_router.dart index 882314e745..232e20029d 100644 --- a/apps/flutter_parent/lib/router/panda_router.dart +++ b/apps/flutter_parent/lib/router/panda_router.dart @@ -115,10 +115,11 @@ class PandaRouter { static String loginWeb( String domain, { + String accountName = '', String authenticationProvider = null, LoginFlow loginFlow = LoginFlow.normal, }) => - '$_loginWeb?${_RouterKeys.domain}=${Uri.encodeQueryComponent(domain)}&${_RouterKeys.authenticationProvider}=$authenticationProvider&${_RouterKeys.loginFlow}=${loginFlow.toString()}'; + '$_loginWeb?${_RouterKeys.domain}=${Uri.encodeQueryComponent(domain)}&${_RouterKeys.accountName}=${Uri.encodeQueryComponent(accountName)}&${_RouterKeys.authenticationProvider}=$authenticationProvider&${_RouterKeys.loginFlow}=${loginFlow.toString()}'; static String notParent() => '/not_parent'; @@ -325,6 +326,7 @@ class PandaRouter { return WebLoginScreen( params[_RouterKeys.domain][0], + accountName: params[_RouterKeys.accountName][0], authenticationProvider: authProvider, loginFlow: loginFlow, ); @@ -507,6 +509,7 @@ class _RouterKeys { static final calendarView = 'view_name'; static final courseId = 'courseId'; static final domain = 'domain'; + static final accountName = 'accountName'; static final eventId = 'eventId'; static final infoText = 'infoText'; static final isCreatingAccount = 'isCreatingAccount'; 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/screens/courses/details/course_grades_screen.dart b/apps/flutter_parent/lib/screens/courses/details/course_grades_screen.dart index 62d53352ee..bbe10a671c 100644 --- a/apps/flutter_parent/lib/screens/courses/details/course_grades_screen.dart +++ b/apps/flutter_parent/lib/screens/courses/details/course_grades_screen.dart @@ -371,7 +371,10 @@ class _AssignmentRow extends StatelessWidget { final localizations = L10n(context); final submission = assignment.submission(studentId); - if (submission?.grade != null) { + if (submission?.excused ?? false) { + text = localizations.gradeFormatScoreOutOfPointsPossible(localizations.excused, points); + semantics = localizations.contentDescriptionScoreOutOfPointsPossible('', points); + } else if (submission?.grade != null) { text = localizations.gradeFormatScoreOutOfPointsPossible(submission.grade, points); semantics = localizations.contentDescriptionScoreOutOfPointsPossible(submission.grade, points); } else { diff --git a/apps/flutter_parent/lib/screens/domain_search/domain_search_screen.dart b/apps/flutter_parent/lib/screens/domain_search/domain_search_screen.dart index df3622532b..06a752f13a 100644 --- a/apps/flutter_parent/lib/screens/domain_search/domain_search_screen.dart +++ b/apps/flutter_parent/lib/screens/domain_search/domain_search_screen.dart @@ -195,8 +195,11 @@ class _DomainSearchScreenState extends State { var item = _schoolDomains[index]; return ListTile( title: Text(item.name), - onTap: () => locator().pushRoute(context, - PandaRouter.loginWeb(item.domain, authenticationProvider: item.authenticationProvider)), + onTap: () { + final accountName = (item.name == null || item.name.isEmpty) ? item.domain : item.name; + return locator().pushRoute(context, + PandaRouter.loginWeb(item.domain, accountName: accountName, authenticationProvider: item.authenticationProvider)); + }, ); }, ), @@ -280,6 +283,6 @@ class _DomainSearchScreenState extends State { if (regExp.hasMatch(domain)) domain = regExp.stringMatch(domain); if (domain.startsWith('www.')) domain = domain.substring(4); // Strip off www. if they typed it if (!domain.contains('.') || domain.endsWith('.beta')) domain += '.instructure.com'; - locator().pushRoute(context, PandaRouter.loginWeb(domain, loginFlow: widget.loginFlow)); + locator().pushRoute(context, PandaRouter.loginWeb(domain, accountName: domain, loginFlow: widget.loginFlow)); } } diff --git a/apps/flutter_parent/lib/screens/login_landing_screen.dart b/apps/flutter_parent/lib/screens/login_landing_screen.dart index b9c9d02bd6..47ca747713 100644 --- a/apps/flutter_parent/lib/screens/login_landing_screen.dart +++ b/apps/flutter_parent/lib/screens/login_landing_screen.dart @@ -17,6 +17,7 @@ import 'dart:math'; import 'package:flutter/material.dart'; import 'package:flutter_parent/l10n/app_localizations.dart'; import 'package:flutter_parent/models/login.dart'; +import 'package:flutter_parent/models/school_domain.dart'; import 'package:flutter_parent/network/utils/analytics.dart'; import 'package:flutter_parent/network/utils/api_prefs.dart'; import 'package:flutter_parent/router/panda_router.dart'; @@ -38,6 +39,7 @@ import 'package:flutter_parent/utils/service_locator.dart'; import 'package:flutter_parent/utils/snickers.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:flutter_svg/svg.dart'; +import 'package:tuple/tuple.dart'; class LoginLandingScreen extends StatelessWidget { final GlobalKey _scaffoldKey = GlobalKey(); @@ -100,6 +102,7 @@ class LoginLandingScreen extends StatelessWidget { } Widget _body(BuildContext context) { + final lastLoginAccount = ApiPrefs.getLastAccount(); return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, @@ -109,24 +112,23 @@ class LoginLandingScreen extends StatelessWidget { semanticsLabel: L10n(context).canvasLogoLabel, ), SizedBox(height: 64), - ButtonTheme( - minWidth: 260, - child: RaisedButton( - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Text( - L10n(context).findSchool, - style: TextStyle(fontSize: 16), - ), - ), - color: Theme.of(context).accentColor, - textColor: Colors.white, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(4))), - onPressed: () { - onFindSchoolPressed(context); - }, - ), - ), + if (lastLoginAccount == null) + _filledButton(context, L10n(context).findSchool, () { + onFindSchoolPressed(context); + }), + if (lastLoginAccount != null) + _filledButton( + context, + lastLoginAccount.item1.name == null || lastLoginAccount.item1.name.isEmpty + ? lastLoginAccount.item1.domain + : lastLoginAccount.item1.name, () { + onSavedSchoolPressed(context, lastLoginAccount); + }), + SizedBox(height: 16), + if (lastLoginAccount != null) + _outlineButton(context, L10n(context).findAnotherSchool, () { + onFindSchoolPressed(context); + }), SizedBox(height: 8), if (_hasCameras()) _qrLogin(context), ], @@ -134,6 +136,51 @@ class LoginLandingScreen extends StatelessWidget { ); } + Widget _filledButton( + BuildContext context, String title, VoidCallback onPressed) { + return ButtonTheme( + minWidth: 260, + child: RaisedButton( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Text( + title, + style: TextStyle(fontSize: 16), + ), + ), + color: Theme.of(context).accentColor, + textColor: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(4))), + onPressed: onPressed, + ), + ); + } + + Widget _outlineButton( + BuildContext context, String title, VoidCallback onPressed) { + return ButtonTheme( + minWidth: 260, + child: OutlinedButton( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Text( + title, + style: Theme.of(context).textTheme.subtitle1, + ), + ), + style: OutlinedButton.styleFrom( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(4.0), + ), + side: BorderSide( + width: 1, color: ParentTheme.of(context).onSurfaceColor), + ), + onPressed: onPressed, + ), + ); + } + bool _hasCameras() { return ApiPrefs.getCameraCount() != null && ApiPrefs.getCameraCount() != 0; } @@ -233,36 +280,18 @@ class LoginLandingScreen extends StatelessWidget { ); } - Widget _helpRequestButton(BuildContext context) { - return Semantics( - label: L10n(context).loginHelpHint, - child: GestureDetector( - onTap: () { - locator().logEvent(AnalyticsEventConstants.HELP_LOGIN); - ErrorReportDialog.asDialog(context, - title: L10n(context).loginHelpTitle, - subject: L10n(context).loginHelpSubject, - severity: ErrorReportSeverity.BLOCKING, - includeEmail: true, - hideSeverityPicker: true); - }, - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Align( - child: Icon( - CanvasIcons.question, - color: ParentColors.ash, - ), - alignment: Alignment.topRight, - ), - ), - ), - ); - } - onFindSchoolPressed(BuildContext context) { LoginFlow flow = LoginFlow.values[loginFlowIndex % LoginFlow.values.length]; - locator().pushRoute(context, PandaRouter.domainSearch(loginFlow: flow)); + locator() + .pushRoute(context, PandaRouter.domainSearch(loginFlow: flow)); + } + + onSavedSchoolPressed(BuildContext context, Tuple2 lastAccount) { + locator().pushRoute( + context, + PandaRouter.loginWeb(lastAccount.item1.domain, + accountName: lastAccount.item1.name, + loginFlow: lastAccount.item2)); } void _changeLoginFlow(BuildContext context) { diff --git a/apps/flutter_parent/lib/screens/web_login/web_login_screen.dart b/apps/flutter_parent/lib/screens/web_login/web_login_screen.dart index 5743495bc0..c63a4cd388 100644 --- a/apps/flutter_parent/lib/screens/web_login/web_login_screen.dart +++ b/apps/flutter_parent/lib/screens/web_login/web_login_screen.dart @@ -18,6 +18,7 @@ import 'package:device_info/device_info.dart'; import 'package:flutter/material.dart'; import 'package:flutter_parent/l10n/app_localizations.dart'; import 'package:flutter_parent/models/mobile_verify_result.dart'; +import 'package:flutter_parent/models/school_domain.dart'; import 'package:flutter_parent/network/utils/analytics.dart'; import 'package:flutter_parent/network/utils/api_prefs.dart'; import 'package:flutter_parent/router/panda_router.dart'; @@ -37,8 +38,8 @@ enum LoginFlow { } class WebLoginScreen extends StatefulWidget { - WebLoginScreen( - this.domain, { + WebLoginScreen(this.domain, { + this.accountName, this.user, this.pass, this.authenticationProvider, @@ -47,6 +48,7 @@ class WebLoginScreen extends StatefulWidget { }) : super(key: key); final String user; + final String accountName; final String pass; final String domain; final String authenticationProvider; @@ -200,7 +202,13 @@ class _WebLoginScreenState extends State { AnalyticsEventConstants.LOGIN_SUCCESS, extras: {AnalyticsParamConstants.DOMAIN_PARAM: result.baseUrl}, ); - locator().pushRouteAndClearStack(context, PandaRouter.rootSplash()); + final lastAccount = new SchoolDomain((builder) => + builder + ..domain = widget.domain + ..name = widget.accountName); + ApiPrefs.setLastAccount(lastAccount, widget.loginFlow); + locator().pushRouteAndClearStack( + context, PandaRouter.rootSplash()); }).catchError((_) { locator().logEvent( AnalyticsEventConstants.LOGIN_FAILURE, 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/lib/utils/veneers/flutter_downloader_veneer.dart b/apps/flutter_parent/lib/utils/veneers/flutter_downloader_veneer.dart index 7933ed64db..1fdd0c3ced 100644 --- a/apps/flutter_parent/lib/utils/veneers/flutter_downloader_veneer.dart +++ b/apps/flutter_parent/lib/utils/veneers/flutter_downloader_veneer.dart @@ -24,7 +24,7 @@ class FlutterDownloaderVeneer { bool showNotification = true, bool openFileFromNotification = true, bool requiresStorageNotLow = true, - }) => + bool saveInPublicStorage = true}) => FlutterDownloader.enqueue( url: url, savedDir: savedDir, @@ -32,7 +32,8 @@ class FlutterDownloaderVeneer { headers: headers, showNotification: showNotification, openFileFromNotification: openFileFromNotification, - requiresStorageNotLow: requiresStorageNotLow); + requiresStorageNotLow: requiresStorageNotLow, + saveInPublicStorage: saveInPublicStorage); Future> loadTasks() => FlutterDownloader.loadTasks(); diff --git a/apps/flutter_parent/pubspec.lock b/apps/flutter_parent/pubspec.lock index 3def51903f..5f436e3e84 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: @@ -453,7 +425,7 @@ packages: name: flutter_downloader url: "https://pub.dartlang.org" source: hosted - version: "1.7.1" + version: "1.7.4" flutter_driver: dependency: "direct dev" description: flutter diff --git a/apps/flutter_parent/pubspec.yaml b/apps/flutter_parent/pubspec.yaml index 98699a76f3..566c85fc5f 100644 --- a/apps/flutter_parent/pubspec.yaml +++ b/apps/flutter_parent/pubspec.yaml @@ -25,7 +25,7 @@ description: Canvas Parent # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -version: 3.4.0+42 +version: 3.5.0+43 module: androidX: true @@ -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 @@ -59,7 +58,7 @@ dependencies: # File handling path_provider: ^2.0.6 - flutter_downloader: ^1.7.1 + flutter_downloader: 1.7.4 mime: ^1.0.1 file_picker: ^4.2.0 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 bf0f74afac..7d86ad0261 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 = 238 - versionName = '6.17.1' + versionCode = 243 + versionName = '6.20.0' vectorDrawables.useSupportLibrary = true multiDexEnabled = true @@ -182,11 +182,6 @@ android { resolutionStrategy.force Libs.ANDROIDX_ANNOTATION resolutionStrategy.force Libs.KOTLIN_COROUTINES_CORE - - // Some libraries want to resolve never versions of this library that requires targetSdkVersion 31. - // Once we upgrade the targetSdkVersion this should be removed. - resolutionStrategy.force 'androidx.core:core:1.6.0' - resolutionStrategy.force 'androidx.core:core-ktx:1.6.0' } /* @@ -271,12 +266,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 +302,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 +325,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.yml b/apps/student/flank.yml index c2a137af89..c1c32355e7 100644 --- a/apps/student/flank.yml +++ b/apps/student/flank.yml @@ -14,7 +14,7 @@ gcloud: test-targets: - notAnnotation com.instructure.canvas.espresso.E2E, com.instructure.canvas.espresso.Stub, com.instructure.canvas.espresso.FlakyE2E, com.instructure.canvas.espresso.KnownBug device: - - model: NexusLowRes + - model: Nexus6P version: 26 locale: en_US orientation: portrait diff --git a/apps/student/flank_e2e.yml b/apps/student/flank_e2e.yml index d7862027cb..f494132881 100644 --- a/apps/student/flank_e2e.yml +++ b/apps/student/flank_e2e.yml @@ -15,7 +15,7 @@ gcloud: - annotation com.instructure.canvas.espresso.E2E - notAnnotation com.instructure.canvas.espresso.Stub, com.instructure.canvas.espresso.FlakyE2E, com.instructure.canvas.espresso.KnownBug device: - - model: NexusLowRes + - model: Nexus6P version: 26 locale: en_US orientation: portrait diff --git a/apps/student/flank_e2e_coverage.yml b/apps/student/flank_e2e_coverage.yml index 87e66ca5e4..ef7cccbfd2 100644 --- a/apps/student/flank_e2e_coverage.yml +++ b/apps/student/flank_e2e_coverage.yml @@ -22,7 +22,7 @@ gcloud: - annotation com.instructure.canvas.espresso.E2E - notAnnotation com.instructure.canvas.espresso.Stub device: - - model: NexusLowRes + - model: Nexus6P version: 26 locale: en_US orientation: portrait diff --git a/apps/student/flank_e2e_flaky.yml b/apps/student/flank_e2e_flaky.yml index d0693d5bcc..9a73a6fd64 100644 --- a/apps/student/flank_e2e_flaky.yml +++ b/apps/student/flank_e2e_flaky.yml @@ -14,7 +14,7 @@ gcloud: test-targets: - annotation com.instructure.canvas.espresso.FlakyE2E device: - - model: NexusLowRes + - model: Nexus6P version: 26 locale: en_US orientation: portrait diff --git a/apps/student/flank_e2e_knownbug.yml b/apps/student/flank_e2e_knownbug.yml index 88fabb644b..b1cb8ff730 100644 --- a/apps/student/flank_e2e_knownbug.yml +++ b/apps/student/flank_e2e_knownbug.yml @@ -14,7 +14,7 @@ gcloud: test-targets: - annotation com.instructure.canvas.espresso.KnownBug device: - - model: NexusLowRes + - model: Nexus6P version: 26 locale: en_US orientation: portrait diff --git a/apps/student/flank_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 c9c633567f..dd25260b89 100644 --- a/apps/student/flank_multi_api_level.yml +++ b/apps/student/flank_multi_api_level.yml @@ -12,7 +12,7 @@ gcloud: record-video: true timeout: 60m test-targets: - - notAnnotation com.instructure.canvas.espresso.E2E, com.instructure.canvas.espresso.Stub + - notAnnotation com.instructure.canvas.espresso.E2E, com.instructure.canvas.espresso.Stub, com.instructure.canvas.espresso.StubMultiAPILevel, com.instructure.canvas.espresso.FlakyE2E, com.instructure.canvas.espresso.KnownBug device: - model: NexusLowRes version: 27 diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/AnnouncementsE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/AnnouncementsE2ETest.kt index 6b03be97c9..6a980dbeaa 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/AnnouncementsE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/AnnouncementsE2ETest.kt @@ -52,7 +52,7 @@ class AnnouncementsE2ETest : StudentTest() { val announcement = data.announcementsList[0] val secondAnnouncement = data.announcementsList[1] - Log.d(STEP_TAG,"Login with user: ${student.name}, login id: ${student.loginId} , password: ${student.password}") + Log.d(STEP_TAG,"Login with user: ${student.name}, login id: ${student.loginId}.") tokenLogin(student) dashboardPage.waitForRender() 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..972fef2d13 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 @@ -77,7 +77,7 @@ class AssignmentsE2ETest: StudentTest() { dueAt = 1.days.fromNow.iso8601 )) - Log.d(STEP_TAG, "Login with user: ${student.name}, login id: ${student.loginId} , password: ${student.password}") + Log.d(STEP_TAG, "Login with user: ${student.name}, login id: ${student.loginId}.") tokenLogin(student) dashboardPage.waitForRender() @@ -166,7 +166,7 @@ class AssignmentsE2ETest: StudentTest() { excused = false ) - Log.d(STEP_TAG, "Login with user: ${student.name}, login id: ${student.loginId} , password: ${student.password}") + Log.d(STEP_TAG, "Login with user: ${student.name}, login id: ${student.loginId}.") tokenLogin(student) dashboardPage.waitForRender() @@ -200,7 +200,7 @@ class AssignmentsE2ETest: StudentTest() { allowedExtensions = listOf("txt", "pdf", "jpg") )) - Log.d(STEP_TAG, "Login with user: ${student.name}, login id: ${student.loginId} , password: ${student.password}") + Log.d(STEP_TAG, "Login with user: ${student.name}, login id: ${student.loginId}.") tokenLogin(student) dashboardPage.waitForRender() @@ -342,7 +342,7 @@ class AssignmentsE2ETest: StudentTest() { excused = false ) - Log.d(STEP_TAG, "Login with user: ${student.name}, login id: ${student.loginId} , password: ${student.password}") + Log.d(STEP_TAG, "Login with user: ${student.name}, login id: ${student.loginId}.") tokenLogin(student) dashboardPage.waitForRender() @@ -389,7 +389,7 @@ class AssignmentsE2ETest: StudentTest() { )) )) - Log.d(STEP_TAG, "Login with user: ${student.name}, login id: ${student.loginId} , password: ${student.password}") + Log.d(STEP_TAG, "Login with user: ${student.name}, login id: ${student.loginId}.") tokenLogin(student) dashboardPage.waitForRender() @@ -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/BookmarksE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/BookmarksE2ETest.kt index e05d57161a..f18380ab0c 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/BookmarksE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/BookmarksE2ETest.kt @@ -68,7 +68,7 @@ class BookmarksE2ETest : StudentTest() { dueAt = 1.days.fromNow.iso8601 )) - Log.d(STEP_TAG,"Login with user: ${student.name}, login id: ${student.loginId} , password: ${student.password}") + Log.d(STEP_TAG,"Login with user: ${student.name}, login id: ${student.loginId}.") tokenLogin(student) dashboardPage.waitForRender() diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/CollaborationsE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/CollaborationsE2ETest.kt index 323fee40d5..c1f0c7e521 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/CollaborationsE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/CollaborationsE2ETest.kt @@ -2,6 +2,7 @@ package com.instructure.student.ui.e2e import android.util.Log import com.instructure.canvas.espresso.E2E +import com.instructure.canvas.espresso.KnownBug import com.instructure.panda_annotations.FeatureCategory import com.instructure.panda_annotations.Priority import com.instructure.panda_annotations.TestCategory @@ -13,6 +14,7 @@ import com.instructure.student.ui.utils.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test + /** * Very basic test to verify that the collaborations web page shows up correctly. * We make no attempt to actually start a collaboration. @@ -20,26 +22,22 @@ import org.junit.Test */ @HiltAndroidTest class CollaborationsE2ETest: StudentTest() { - override fun displaysPageObjects() { - TODO("not implemented") //To change body of created functions use File | Settings | File Templates. - } + override fun displaysPageObjects() = Unit - override fun enableAndConfigureAccessibilityChecks() { - //We don't want to see accessibility errors on E2E tests - } + override fun enableAndConfigureAccessibilityChecks() = Unit @E2E @Test + @KnownBug("https://instructure.atlassian.net/browse/VICE-3157") @TestMetaData(Priority.MANDATORY, FeatureCategory.COLLABORATIONS, TestCategory.E2E) fun testCollaborationsE2E() { - Log.d(PREPARATION_TAG,"Seeding data.") val data = seedData(students = 1, teachers = 1, courses = 1) val student = data.studentsList[0] val course = data.coursesList[0] - Log.d(STEP_TAG,"Login with user: ${student.name}, login id: ${student.loginId} , password: ${student.password}") + Log.d(STEP_TAG,"Login with user: ${student.name}, login id: ${student.loginId}.") tokenLogin(student) dashboardPage.waitForRender() @@ -50,11 +48,13 @@ class CollaborationsE2ETest: StudentTest() { Log.d(STEP_TAG,"Verify that various elements of the web page are present.") CollaborationsPage.assertCurrentCollaborationsHeaderPresent() - // For some reason, these aren't showing up when run in FTL, though they do - // show up when run locally (same server environment in each). I'll comment - // them out for now, with MBL-14427 being created to pursue the issue. -// CollaborationsPage.assertStartANewCollaborationPresent() -// CollaborationsPage.assertGoogleDocsChoicePresent() -// CollaborationsPage.assertGoogleDocsExplanationPresent() + //On some screen size, this spinner does not displayed at all, instead of it, + //there is a button on the top-right corner with the 'Start a new Collaboration' text + //and clicking on it will 'expand' and display this spinner. + //However, there is a bug (see link in this @KnownBug annotation) which is about the button not displayed on some screen size + //So this test will breaks until it this ticket will be fixed. + CollaborationsPage.assertStartANewCollaborationPresent() + CollaborationsPage.assertGoogleDocsChoicePresent() + CollaborationsPage.assertGoogleDocsExplanationPresent() } } \ No newline at end of file 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..0e4b170ac8 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,18 +32,18 @@ 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}") + Log.d(STEP_TAG,"Login with user: ${student.name}, login id: ${student.loginId}.") tokenLogin(student) dashboardPage.waitForRender() @@ -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/DashboardE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/DashboardE2ETest.kt index 40acdb7f5f..ea25f2bf4c 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/DashboardE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/DashboardE2ETest.kt @@ -28,7 +28,6 @@ import com.instructure.student.ui.utils.StudentTest import com.instructure.student.ui.utils.seedData import com.instructure.student.ui.utils.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest -import junit.framework.Assert.assertEquals import org.junit.Test @HiltAndroidTest @@ -64,7 +63,7 @@ class DashboardE2ETest : StudentTest() { Log.d(PREPARATION_TAG,"Create group membership for ${student.name} student.") GroupsApi.createGroupMembership(group.id, student.id, teacher.token) - Log.d(STEP_TAG,"Login with user: ${student.name}, login id: ${student.loginId} , password: ${student.password}") + Log.d(STEP_TAG,"Login with user: ${student.name}, login id: ${student.loginId}.") tokenLogin(student) dashboardPage.waitForRender() diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/DiscussionsE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/DiscussionsE2ETest.kt index cd2238f1e5..e8f3650e4a 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/DiscussionsE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/DiscussionsE2ETest.kt @@ -77,7 +77,7 @@ class DiscussionsE2ETest: StudentTest() { token = teacher.token ) - Log.d(STEP_TAG,"Login with user: ${student.name}, login id: ${student.loginId} , password: ${student.password}") + Log.d(STEP_TAG,"Login with user: ${student.name}, login id: ${student.loginId}.") tokenLogin(student) dashboardPage.waitForRender() diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/FilesE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/FilesE2ETest.kt index 6431b77053..d274e63fd2 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/FilesE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/FilesE2ETest.kt @@ -35,11 +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.student.ui.utils.StudentTest -import com.instructure.student.ui.utils.ViewUtils -import com.instructure.student.ui.utils.seedData -import com.instructure.student.ui.utils.tokenLogin -import com.instructure.student.ui.utils.uploadTextFile +import com.instructure.student.ui.utils.* import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test import java.io.File @@ -113,7 +109,7 @@ class FilesE2ETest: StudentTest() { token = student.token ) - Log.d(STEP_TAG,"Login with user: ${student.name}, login id: ${student.loginId} , password: ${student.password}") + Log.d(STEP_TAG,"Login with user: ${student.name}, login id: ${student.loginId}.") tokenLogin(student) dashboardPage.waitForRender() @@ -196,7 +192,7 @@ class FilesE2ETest: StudentTest() { submissionDetailsPage.assertCommentAttachmentDisplayed(commentUploadInfo.fileName, student) Log.d(STEP_TAG,"Navigate back to Dashboard Page.") - ViewUtils.pressBackButton(5) + ViewUtils.pressBackButton(4) Log.d(STEP_TAG,"Navigate to 'Files' menu in user left-side menubar.") dashboardPage.gotoGlobalFiles() @@ -217,7 +213,6 @@ class FilesE2ETest: StudentTest() { Log.d(STEP_TAG,"Delete $newFileName file.") fileListPage.deleteFile(newFileName) - fileListPage.assertPageObjects() Log.d(STEP_TAG,"Assert that empty view is displayed after deletion.") fileListPage.assertViewEmpty() diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/GradesE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/GradesE2ETest.kt index 81856f5373..1511a783d3 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/GradesE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/GradesE2ETest.kt @@ -85,7 +85,7 @@ class GradesE2ETest: StudentTest() { Log.d(STEP_TAG,"Publish the previously made quiz.") val quiz = QuizzesApi.createAndPublishQuiz(course.id, teacher.token, quizQuestions) - Log.d(STEP_TAG,"Login with user: ${student.name}, login id: ${student.loginId} , password: ${student.password}") + Log.d(STEP_TAG,"Login with user: ${student.name}, login id: ${student.loginId}.") tokenLogin(student) dashboardPage.waitForRender() diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/InboxE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/InboxE2ETest.kt index 657015df25..d49a85c131 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/InboxE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/InboxE2ETest.kt @@ -18,7 +18,11 @@ package com.instructure.student.ui.e2e import android.os.SystemClock.sleep import android.util.Log +import androidx.test.espresso.Espresso +import androidx.test.espresso.matcher.ViewMatchers import com.instructure.canvas.espresso.E2E +import com.instructure.canvas.espresso.refresh +import com.instructure.canvasapi2.apis.InboxApi import com.instructure.dataseeding.api.ConversationsApi import com.instructure.dataseeding.api.GroupsApi import com.instructure.panda_annotations.FeatureCategory @@ -53,26 +57,30 @@ class InboxE2ETest: StudentTest() { val student1 = data.studentsList[0] val student2 = data.studentsList[1] - // Create a group and put both students in it val groupCategory = GroupsApi.createCourseGroupCategory(course.id, teacher.token) val group = GroupsApi.createGroup(groupCategory.id, teacher.token) Log.d(PREPARATION_TAG, "Create group membership for ${student1.name} and ${student2.name} students to the group: ${group.name}.") GroupsApi.createGroupMembership(group.id, student1.id, teacher.token) GroupsApi.createGroupMembership(group.id, student2.id, teacher.token) - Log.d(PREPARATION_TAG,"Seed an email from the teacher to ${student1.name} and ${student2.name} students.") - val seededConversation = ConversationsApi.createConversation( - token = teacher.token, - recipients = listOf(student1.id.toString(), student2.id.toString()) - ).get(0) - - Log.d(STEP_TAG,"Login with user: ${student1.name}, login id: ${student1.loginId} , password: ${student1.password}") + Log.d(STEP_TAG,"Login with user: ${student1.name}, login id: ${student1.loginId}.") tokenLogin(student1) dashboardPage.waitForRender() + dashboardPage.assertDisplaysCourse(course) Log.d(STEP_TAG,"Open Inbox Page. Assert that the previously seeded conversation is displayed.") dashboardPage.clickInboxTab() - inboxPage.assertPageObjects() //TODO: Refactor to assert to the empty view just like in teacher would be better. AFTER THAT, seed the conversation. + inboxPage.assertInboxEmpty() + + Log.d(PREPARATION_TAG,"Seed an email from the teacher to ${student1.name} and ${student2.name} students.") + val seededConversation = ConversationsApi.createConversation( + token = teacher.token, + recipients = listOf(student1.id.toString(), student2.id.toString()) + )[0] + + Log.d(STEP_TAG,"Refresh the page. Assert that there is a conversation and it is the previously seeded one.") + refresh() + inboxPage.assertHasConversation() inboxPage.assertConversationDisplayed(seededConversation) Log.d(STEP_TAG,"Click on 'New Message' button.") @@ -116,14 +124,65 @@ class InboxE2ETest: StudentTest() { Log.d(STEP_TAG,"Log out with ${student1.name} student.") dashboardPage.logOut() - Log.d(STEP_TAG,"Login with user: ${student2.name}, login id: ${student2.loginId} , password: ${student2.password}") + Log.d(STEP_TAG,"Login with user: ${student2.name}, login id: ${student2.loginId}.") tokenLogin(student2) dashboardPage.waitForRender() Log.d(STEP_TAG,"Open Inbox Page. Assert that both, the previously seeded 'normal' conversation and the group conversation are displayed.") dashboardPage.clickInboxTab() inboxPage.assertConversationDisplayed(seededConversation) - inboxPage.assertConversationDisplayed("Hey There") + inboxPage.assertConversationDisplayed(newMessageSubject) + inboxPage.assertConversationDisplayed("Group Message") + + Log.d(STEP_TAG,"Select $newGroupMessageSubject conversation.") + inboxPage.selectConversation(newMessageSubject) + val newReplyMessage = "This is a quite new reply message." + Log.d(STEP_TAG,"Reply to $newGroupMessageSubject conversation with '$newReplyMessage' message. Assert that the reply is displayed.") + inboxConversationPage.replyToMessage(newReplyMessage) + + Log.d(STEP_TAG,"Delete $newReplyMessage reply and assert is has been deleted.") + inboxConversationPage.deleteMessage(newReplyMessage) + inboxConversationPage.assertMessageNotDisplayed(newReplyMessage) + + Log.d(STEP_TAG,"Delete the whole '$newGroupMessageSubject' subject and assert that it has been removed from the conversation list on the Inbox Page.") + inboxConversationPage.deleteConversation() //After deletion we will be navigated back to Inbox Page + inboxPage.assertConversationNotDisplayed(newMessageSubject) + inboxPage.assertConversationDisplayed(seededConversation) inboxPage.assertConversationDisplayed("Group Message") + + Log.d(STEP_TAG,"Select ${seededConversation.subject} conversation. Assert that is has not been starred already.") + inboxPage.selectConversation(seededConversation) + inboxConversationPage.assertNotStarred() + + Log.d(STEP_TAG,"Toggle Starred to mark ${seededConversation.subject} conversation as favourite. Assert that it has became starred.") + inboxConversationPage.toggleStarred() + inboxConversationPage.assertStarred() + + Log.d(STEP_TAG,"Navigate back to Inbox Page and assert that the conversation itself is starred as well.") + Espresso.pressBack() // To main inbox page + inboxPage.assertConversationStarred(seededConversation.subject) + + Log.d(STEP_TAG,"Select ${seededConversation.subject} conversation. Mark as Unread by clicking on the 'More Options' menu, 'Mark as Unread' menu point.") + inboxPage.assertUnreadMarkerVisibility(seededConversation.subject, ViewMatchers.Visibility.GONE) + inboxPage.selectConversation(seededConversation) + inboxConversationPage.markUnread() //After select 'Mark as Unread', we will be navigated back to Inbox Page + + Log.d(STEP_TAG,"Assert that ${seededConversation.subject} conversation has been marked as unread.") + inboxPage.assertUnreadMarkerVisibility(seededConversation.subject, ViewMatchers.Visibility.VISIBLE) + + Log.d(STEP_TAG,"Select ${seededConversation.subject} conversation. Archive it by clicking on the 'More Options' menu, 'Archive' menu point.") + inboxPage.selectConversation(seededConversation) + inboxConversationPage.archive() //After select 'Archive', we will be navigated back to Inbox Page + + Log.d(STEP_TAG,"Assert that ${seededConversation.subject} conversation has removed from 'All' tab.") //TODO: Discuss this logic if it's ok if we don't show Archived messages on 'All' tab... + inboxPage.assertConversationNotDisplayed(seededConversation) + + Log.d(STEP_TAG,"Select 'Archived' conversation filter.") + inboxPage.selectInboxScope(InboxApi.Scope.ARCHIVED) + + Log.d(STEP_TAG,"Assert that ${seededConversation.subject} conversation is displayed by the 'Archived' filter, and other conversations are not displayed.") + inboxPage.assertConversationDisplayed(seededConversation) + inboxPage.assertConversationNotDisplayed("Group Message") + } } \ No newline at end of file diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/LoginE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/LoginE2ETest.kt index 62d522e87a..899e9d8dc6 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) + Log.d(STEP_TAG,"Login with user: ${student1.name}, login id: ${student1.loginId}.") + 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) + Log.d(STEP_TAG,"Login with user: ${student2.name}, login id: ${student2.loginId}.") + 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) + Log.d(STEP_TAG,"Login with user: ${student1.name}, login id: ${student1.loginId}.") + 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}.") + 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}.") + 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}.") + 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) + Log.d(STEP_TAG,"Login with user: ${parent.name}, login id: ${parent.loginId}.") + 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}.") + 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/ModulesE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/ModulesE2ETest.kt index 03c4305a3a..e7db107328 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/ModulesE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/ModulesE2ETest.kt @@ -19,11 +19,7 @@ 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.AssignmentsApi -import com.instructure.dataseeding.api.DiscussionTopicsApi -import com.instructure.dataseeding.api.ModulesApi -import com.instructure.dataseeding.api.PagesApi -import com.instructure.dataseeding.api.QuizzesApi +import com.instructure.dataseeding.api.* import com.instructure.dataseeding.model.ModuleItemTypes import com.instructure.dataseeding.model.SubmissionType import com.instructure.dataseeding.util.days @@ -165,7 +161,7 @@ class ModulesE2ETest: StudentTest() { contentId = discussionTopic1.id.toString() ) - Log.d(STEP_TAG, "Login with user: ${teacher.name}, login id: ${teacher.loginId} , password: ${teacher.password}") + Log.d(STEP_TAG, "Login with user: ${teacher.name}, login id: ${teacher.loginId}.") tokenLogin(student) dashboardPage.waitForRender() diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/NotificationsE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/NotificationsE2ETest.kt index ab8d53bfd4..6743ac91bd 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/NotificationsE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/NotificationsE2ETest.kt @@ -98,7 +98,7 @@ class NotificationsE2ETest : StudentTest() { Log.d(PREPARATION_TAG,"Create and publish a quiz with the previously seeded questions.") QuizzesApi.createAndPublishQuiz(course.id, teacher.token, quizQuestions) - Log.d(STEP_TAG,"Login with user: ${student.name}, login id: ${student.loginId} , password: ${student.password}") + Log.d(STEP_TAG,"Login with user: ${student.name}, login id: ${student.loginId}.") tokenLogin(student) dashboardPage.waitForRender() diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/PagesE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/PagesE2ETest.kt index 0ab9529ddc..203a32fd48 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/PagesE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/PagesE2ETest.kt @@ -79,7 +79,7 @@ class PagesE2ETest: StudentTest() { body = "

Front Page Text

" ) - Log.d(STEP_TAG,"Login with user: ${student.name}, login id: ${student.loginId} , password: ${student.password}") + Log.d(STEP_TAG,"Login with user: ${student.name}, login id: ${student.loginId}.") tokenLogin(student) dashboardPage.waitForRender() diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/PeopleE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/PeopleE2ETest.kt index 7d1eb54def..4451f3b26b 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/PeopleE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/PeopleE2ETest.kt @@ -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.panda_annotations.FeatureCategory import com.instructure.panda_annotations.Priority @@ -55,7 +54,7 @@ class PeopleE2ETest : StudentTest() { val student1 = data.studentsList[0] val student2 = data.studentsList[1] - Log.d(STEP_TAG, "Login with user: ${student1.name}, login id: ${student1.loginId} , password: ${student1.password}") + Log.d(STEP_TAG, "Login with user: ${student1.name}, login id: ${student1.loginId}.") tokenLogin(student1) dashboardPage.waitForRender() @@ -96,7 +95,7 @@ class PeopleE2ETest : StudentTest() { Log.d(STEP_TAG,"Sign out with ${student1.name} student.") dashboardPage.logOut() - Log.d(STEP_TAG, "Login with user: ${student2.name}, login id: ${student2.loginId} , password: ${student2.password}") + Log.d(STEP_TAG, "Login with user: ${student2.name}, login id: ${student2.loginId}.") tokenLogin(student2) dashboardPage.waitForRender() diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/QuizzesE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/QuizzesE2ETest.kt index d7b9a5260b..200cf16b51 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/QuizzesE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/QuizzesE2ETest.kt @@ -120,7 +120,7 @@ class QuizzesE2ETest: StudentTest() { val quizPublished = createAndPublishQuiz(course.id, teacher.token, quizQuestions) - Log.d(STEP_TAG, "Login with user: ${student.name}, login id: ${student.loginId} , password: ${student.password}") + Log.d(STEP_TAG, "Login with user: ${student.name}, login id: ${student.loginId}.") tokenLogin(student) dashboardPage.waitForRender() diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/SettingsE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/SettingsE2ETest.kt index f74e1faa41..9d1e86c595 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/SettingsE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/SettingsE2ETest.kt @@ -51,10 +51,9 @@ class SettingsE2ETest : StudentTest() { Log.d(PREPARATION_TAG, "Seeding data.") val data = seedData(students = 1, teachers = 1, courses = 1) - val teacher = data.teachersList[0] + val student = data.studentsList[0] - Log.d(STEP_TAG, "Login with user: ${teacher.name}, login id: ${teacher.loginId} , password: ${teacher.password}") - tokenLogin(teacher) + tokenLogin(student) dashboardPage.waitForRender() Log.d(STEP_TAG, "Navigate to User Settings Page.") @@ -62,7 +61,7 @@ class SettingsE2ETest : StudentTest() { settingsPage.assertPageObjects() Log.d(STEP_TAG, "Open Profile Settings Page.") - settingsPage.launchProfileSettings() + settingsPage.openProfileSettings() profileSettingsPage.assertPageObjects() val newUserName = "John Doe" @@ -78,7 +77,7 @@ class SettingsE2ETest : StudentTest() { Log.d(STEP_TAG, "Navigate to Settings Page again and open Panda Avatar Creator.") dashboardPage.launchSettingsPage() settingsPage.assertPageObjects() - settingsPage.launchProfileSettings() + settingsPage.openProfileSettings() profileSettingsPage.assertPageObjects() profileSettingsPage.launchPandaAvatarCreator() @@ -106,6 +105,55 @@ class SettingsE2ETest : StudentTest() { } + @E2E + @Test + @TestMetaData(Priority.IMPORTANT, FeatureCategory.SETTINGS, TestCategory.E2E) + fun testDarkModeE2E() { + Log.d(PREPARATION_TAG, "Seeding data.") + val data = seedData(students = 1, teachers = 1, courses = 1) + val student = data.studentsList[0] + val course = data.coursesList[0] + + Log.d(STEP_TAG, "Login with user: ${student.name}, login id: ${student.loginId}.") + tokenLogin(student) + dashboardPage.waitForRender() + + Log.d(STEP_TAG, "Navigate to User Settings Page.") + dashboardPage.launchSettingsPage() + settingsPage.assertPageObjects() + + Log.d(STEP_TAG,"Navigate to Settings Page and open App Theme Settings.") + settingsPage.openAppThemeSettings() + + Log.d(STEP_TAG,"Select Dark App Theme and assert that the App Theme Title and Status has the proper text color (which is used in Dark mode).") + settingsPage.selectAppTheme("Dark") + settingsPage.assertAppThemeTitleTextColor("#FFFFFFFF") //Currently, this color is used in the Dark mode for the AppTheme Title text. + settingsPage.assertAppThemeStatusTextColor("#FFC7CDD1") //Currently, this color is used in the Dark mode for the AppTheme Status text. + + Log.d(STEP_TAG,"Navigate back to Dashboard. Assert that the 'Courses' label has the proper text color (which is used in Dark mode).") + Espresso.pressBack() + dashboardPage.assertCourseLabelTextColor("#FFFFFFFF") + + Log.d(STEP_TAG,"Select ${course.name} course and assert on the Course Browser Page that the tabs has the proper text color (which is used in Dark mode).") + dashboardPage.selectCourse(course) + courseBrowserPage.assertTabLabelTextColor("Discussions","#FFFFFFFF") + courseBrowserPage.assertTabLabelTextColor("Grades","#FFFFFFFF") + + Log.d(STEP_TAG,"Navigate to Settings Page and open App Theme Settings again.") + Espresso.pressBack() + dashboardPage.launchSettingsPage() + settingsPage.openAppThemeSettings() + + Log.d(STEP_TAG,"Select Light App Theme and assert that the App Theme Title and Status has the proper text color (which is used in Light mode).") + settingsPage.selectAppTheme("Light") + settingsPage.assertAppThemeTitleTextColor("#FF2D3B45") //Currently, this color is used in the Light mode for the AppTheme Title texts. + settingsPage.assertAppThemeStatusTextColor("#FF556572") //Currently, this color is used in the Light mode for the AppTheme Status text. + + Log.d(STEP_TAG,"Navigate back to Dashboard. Assert that the 'Courses' label has the proper text color (which is used in Light mode).") + Espresso.pressBack() + dashboardPage.assertCourseLabelTextColor("#FF2D3B45") + } + @E2E @Test @TestMetaData(Priority.MANDATORY, FeatureCategory.SETTINGS, TestCategory.E2E) @@ -115,7 +163,7 @@ class SettingsE2ETest : StudentTest() { val data = seedData(students = 1, teachers = 1, courses = 1) val student = data.studentsList[0] - Log.d(STEP_TAG, "Login with user: ${student.name}, login id: ${student.loginId} , password: ${student.password}") + Log.d(STEP_TAG, "Login with user: ${student.name}, login id: ${student.loginId}.") tokenLogin(student) dashboardPage.waitForRender() @@ -124,7 +172,7 @@ class SettingsE2ETest : StudentTest() { settingsPage.assertPageObjects() Log.d(STEP_TAG, "Click on 'Legal' link to open Legal Page. Assert that Legal Page has opened.") - settingsPage.launchLegalPage() + settingsPage.openLegalPage() legalPage.assertPageObjects() } @@ -137,7 +185,7 @@ class SettingsE2ETest : StudentTest() { val data = seedData(students = 1, teachers = 1, courses = 1) val student = data.studentsList[0] - Log.d(STEP_TAG, "Login with user: ${student.name}, login id: ${student.loginId} , password: ${student.password}") + Log.d(STEP_TAG, "Login with user: ${student.name}, login id: ${student.loginId}.") tokenLogin(student) dashboardPage.waitForRender() @@ -146,7 +194,7 @@ class SettingsE2ETest : StudentTest() { settingsPage.assertPageObjects() Log.d(STEP_TAG, "Click on 'About' link to open About Page. Assert that About Page has opened.") - settingsPage.launchAboutPage() + settingsPage.openAboutPage() aboutPage.assertPageObjects() Log.d(STEP_TAG,"Check that domain is equal to: ${student.domain} (student's domain).") @@ -170,7 +218,7 @@ class SettingsE2ETest : StudentTest() { val data = seedData(students = 1, teachers = 1, courses = 1) val student = data.studentsList[0] - Log.d(STEP_TAG, "Login with user: ${student.name}, login id: ${student.loginId} , password: ${student.password}") + Log.d(STEP_TAG, "Login with user: ${student.name}, login id: ${student.loginId}.") tokenLogin(student) dashboardPage.waitForRender() @@ -182,7 +230,7 @@ class SettingsE2ETest : StudentTest() { RemoteConfigParam.values().forEach {param -> initialValues.put(param.rc_name, RemoteConfigUtils.getString(param))} Log.d(STEP_TAG, "Navigate to Remote Config Settings Page.") - settingsPage.launchRemoteConfigParams() + settingsPage.openRemoteConfigParams() RemoteConfigParam.values().forEach { param -> @@ -202,7 +250,7 @@ class SettingsE2ETest : StudentTest() { Espresso.pressBack() Log.d(STEP_TAG, "Navigate to Remote Config Settings Page.") - settingsPage.launchRemoteConfigParams() + settingsPage.openRemoteConfigParams() Log.d(STEP_TAG, "Assert that all fields have maintained their initial value.") RemoteConfigParam.values().forEach { param -> diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/ShareExtensionE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/ShareExtensionE2ETest.kt new file mode 100644 index 0000000000..80d8ae2cf3 --- /dev/null +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/ShareExtensionE2ETest.kt @@ -0,0 +1,231 @@ +/* + * Copyright (C) 2022 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.instructure.student.ui.e2e + +import android.content.Intent +import android.net.Uri +import android.util.Log +import androidx.core.content.FileProvider +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.uiautomator.UiDevice +import androidx.test.uiautomator.UiSelector +import com.instructure.canvas.espresso.E2E +import com.instructure.dataseeding.api.AssignmentsApi +import com.instructure.dataseeding.model.GradingType +import com.instructure.dataseeding.model.SubmissionType +import com.instructure.dataseeding.util.days +import com.instructure.dataseeding.util.fromNow +import com.instructure.dataseeding.util.iso8601 +import com.instructure.pandautils.utils.Const +import com.instructure.student.ui.utils.StudentTest +import com.instructure.student.ui.utils.ViewUtils +import com.instructure.student.ui.utils.seedData +import com.instructure.student.ui.utils.tokenLogin +import dagger.hilt.android.testing.HiltAndroidTest +import org.junit.Test +import java.io.File + +@HiltAndroidTest +class ShareExtensionE2ETest: StudentTest() { + + override fun displaysPageObjects() = Unit + override fun enableAndConfigureAccessibilityChecks() = Unit + + @E2E + @Test + fun shareExtensionE2ETest() { + + Log.d(PREPARATION_TAG, "Seeding data.") + val data = seedData(students = 1, teachers = 1, courses = 1) + val jpgTestFileName = "sample.jpg" + val pdfTestFileName = "samplepdf.pdf" + val uri = setupFileOnDevice(jpgTestFileName) + val uri2 = setupFileOnDevice(pdfTestFileName) + val student = data.studentsList[0] + val course = data.coursesList[0] + val teacher = data.teachersList[0] + + Log.d(PREPARATION_TAG,"Seeding 'Text Entry' assignment for ${course.name} course.") + val testAssignmentOne = AssignmentsApi.createAssignment( + AssignmentsApi.CreateAssignmentRequest( + courseId = course.id, + submissionTypes = listOf(SubmissionType.ONLINE_UPLOAD), + gradingType = GradingType.POINTS, + teacherToken = teacher.token, + pointsPossible = 15.0, + dueAt = 1.days.fromNow.iso8601 + )) + + AssignmentsApi.createAssignment( + AssignmentsApi.CreateAssignmentRequest( + courseId = course.id, + submissionTypes = listOf(SubmissionType.ONLINE_UPLOAD), + gradingType = GradingType.POINTS, + teacherToken = teacher.token, + pointsPossible = 30.0, + dueAt = 1.days.fromNow.iso8601 + )) + + Log.d(PREPARATION_TAG, "Get the device to be able to perform app-independent actions on it.") + val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) + + Log.d(STEP_TAG, "Login with user: ${student.name}, login id: ${student.loginId}.") + tokenLogin(student) + + Log.d(STEP_TAG, "Press 'Home' button on the device so it will take the Student application into the background.") + device.pressHome() + + Log.d(STEP_TAG," Share the '$jpgTestFileName' and '$pdfTestFileName' files from the following uris: '$uri' and '$uri2'.") + shareMultipleFiles(arrayListOf(uri, uri2)) + + Log.d(STEP_TAG,"Click on the Canvas Student app.") + device.findObject(UiSelector().text("Canvas")).click() + device.waitForIdle() + + Log.d(STEP_TAG,"Assert that the Share Extension Page is displayed and the " + + "'My Files' button is selected by default, and so the '${student.name}' username is displayed as well.") + shareExtensionTargetPage.assertPageObjects() + shareExtensionTargetPage.assertFilesCheckboxIsSelected() + shareExtensionTargetPage.assertUserName(student.name) + + Log.d(STEP_TAG, "Select 'Upload as Submission' and assert that the corresponding course and assignment are displayed within the spinners.") + shareExtensionTargetPage.selectSubmission() + shareExtensionTargetPage.assertCourseSelectorDisplayedWithCourse(course.name) + shareExtensionTargetPage.assertAssignmentSelectorDisplayedWithAssignment(testAssignmentOne.name) + + Log.d(STEP_TAG, "Click on 'Next' button.") + shareExtensionTargetPage.pressNext() + + Log.d(STEP_TAG, "Assert that the File Upload page is displayed with the corresponding title.") + fileUploadPage.assertPageObjects() + fileUploadPage.assertDialogTitle("Submission") + + Log.d(STEP_TAG, "Click on 'Turn In' button to upload both of the files.") + fileUploadPage.clickTurnIn() + + Log.d(STEP_TAG, "Assert that the submission upload was successful.") + shareExtensionStatusPage.assertPageObjects() + shareExtensionStatusPage.assertAssignmentSubmissionSuccess() + + Log.d(STEP_TAG, "Click on 'Done' button.") + shareExtensionStatusPage.clickOnDone() + + Log.d(STEP_TAG, "Click 'Recent Apps' device button and bring Canvas Student into the foreground again." + + "Assert that the Dashboard Page is displayed.") + device.pressRecentApps() + device.findObject(UiSelector().descriptionContains("Canvas")).click() + + Log.d(STEP_TAG, "Assert that the Dashboard Page is displayed. Select ${course.name} and navigate to Assignments Page.") + dashboardPage.assertPageObjects() + dashboardPage.selectCourse(course) + courseBrowserPage.selectAssignments() + + Log.d(STEP_TAG, "Click on $testAssignmentOne assignment and refresh the Assignment Details Page." + + "Assert that the $testAssignmentOne assignment's status is 'Submitted'.") + assignmentListPage.clickAssignment(testAssignmentOne) + assignmentDetailsPage.refresh() + assignmentDetailsPage.assertAssignmentSubmitted() + + Log.d(STEP_TAG, "Press 'Home' button on the device so it will take the Student application into the background.") + device.pressHome() + + Log.d(STEP_TAG," Share the '$jpgTestFileName' and '$pdfTestFileName' files from the following uris: '$uri' and '$uri2'.") + shareMultipleFiles(arrayListOf(uri, uri2)) + + Log.d(STEP_TAG,"Click on the Canvas Student app.") + device.findObject(UiSelector().text("Canvas")).click() + device.waitForIdle() + + Log.d(STEP_TAG,"Assert that the Share Extension Page is displayed and the " + + "'My Files' button is selected by default, and so the '${student.name}' username is displayed as well.") + shareExtensionTargetPage.assertPageObjects() + shareExtensionTargetPage.assertFilesCheckboxIsSelected() + shareExtensionTargetPage.assertUserName(student.name) + + Log.d(STEP_TAG, "Press 'Next' button.") + shareExtensionTargetPage.pressNext() + + Log.d(STEP_TAG,"Assert that the title of the File Upload Page is correct and both of the shared files are displayed.") + fileUploadPage.assertPageObjects() + fileUploadPage.assertDialogTitle("Upload To My Files") + fileUploadPage.assertFileDisplayed(jpgTestFileName) + fileUploadPage.assertFileDisplayed(pdfTestFileName) + + Log.d(STEP_TAG,"Remove '$pdfTestFileName' file and assert that it's not displayed any more on the list but the other file is displayed.") + fileUploadPage.removeFile(pdfTestFileName) + fileUploadPage.assertFileNotDisplayed(pdfTestFileName) + fileUploadPage.assertFileDisplayed("$pdfTestFileName.jpg") + + Log.d(STEP_TAG, "Click on 'Upload' button to upload the file.") + fileUploadPage.clickUpload() + + Log.d(STEP_TAG, "Assert that the file upload (into my 'Files') was successful.") + shareExtensionStatusPage.assertPageObjects() + shareExtensionStatusPage.assertFileUploadSuccess() + + Log.d(STEP_TAG, "Click on 'Done' button.") + shareExtensionStatusPage.clickOnDone() + + Log.d(STEP_TAG, "Click 'Recent Apps' device button and bring Canvas Student into the foreground again." + + "Assert that the Dashboard Page is displayed.") + device.pressRecentApps() + device.findObject(UiSelector().descriptionContains("Canvas")).click() + + Log.d(STEP_TAG, "Press back button to navigate back to the Dashboard Page.") + assignmentDetailsPage.assertPageObjects() + ViewUtils.pressBackButton(3) + + Log.d(STEP_TAG, "Navigate to (Global) Files Page.") + dashboardPage.assertPageObjects() + Thread.sleep(4000) //Make sure that the toast message has disappeared. + dashboardPage.gotoGlobalFiles() + + Log.d(STEP_TAG, "Assert that the 'unfiled' directory is displayed." + + "Click on it, and assert that the previously uploaded file ($jpgTestFileName) is displayed within the folder.") + fileListPage.assertItemDisplayed("unfiled") + fileListPage.selectItem("unfiled") + fileListPage.assertItemDisplayed(jpgTestFileName) + + } + + 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 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) + } +} \ No newline at end of file diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/SyllabusE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/SyllabusE2ETest.kt index 5be307ab37..1df4dbb0b1 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/SyllabusE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/SyllabusE2ETest.kt @@ -55,7 +55,7 @@ class SyllabusE2ETest: StudentTest() { 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}") + Log.d(STEP_TAG, "Login with user: ${student.name}, login id: ${student.loginId}.") tokenLogin(student) dashboardPage.waitForRender() diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/TodoE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/TodoE2ETest.kt index 34cef2228f..7e0eb1714a 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/TodoE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/TodoE2ETest.kt @@ -16,7 +16,6 @@ import com.instructure.student.ui.utils.seedData import com.instructure.student.ui.utils.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test -import java.util.Calendar @HiltAndroidTest class TodoE2ETest: StudentTest() { @@ -33,28 +32,28 @@ class TodoE2ETest: StudentTest() { @TestMetaData(Priority.MANDATORY, FeatureCategory.TODOS, TestCategory.E2E) fun testTodoE2E() { - // Don't attempt this test on a Friday, Saturday or Sunday. - // The TODO tab doesn't seem to behave correctly on Fridays (or presumably weekends). - val dayOfWeek = Calendar.getInstance().get(Calendar.DAY_OF_WEEK) - if(dayOfWeek == Calendar.FRIDAY || dayOfWeek == Calendar.SATURDAY || dayOfWeek == Calendar.SUNDAY) { - println("We don't run the TODO E2E test on weekends") - return - } - Log.d(PREPARATION_TAG, "Seeding data.") val data = seedData(students = 1, teachers = 1, courses = 1) val student = data.studentsList[0] val teacher = data.teachersList[0] val course = data.coursesList[0] - Log.d(PREPARATION_TAG,"Seed an assignment for ${course.name} course.") + Log.d(PREPARATION_TAG,"Seed an assignment for ${course.name} course with tomorrow due date.") val seededAssignments = seedAssignments( courseId = course.id, teacherToken = teacher.token, dueAt = 1.days.fromNow.iso8601 ) + Log.d(PREPARATION_TAG,"Seed another assignment for ${course.name} course with 7 days from now due date.") + val seededAssignments2 = seedAssignments( + courseId = course.id, + teacherToken = teacher.token, + dueAt = 7.days.fromNow.iso8601 + ) + val testAssignment = seededAssignments[0] + val borderDateAssignment = seededAssignments2[0] //We show items in the to do section which are within 7 days. Log.d(PREPARATION_TAG,"Seed a quiz for ${course.name} course with tomorrow due date.") val quiz = QuizzesApi.createQuiz( @@ -66,17 +65,29 @@ class TodoE2ETest: StudentTest() { dueAt = 1.days.fromNow.iso8601) ) - Log.d(STEP_TAG, "Login with user: ${student.name}, login id: ${student.loginId} , password: ${student.password}") + Log.d(PREPARATION_TAG,"Seed another quiz for ${course.name} course with 8 days from now due date..") + val tooFarAwayQuiz = QuizzesApi.createQuiz( + QuizzesApi.CreateQuizRequest( + courseId = course.id, + withDescription = true, + published = true, + token = teacher.token, + dueAt = 8.days.fromNow.iso8601) + ) + + Log.d(STEP_TAG, "Login with user: ${student.name}, login id: ${student.loginId}.") tokenLogin(student) dashboardPage.waitForRender() Log.d(STEP_TAG,"Navigate to 'To Do' page via bottom-menu.") dashboardPage.clickTodoTab() - Log.d(STEP_TAG,"Assert that ${testAssignment.name} assignment is displayed.") + Log.d(STEP_TAG,"Assert that ${testAssignment.name} assignment is displayed and ${borderDateAssignment.name} is displayed because it's 7 days away from now..") todoPage.assertAssignmentDisplayed(testAssignment) + todoPage.assertAssignmentDisplayed(borderDateAssignment) - Log.d(STEP_TAG,"Assert that ${quiz.title} quiz is displayed.") + Log.d(STEP_TAG,"Assert that ${quiz.title} quiz is displayed and ${tooFarAwayQuiz.title} quiz is not displayed because it's end date is more than a week away..") todoPage.assertQuizDisplayed(quiz) + todoPage.assertQuizNotDisplayed(tooFarAwayQuiz) } } \ No newline at end of file diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/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/AnnouncementInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/AnnouncementInteractionTest.kt index 56ffbf4801..a857544b3b 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/AnnouncementInteractionTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/AnnouncementInteractionTest.kt @@ -17,14 +17,8 @@ package com.instructure.student.ui.interaction import androidx.test.espresso.Espresso import androidx.test.espresso.web.webdriver.Locator -import com.instructure.canvas.espresso.mockCanvas.MockCanvas -import com.instructure.canvas.espresso.mockCanvas.addCoursePermissions -import com.instructure.canvas.espresso.mockCanvas.addDiscussionTopicToCourse -import com.instructure.canvas.espresso.mockCanvas.init -import com.instructure.canvasapi2.models.CanvasContextPermission -import com.instructure.canvasapi2.models.Course -import com.instructure.canvasapi2.models.Tab -import com.instructure.canvasapi2.models.User +import com.instructure.canvas.espresso.mockCanvas.* +import com.instructure.canvasapi2.models.* import com.instructure.panda_annotations.FeatureCategory import com.instructure.panda_annotations.Priority import com.instructure.panda_annotations.TestCategory @@ -42,6 +36,10 @@ class AnnouncementInteractionTest : StudentTest() { private lateinit var course: Course private lateinit var user: User + private lateinit var group : Group + private lateinit var discussion : DiscussionTopicHeader + private lateinit var announcement : DiscussionTopicHeader + // Student enrolled in intended section can see and reply to the announcement // (This kind of seems like more of a test of the mocked endpoint, but we'll go with it.) @Test @@ -156,10 +154,12 @@ class AnnouncementInteractionTest : StudentTest() { val course = data.courses.values.first() val announcement = data.courseDiscussionTopicHeaders[course.id]!!.first() discussionListPage.assertTopicDisplayed(announcement.title!!) - discussionListPage.createAnnouncement("Announcement Topic", "Awesome announcement topic") + val newAnnouncementName = "Announcement Topic" + discussionListPage.createAnnouncement(newAnnouncementName, "Awesome announcement topic") + discussionListPage.assertAnnouncementCreated(newAnnouncementName) } - // Tests code around closing / aborting announcement creation + // Tests code around closing / aborting announcement creation (as a teacher) @Test @TestMetaData(Priority.IMPORTANT, FeatureCategory.ANNOUNCEMENTS, TestCategory.INTERACTION, false) fun testAnnouncementCreate_abort() { @@ -177,23 +177,34 @@ class AnnouncementInteractionTest : StudentTest() { discussionListPage.assertAnnouncementCount(2) // header + the one test announcement } - // Tests code around creating an announcement with no description + // Tests code around creating an announcement with no description (as a teacher) @Test @TestMetaData(Priority.COMMON, FeatureCategory.ANNOUNCEMENTS, TestCategory.INTERACTION, false) fun testAnnouncementCreate_missingDescription() { getToAnnouncementList() - discussionListPage.createAnnouncement("title", "", verify = false) - // easier than looking for the "A description is required" toast message - discussionListPage.assertOnNewAnnouncementPage() + discussionListPage.createAnnouncement("title", "") + discussionListPage.assertOnNewAnnouncementPage() // easier than looking for the "A description is required" toast message } - // Tests code around creating an announcement with no title + // Tests code around creating an announcement with no title (as a teacher) @Test @TestMetaData(Priority.COMMON, FeatureCategory.ANNOUNCEMENTS, TestCategory.INTERACTION, false) fun testAnnouncementCreate_missingTitle() { getToAnnouncementList() discussionListPage.createAnnouncement("", "description") + discussionListPage.assertAnnouncementCreated("") + } + + @Test + @TestMetaData(Priority.IMPORTANT, FeatureCategory.ANNOUNCEMENTS, TestCategory.INTERACTION, false) + fun testGroupAnnouncementCreateAsStudent() { + getToGroup() + + courseBrowserPage.selectAnnouncements() + val newAnnouncementName = "Student created Group Announcement" + discussionListPage.createAnnouncement(newAnnouncementName, "Cool group announcement") + discussionListPage.assertAnnouncementCreated(newAnnouncementName) } @Test @@ -206,6 +217,7 @@ class AnnouncementInteractionTest : StudentTest() { val existingAnnouncementName = announcement.title discussionListPage.createAnnouncement(testAnnouncementName, "description") + discussionListPage.assertAnnouncementCreated(testAnnouncementName) discussionListPage.clickOnSearchButton() discussionListPage.typeToSearchBar(testAnnouncementName) @@ -225,24 +237,82 @@ class AnnouncementInteractionTest : StudentTest() { courseCount: Int = 1, createSections: Boolean = false ): MockCanvas { + + val data = initData(studentCount,courseCount,createSections) + + val token = data.tokenFor(user)!! + tokenLogin(data.domain, token, user) + dashboardPage.waitForRender() + + dashboardPage.selectCourse(course) + + return data + } + + private fun getToGroup( + studentCount: Int = 1, + courseCount: Int = 1, + createSections: Boolean = false + ): MockCanvas { + + val data = initData(studentCount,courseCount,createSections) + + val token = data.tokenFor(user)!! + tokenLogin(data.domain, token, user) + dashboardPage.waitForRender() + + dashboardPage.selectGroup(group) + + return data + } + + private fun initData( studentCount: Int = 1, + courseCount: Int = 1, + createSections: Boolean = false): MockCanvas { val data = MockCanvas.init( - studentCount = studentCount, - courseCount = courseCount, - favoriteCourseCount = courseCount, - createSections = createSections) + studentCount = studentCount, + courseCount = courseCount, + favoriteCourseCount = courseCount, + createSections = createSections) course = data.courses.values.first() user = data.students[0] + // Add a group + val user = data.users.values.first() + group = data.addGroupToCourse( + course = course, + members = listOf(user), + isFavorite = true + ) + + // Add a discussion + discussion = data.addDiscussionTopicToCourse( + course = course, + user = user, + groupId = group.id + ) + + // Add an announcement + announcement = data.addDiscussionTopicToCourse( + course = course, + user = user, + groupId = group.id, + isAnnouncement = true + ) + val announcementsTab = Tab(position = 2, label = "Announcements", visibility = "public", tabId = Tab.ANNOUNCEMENTS_ID) data.courseTabs[course.id]!! += announcementsTab - val token = data.tokenFor(user)!! - tokenLogin(data.domain, token, user) - dashboardPage.waitForRender() - - dashboardPage.selectCourse(course) + data.groupTabs[group.id] = mutableListOf( + Tab(position = 0, label = "Discussions", tabId = Tab.DISCUSSIONS_ID, visibility = "public"), + Tab(position = 1, label = "Announcements", tabId = Tab.ANNOUNCEMENTS_ID, visibility = "public"), + ) + MockCanvas.data.addCoursePermissions( + course.id, + CanvasContextPermission(canCreateAnnouncement = true) + ) return data } diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/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/InboxInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/InboxInteractionTest.kt index b4c7a157f4..f3f75e8622 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/InboxInteractionTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/InboxInteractionTest.kt @@ -394,7 +394,7 @@ class InboxInteractionTest : StudentTest() { inboxConversationPage.toggleStarred() inboxConversationPage.assertStarred() Espresso.pressBack() // To main inbox page - inboxPage.assertConversationStarred(conversation) + inboxPage.assertConversationStarred(conversation.subject!!) } @Test diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/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/ProfileSettingsInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ProfileSettingsInteractionTest.kt index 06953de666..7a861663c9 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ProfileSettingsInteractionTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ProfileSettingsInteractionTest.kt @@ -43,7 +43,7 @@ class ProfileSettingsInteractionTest : StudentTest() { tokenLogin(data.domain, token, student) dashboardPage.launchSettingsPage() - settingsPage.launchProfileSettings() + settingsPage.openProfileSettings() profileSettingsPage.changeUserNameTo(newUserName) Espresso.pressBack() // to settings page @@ -62,7 +62,7 @@ class ProfileSettingsInteractionTest : StudentTest() { tokenLogin(data.domain, token, student) dashboardPage.launchSettingsPage() - settingsPage.launchProfileSettings() + settingsPage.openProfileSettings() profileSettingsPage.assertSettingsDisabled() // No permissions granted } @@ -86,7 +86,7 @@ class ProfileSettingsInteractionTest : StudentTest() { // Navigate to avatar creation page dashboardPage.launchSettingsPage() - settingsPage.launchProfileSettings() + settingsPage.openProfileSettings() profileSettingsPage.launchPandaAvatarCreator() // Select head diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/SettingsInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/SettingsInteractionTest.kt index da3ac3fb7b..1a2c2544c7 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/SettingsInteractionTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/SettingsInteractionTest.kt @@ -58,7 +58,7 @@ class SettingsInteractionTest : StudentTest() { setUpAndSignIn() dashboardPage.launchSettingsPage() - settingsPage.launchLegalPage() + settingsPage.openLegalPage() Intents.init() try { @@ -79,7 +79,7 @@ class SettingsInteractionTest : StudentTest() { setUpAndSignIn() dashboardPage.launchSettingsPage() - settingsPage.launchLegalPage() + settingsPage.openLegalPage() legalPage.openTermsOfUse() legalPage.assertTermsOfUseDisplayed() } @@ -91,7 +91,7 @@ class SettingsInteractionTest : StudentTest() { setUpAndSignIn() dashboardPage.launchSettingsPage() - settingsPage.launchLegalPage() + settingsPage.openLegalPage() legalPage.openPrivacyPolicy() canvasWebViewPage.acceptCookiePolicyIfNecessary() canvasWebViewPage.checkWebViewURL("https://www.instructure.com/canvas/privacy") @@ -107,7 +107,7 @@ class SettingsInteractionTest : StudentTest() { ApiPrefs.canGeneratePairingCode = true dashboardPage.launchSettingsPage() - settingsPage.launchPairObserverPage() + settingsPage.openPairObserverPage() pairObserverPage.hasCode("1") pairObserverPage.refresh() diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ShareExtensionInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ShareExtensionInteractionTest.kt new file mode 100644 index 0000000000..2e9d14f952 --- /dev/null +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ShareExtensionInteractionTest.kt @@ -0,0 +1,333 @@ +/* + * 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") + } + + @Test + fun shareExtensionNoAssignmentTest() { + 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.selectSubmission() + shareExtensionTargetPage.assertNoAssignmentSelectedStringDisplayed() + shareExtensionTargetPage.pressNext() + shareExtensionTargetPage.assertPageObjects() //Make sure that we are still on the Target Page. + } + + + // 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.assertAssignmentSubmissionSuccess() + } + + 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/TodoInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/TodoInteractionTest.kt index ffe511b42d..d30a428387 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/TodoInteractionTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/TodoInteractionTest.kt @@ -16,6 +16,7 @@ package com.instructure.student.ui.interaction import androidx.test.espresso.Espresso +import com.instructure.canvas.espresso.StubLandscape import com.instructure.canvas.espresso.mockCanvas.MockCanvas import com.instructure.canvas.espresso.mockCanvas.addAssignment import com.instructure.canvas.espresso.mockCanvas.addQuizToCourse @@ -63,8 +64,10 @@ class TodoInteractionTest : StudentTest() { } @Test + @StubLandscape("Stubbed because on lowres device in landscape mode, the space is too narrow to scroll properly. Will be refactored and running when we changed to non-lowres device on nightly runs.") @TestMetaData(Priority.IMPORTANT, FeatureCategory.TODOS, TestCategory.INTERACTION, false) fun testFilters() { + //TODO: Check and refactor (if necessary) after migrated nightly runs from lowres device to non-lowres one. val data = goToTodos(courseCount = 2, favoriteCourseCount = 1) val favoriteCourse = data.courses.values.first {course -> course.isFavorite} val notFavoriteCourse = data.courses.values.first {course -> !course.isFavorite} diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/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/CollaborationsPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/CollaborationsPage.kt index 6f7528af03..ae3252b91b 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/CollaborationsPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/CollaborationsPage.kt @@ -16,17 +16,14 @@ */ package com.instructure.student.ui.pages -import androidx.test.espresso.Espresso.onView -import androidx.test.espresso.action.ViewActions.swipeUp -import androidx.test.espresso.matcher.ViewMatchers -import androidx.test.espresso.matcher.ViewMatchers.* +import androidx.test.espresso.matcher.ViewMatchers.isDisplayed +import androidx.test.espresso.matcher.ViewMatchers.withId import androidx.test.espresso.web.assertion.WebViewAssertions.webMatches import androidx.test.espresso.web.sugar.Web import androidx.test.espresso.web.webdriver.DriverAtoms import androidx.test.espresso.web.webdriver.DriverAtoms.getText import androidx.test.espresso.web.webdriver.Locator import com.instructure.canvas.espresso.checkRepeat -import com.instructure.canvas.espresso.withElementRepeat import com.instructure.student.R import org.hamcrest.Matchers import org.hamcrest.Matchers.containsString @@ -44,12 +41,8 @@ object CollaborationsPage { } fun assertStartANewCollaborationPresent() { - - // Debug maneuver to help see what was being displayed - //onView(withId(R.id.canvasWebView)).perform(swipeUp()) - Web.onWebView(Matchers.allOf(withId(R.id.canvasWebView), isDisplayed())) - .withElement(DriverAtoms.findElement(Locator.TAG_NAME, "h2")) // lucky there is only one of these! + .withElement(DriverAtoms.findElement(Locator.TAG_NAME, "h2")) .perform(DriverAtoms.webScrollIntoView()) .checkRepeat(webMatches(getText(), containsString("Start a New Collaboration") ), 30) } 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/CourseBrowserPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/CourseBrowserPage.kt index 669ce60d3c..723ce6ccce 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/CourseBrowserPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/CourseBrowserPage.kt @@ -23,22 +23,15 @@ import androidx.test.espresso.UiController import androidx.test.espresso.ViewAction import androidx.test.espresso.action.ViewActions import androidx.test.espresso.assertion.ViewAssertions.doesNotExist -import androidx.test.espresso.matcher.ViewMatchers.isAssignableFrom -import androidx.test.espresso.matcher.ViewMatchers.isDisplayed -import androidx.test.espresso.matcher.ViewMatchers.isDisplayingAtLeast -import androidx.test.espresso.matcher.ViewMatchers.withId -import androidx.test.espresso.matcher.ViewMatchers.withText +import androidx.test.espresso.matcher.ViewMatchers.* import com.instructure.canvas.espresso.scrollRecyclerView import com.instructure.canvas.espresso.withCustomConstraints import com.instructure.canvasapi2.models.Course import com.instructure.canvasapi2.models.Group import com.instructure.canvasapi2.models.Tab import com.instructure.dataseeding.model.CourseApiModel -import com.instructure.espresso.WaitForViewWithId -import com.instructure.espresso.assertHasText -import com.instructure.espresso.click +import com.instructure.espresso.* import com.instructure.espresso.page.BasePage -import com.instructure.espresso.swipeUp import com.instructure.pandautils.views.SwipeRefreshLayoutAppBar import com.instructure.student.R import org.hamcrest.Matcher @@ -136,6 +129,10 @@ class CourseBrowserPage : BasePage(R.id.courseBrowserPage) { assertTabDisplayed(tab.label!!) } + fun assertTabLabelTextColor(tabTitle: String, expectedColor: String) { + onView(withText(tabTitle)).check(TextViewColorAssertion(expectedColor)) + } + fun assertTabDisplayed(tabTitle: String) { recyclerViewScrollTo(allOf(withText(tabTitle),withId(R.id.label))) } diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/DashboardPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/DashboardPage.kt index 551826ff7b..8c343ee9b7 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/DashboardPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/DashboardPage.kt @@ -28,11 +28,7 @@ import androidx.test.espresso.action.ViewActions.click import androidx.test.espresso.assertion.ViewAssertions.doesNotExist import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.matcher.ViewMatchers -import androidx.test.espresso.matcher.ViewMatchers.isAssignableFrom -import androidx.test.espresso.matcher.ViewMatchers.isDisplayed -import androidx.test.espresso.matcher.ViewMatchers.isDisplayingAtLeast -import androidx.test.espresso.matcher.ViewMatchers.withContentDescription -import androidx.test.espresso.matcher.ViewMatchers.withText +import androidx.test.espresso.matcher.ViewMatchers.* import com.instructure.canvas.espresso.scrollRecyclerView import com.instructure.canvas.espresso.waitForMatcherWithSleeps import com.instructure.canvas.espresso.withCustomConstraints @@ -43,24 +39,8 @@ import com.instructure.canvasapi2.models.User import com.instructure.dataseeding.model.CanvasUserApiModel import com.instructure.dataseeding.model.CourseApiModel import com.instructure.dataseeding.model.GroupApiModel -import com.instructure.espresso.OnViewWithContentDescription -import com.instructure.espresso.OnViewWithId -import com.instructure.espresso.WaitForViewWithId -import com.instructure.espresso.assertDisplayed -import com.instructure.espresso.assertNotDisplayed -import com.instructure.espresso.click -import com.instructure.espresso.page.BasePage -import com.instructure.espresso.page.onView -import com.instructure.espresso.page.onViewWithId -import com.instructure.espresso.page.onViewWithText -import com.instructure.espresso.page.plus -import com.instructure.espresso.page.withAncestor -import com.instructure.espresso.page.withId -import com.instructure.espresso.page.withParent -import com.instructure.espresso.page.withText -import com.instructure.espresso.scrollTo -import com.instructure.espresso.swipeDown -import com.instructure.espresso.waitForCheck +import com.instructure.espresso.* +import com.instructure.espresso.page.* import com.instructure.student.R import org.hamcrest.CoreMatchers.allOf import org.hamcrest.CoreMatchers.containsString @@ -140,6 +120,10 @@ class DashboardPage : BasePage(R.id.dashboardPage) { } + fun assertCourseLabelTextColor(expectedTextColor: String) { + onView(withId(R.id.courseLabel)).check(TextViewColorAssertion(expectedTextColor)) + } + fun pressChangeUser() { onView(hamburgerButtonMatcher).click() onViewWithId(R.id.navigationDrawerItem_changeUser).scrollTo().click() @@ -179,7 +163,7 @@ class DashboardPage : BasePage(R.id.dashboardPage) { } fun assertUnreadEmails(count: Int) { - onView(allOf(withParent(R.id.bottomNavigationInbox), withId(R.id.badge), withText(count.toString()))).assertDisplayed() + onView(withId(R.id.bottomBar)).check(NotificationBadgeAssertion(R.id.bottomNavigationInbox, count)) } fun clickCalendarTab() { @@ -238,7 +222,7 @@ class DashboardPage : BasePage(R.id.dashboardPage) { fun selectCourse(course: Course) { assertDisplaysCourse(course) - onView(withText(course.originalName)).perform(withCustomConstraints(click(), isDisplayingAtLeast(10))) + onView(withId(R.id.titleTextView) + withText(course.originalName)).perform(withCustomConstraints(click(), isDisplayingAtLeast(10))) } fun selectGroup(group: Group) { diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/DiscussionDetailsPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/DiscussionDetailsPage.kt index 09bfe982d5..9717e6cd62 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/DiscussionDetailsPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/DiscussionDetailsPage.kt @@ -20,33 +20,21 @@ import android.os.SystemClock.sleep import androidx.test.espresso.Espresso import androidx.test.espresso.Espresso.onView import androidx.test.espresso.action.ViewActions.swipeDown -import androidx.test.espresso.action.ViewActions.swipeUp import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.matcher.ViewMatchers.isDisplayingAtLeast import androidx.test.espresso.matcher.ViewMatchers.withId -import androidx.test.espresso.web.assertion.WebViewAssertions import androidx.test.espresso.web.assertion.WebViewAssertions.webMatches import androidx.test.espresso.web.sugar.Web.onWebView import androidx.test.espresso.web.webdriver.DriverAtoms.findElement import androidx.test.espresso.web.webdriver.DriverAtoms.getText import androidx.test.espresso.web.webdriver.DriverAtoms.webClick import androidx.test.espresso.web.webdriver.Locator -import com.instructure.canvas.espresso.containsTextCaseInsensitive -import com.instructure.canvas.espresso.isElementDisplayed -import com.instructure.canvas.espresso.waitForMatcherWithSleeps -import com.instructure.canvas.espresso.withCustomConstraints -import com.instructure.canvas.espresso.withElementRepeat +import com.instructure.canvas.espresso.* import com.instructure.canvasapi2.models.DiscussionEntry import com.instructure.canvasapi2.models.DiscussionTopicHeader -import com.instructure.espresso.OnViewWithId -import com.instructure.espresso.assertDisplayed -import com.instructure.espresso.assertGone -import com.instructure.espresso.assertHasText -import com.instructure.espresso.assertNotDisplayed -import com.instructure.espresso.click +import com.instructure.espresso.* import com.instructure.espresso.page.BasePage -import com.instructure.espresso.page.withText -import com.instructure.espresso.scrollTo +import com.instructure.espresso.page.waitForViewWithId import com.instructure.student.R import com.instructure.student.ui.utils.TypeInRCETextEditor import org.hamcrest.Matchers.allOf @@ -112,7 +100,7 @@ class DiscussionDetailsPage : BasePage(R.id.discussionDetailsPage) { fun sendReply(replyMessage: String) { clickReply() - onView(withId(R.id.rce_webView)).perform(TypeInRCETextEditor(replyMessage)) + waitForViewWithId(R.id.rce_webView).perform(TypeInRCETextEditor(replyMessage)) onView(withId(R.id.menu_send)).click() sleep(3000) // wait out the toast message diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/DiscussionListPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/DiscussionListPage.kt index ff70d393ea..28bbb99a01 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/DiscussionListPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/DiscussionListPage.kt @@ -106,19 +106,19 @@ class DiscussionListPage : BasePage(R.id.discussionListPage) { waitForDiscussionTopicToDisplay(name) } - fun createAnnouncement(name: String, description: String, verify: Boolean = true) { + fun createAnnouncement(name: String, description: String) { createNewDiscussion.click() onView(withId(R.id.announcementNameEditText)).perform(DirectlyPopulateEditText(name)) onView(withId(R.id.rce_webView)).perform(TypeInRCETextEditor(description)) onView(withId(R.id.menuSaveAnnouncement)).perform(explicitClick()) + } - if(verify) { - var expectedTitle = name - if (name.isNullOrEmpty()) { + fun assertAnnouncementCreated(inputTitle: String) { + var expectedTitle = inputTitle + if (inputTitle.isNullOrEmpty()) { expectedTitle = InstrumentationRegistry.getInstrumentation().targetContext.resources.getString(R.string.utils_noTitle) } waitForDiscussionTopicToDisplay(expectedTitle) - } } fun launchCreateAnnouncementThenClose() { diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/FileUploadPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/FileUploadPage.kt index 39b4c4fbdf..e39fa3a3cc 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,18 @@ 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.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 +38,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() @@ -49,6 +54,29 @@ class FileUploadPage : BasePage() { } fun clickUpload() { - onView(allOf(isAssignableFrom(Button::class.java),containsTextCaseInsensitive("upload"))).click() + onView(allOf(isAssignableFrom(Button::class.java), withText(R.string.upload))).click() + } + + fun clickTurnIn() { + onView(withText(R.string.turnIn)).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/InboxConversationPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/InboxConversationPage.kt index f289d611af..c703f81202 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/InboxConversationPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/InboxConversationPage.kt @@ -36,6 +36,7 @@ import androidx.test.espresso.matcher.ViewMatchers.isAssignableFrom import androidx.test.espresso.matcher.ViewMatchers.isDisplayingAtLeast import androidx.test.espresso.matcher.ViewMatchers.withContentDescription import androidx.test.espresso.matcher.ViewMatchers.withHint +import androidx.test.platform.app.InstrumentationRegistry import com.instructure.canvas.espresso.containsTextCaseInsensitive import com.instructure.canvas.espresso.explicitClick import com.instructure.canvas.espresso.scrollRecyclerView @@ -119,8 +120,8 @@ class InboxConversationPage : BasePage(R.id.inboxConversationPage) { fun assertMessageDisplayed(message: String) { val itemMatcher = CoreMatchers.allOf( - ViewMatchers.hasSibling(withId(R.id.attachmentContainer)), - ViewMatchers.hasSibling(withId(R.id.headerDivider)), + hasSibling(withId(R.id.attachmentContainer)), + hasSibling(withId(R.id.headerDivider)), withId(R.id.messageBody), withText(message) ) @@ -137,8 +138,8 @@ class InboxConversationPage : BasePage(R.id.inboxConversationPage) { } fun refresh() { - Espresso.onView(Matchers.allOf(ViewMatchers.withId(R.id.swipeRefreshLayout), ViewMatchers.isDisplayingAtLeast(10))) - .perform(withCustomConstraints(ViewActions.swipeDown(), ViewMatchers.isDisplayingAtLeast(10))) + onView(allOf(ViewMatchers.withId(R.id.swipeRefreshLayout), isDisplayingAtLeast(10))) + .perform(withCustomConstraints(ViewActions.swipeDown(), isDisplayingAtLeast(10))) } fun toggleStarred() { diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/InboxPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/InboxPage.kt index c4579b654a..4095b8436b 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/InboxPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/InboxPage.kt @@ -35,10 +35,13 @@ import com.instructure.canvasapi2.models.Conversation import com.instructure.canvasapi2.models.Course import com.instructure.dataseeding.model.ConversationApiModel import com.instructure.espresso.OnViewWithId +import com.instructure.espresso.RecyclerViewItemCountGreaterThanAssertion +import com.instructure.espresso.WaitForViewWithId import com.instructure.espresso.assertDisplayed import com.instructure.espresso.click import com.instructure.espresso.page.* import com.instructure.espresso.scrollTo +import com.instructure.espresso.swipeDown import com.instructure.student.R import org.hamcrest.Matchers.allOf import org.hamcrest.Matchers.not @@ -49,6 +52,7 @@ class InboxPage : BasePage(R.id.inboxPage) { private val createMessageButton by OnViewWithId(R.id.addMessage) private val scopeButton by OnViewWithId(R.id.filterButton) private val filterButton by OnViewWithId(R.id.inboxFilter) + private val inboxRecyclerView by WaitForViewWithId(R.id.inboxRecyclerView) fun assertConversationDisplayed(conversation: ConversationApiModel) { assertConversationDisplayed(conversation.subject) @@ -60,6 +64,10 @@ class InboxPage : BasePage(R.id.inboxPage) { onView(matcher).assertDisplayed() } + fun assertConversationNotDisplayed(conversation: ConversationApiModel) { + assertConversationNotDisplayed(conversation.subject) + } + fun assertConversationNotDisplayed(subject: String) { val matcher = withText(subject) onView(matcher).check(doesNotExist()) @@ -72,12 +80,16 @@ class InboxPage : BasePage(R.id.inboxPage) { onView(matcher).assertDisplayed() } - fun selectConversation(conversation: ConversationApiModel) { - val matcher = withText(conversation.subject) + fun selectConversation(subject: String) { + val matcher = withText(subject) scrollRecyclerView(R.id.inboxRecyclerView, matcher) onView(matcher).click() } + fun selectConversation(conversation: ConversationApiModel) { + selectConversation(conversation.subject) + } + fun selectConversation(conversation: Conversation) { waitForView(withId(R.id.inboxRecyclerView)) val matcher = withText(conversation.subject) @@ -110,13 +122,15 @@ class InboxPage : BasePage(R.id.inboxPage) { onView(withId(R.id.bottomNavigationHome)).click() } - fun assertConversationStarred(conversation: Conversation) { + fun assertConversationStarred(subject: String) { val matcher = allOf( - withId(R.id.star), - withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE), - ViewMatchers.withParent(ViewMatchers.withParent(withChild( - allOf(withId(R.id.message), withText(conversation.lastMessage)) - )))) + withId(R.id.star), + withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE), + hasSibling(withId(R.id.userName)), + hasSibling(withId(R.id.date)), + ViewMatchers.withParent(ViewMatchers.withParent(withChild( + allOf(withId(R.id.subjectView), withText(subject)))) + )) waitForMatcherWithRefreshes(matcher) // May need to refresh before the star shows up scrollRecyclerView(R.id.inboxRecyclerView, matcher) onView(matcher).assertDisplayed() @@ -127,7 +141,7 @@ class InboxPage : BasePage(R.id.inboxPage) { val matcher = allOf( withId(R.id.unreadMark), withEffectiveVisibility(visibility), - ViewMatchers.withParent(ViewMatchers.hasSibling(withChild( + ViewMatchers.withParent(hasSibling(withChild( allOf(withId(R.id.message), withText(conversation.lastMessage)) )))) @@ -141,5 +155,34 @@ class InboxPage : BasePage(R.id.inboxPage) { } } + fun assertUnreadMarkerVisibility(subject: String, visibility: ViewMatchers.Visibility) { + val matcher = allOf( + withId(R.id.unreadMark), + withEffectiveVisibility(visibility), + hasSibling(allOf(withId(R.id.avatar))), + ViewMatchers.withParent(hasSibling(withChild( + allOf(withId(R.id.subjectView), withText(subject)))) + ) + ) + if(visibility == ViewMatchers.Visibility.VISIBLE) { + waitForMatcherWithRefreshes(matcher) // May need to refresh before the unread mark shows up + scrollRecyclerView(R.id.inboxRecyclerView, matcher) + onView(matcher).assertDisplayed() + } + else if(visibility == ViewMatchers.Visibility.GONE) { + onView(matcher).check(matches(not(isDisplayed()))) + } + } + + fun assertInboxEmpty() { + onView(withId(R.id.emptyInboxView)).assertDisplayed() + } + fun assertHasConversation() { + assertConversationCountIsGreaterThan(0) + } + + fun assertConversationCountIsGreaterThan(count: Int) { + inboxRecyclerView.check(RecyclerViewItemCountGreaterThanAssertion(count)) + } } \ No newline at end of file diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/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/SettingsPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/SettingsPage.kt index 9cbb41a9de..5301e642e4 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/SettingsPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/SettingsPage.kt @@ -17,9 +17,9 @@ package com.instructure.student.ui.pages import com.instructure.espresso.OnViewWithId -import com.instructure.espresso.assertHasContentDescription +import com.instructure.espresso.TextViewColorAssertion import com.instructure.espresso.click -import com.instructure.espresso.page.BasePage +import com.instructure.espresso.page.* import com.instructure.espresso.scrollTo import com.instructure.student.R @@ -28,29 +28,49 @@ class SettingsPage : BasePage(R.id.settingsFragment) { private val profileSettingLabel by OnViewWithId(R.id.profileSettings) private val accountPreferencesLabel by OnViewWithId(R.id.accountPreferences) private val pushNotificationsLabel by OnViewWithId(R.id.pushNotifications) + // The pairObserverLabel may not be present if the corresponding remote-config flag is disabled. - private val pairObserverLabel by OnViewWithId(R.id.pairObserver,autoAssert=false) + private val pairObserverLabel by OnViewWithId(R.id.pairObserver, autoAssert = false) private val aboutLabel by OnViewWithId(R.id.about) private val legalLabel by OnViewWithId(R.id.legal) private val remoteConfigLabel by OnViewWithId(R.id.remoteConfigParams) + private val appThemeTitle by OnViewWithId(R.id.appThemeTitle) + private val appThemeStatus by OnViewWithId(R.id.appThemeStatus) - fun launchAboutPage() { + fun openAboutPage() { aboutLabel.click() } - fun launchLegalPage() { + fun openLegalPage() { legalLabel.scrollTo().click() } - fun launchRemoteConfigParams() { + fun openRemoteConfigParams() { remoteConfigLabel.scrollTo().click() } - fun launchPairObserverPage() { + fun openPairObserverPage() { pairObserverLabel.scrollTo().click() } - fun launchProfileSettings() { + fun openProfileSettings() { profileSettingLabel.scrollTo().click() } + + fun openAppThemeSettings() { + appThemeTitle.scrollTo().click() + } + + fun selectAppTheme(appTheme: String) + { + onView(withText(appTheme) + withParent(R.id.select_dialog_listview)).click() + } + + fun assertAppThemeTitleTextColor(expectedTextColor: String) { + appThemeTitle.check(TextViewColorAssertion(expectedTextColor)) + } + + fun assertAppThemeStatusTextColor(expectedTextColor: String) { + appThemeStatus.check(TextViewColorAssertion(expectedTextColor)) + } } diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/ShareExtensionStatusPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/ShareExtensionStatusPage.kt new file mode 100644 index 0000000000..83d8ba9a25 --- /dev/null +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/ShareExtensionStatusPage.kt @@ -0,0 +1,49 @@ +/* + * 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.click +import com.instructure.espresso.page.BasePage +import com.instructure.espresso.page.onView +import com.instructure.espresso.page.withId +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 assertAssignmentSubmissionSuccess() { + dialogTitle.assertHasText(R.string.submission) + subtitle.assertHasText(R.string.submissionSuccessTitle) + description.assertHasText(R.string.submissionSuccessMessage) + } + + fun assertFileUploadSuccess() { + dialogTitle.assertHasText(R.string.fileUpload) + subtitle.assertHasText(R.string.fileUploadSuccess) + description.assertHasText(R.string.filesUploadedSuccessfully) + } + + fun clickOnDone() { + onView(withId(R.id.doneButton)).click() + } + +} \ 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..bd94dbda59 --- /dev/null +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/ShareExtensionTargetPage.kt @@ -0,0 +1,74 @@ +/* + * 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.assertion.ViewAssertions +import androidx.test.espresso.matcher.RootMatchers.isDialog +import androidx.test.espresso.matcher.ViewMatchers +import com.instructure.espresso.* +import com.instructure.espresso.page.* +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 assertNoAssignmentSelectedStringDisplayed() { + onViewWithId(R.id.assignmentSpinner).assertDisplayed() + onView(withText(R.string.noAssignmentsWithFileUpload) + 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/pages/TodoPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/TodoPage.kt index e49581bad1..34efa39342 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/TodoPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/TodoPage.kt @@ -17,15 +17,12 @@ package com.instructure.student.ui.pages import android.widget.Button -import androidx.test.espresso.Espresso import androidx.test.espresso.Espresso.onView import androidx.test.espresso.assertion.ViewAssertions.doesNotExist -import androidx.test.espresso.matcher.ViewMatchers import androidx.test.espresso.matcher.ViewMatchers.isAssignableFrom import androidx.test.espresso.matcher.ViewMatchers.withId import androidx.test.espresso.matcher.ViewMatchers.withText import com.instructure.canvas.espresso.containsTextCaseInsensitive -import com.instructure.canvas.espresso.scrollRecyclerView import com.instructure.canvasapi2.models.Assignment import com.instructure.canvasapi2.models.Quiz import com.instructure.dataseeding.model.AssignmentApiModel @@ -51,10 +48,18 @@ class TodoPage: BasePage(R.id.todoPage) { assertTextDisplayedInRecyclerView(assignment.name!!) } + fun assertQuizDisplayed(quiz: QuizApiModel) { + assertTextDisplayedInRecyclerView(quiz.title) + } + fun assertQuizDisplayed(quiz: Quiz) { assertTextDisplayedInRecyclerView(quiz.title!!) } + fun assertQuizNotDisplayed(quiz: QuizApiModel) { + onView(withText(quiz.title!!)).check(doesNotExist()) + } + fun assertQuizNotDisplayed(quiz: Quiz) { onView(withText(quiz.title!!)).check(doesNotExist()) } @@ -69,9 +74,7 @@ class TodoPage: BasePage(R.id.todoPage) { onView(withText(quiz.title!!)).click() } - fun assertQuizDisplayed(quiz: QuizApiModel) { - assertTextDisplayedInRecyclerView(quiz.title) - } + fun chooseFavoriteCourseFilter() { onView(withId(R.id.todoListFilter)).click() diff --git a/apps/student/src/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/StudentActivityTestRule.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/utils/StudentActivityTestRule.kt index bace30a97b..f14c13ddba 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/utils/StudentActivityTestRule.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/utils/StudentActivityTestRule.kt @@ -21,6 +21,7 @@ import android.content.Context import com.instructure.student.util.CacheControlFlags import com.instructure.student.util.StudentPrefs import com.instructure.espresso.InstructureActivityTestRule +import com.instructure.loginapi.login.util.LoginPrefs import com.instructure.loginapi.login.util.PreviousUsersUtils import com.instructure.pandautils.utils.PandaAppResetter import com.instructure.pandautils.utils.ThemePrefs @@ -32,6 +33,7 @@ class StudentActivityTestRule(activityClass: Class) : Instructu StudentPrefs.clearPrefs() CacheControlFlags.clearPrefs() PreviousUsersUtils.clear(context) + LoginPrefs.clearPrefs() // We need to set this true so the theme selector won't stop our tests. ThemePrefs.themeSelectionShown = true 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/AnnotationComments/AnnotationCommentListFragment.kt b/apps/student/src/main/java/com/instructure/student/AnnotationComments/AnnotationCommentListFragment.kt index e85b97f110..fcf1f0b600 100644 --- a/apps/student/src/main/java/com/instructure/student/AnnotationComments/AnnotationCommentListFragment.kt +++ b/apps/student/src/main/java/com/instructure/student/AnnotationComments/AnnotationCommentListFragment.kt @@ -58,6 +58,7 @@ class AnnotationCommentListFragment : ParentFragment() { private var docSession by ParcelableArg() private var apiValues by ParcelableArg() private var headAnnotationId by StringArg() + private var canComment by BooleanArg(true, CAN_COMMENT) private var recyclerAdapter: AnnotationCommentListRecyclerAdapter? = null @@ -132,7 +133,7 @@ class AnnotationCommentListFragment : ParentFragment() { private fun setupCommentInput() { // We want users with read permission to still be able to create and respond to comments. - if(docSession.annotationMetadata?.canRead() == false) { + if(docSession.annotationMetadata?.canRead() == false || !canComment) { commentInputContainer.setVisible(false) } else { sendCommentButton.imageTintList = ViewStyler.generateColorStateList( @@ -233,11 +234,12 @@ class AnnotationCommentListFragment : ParentFragment() { private const val DOC_SESSION = "docSession" private const val API_VALUES = "apiValues" private const val HEAD_ANNOTATION_ID = "headAnnotationId" + private const val CAN_COMMENT = "canComment" fun newInstance(bundle: Bundle) = AnnotationCommentListFragment().apply { arguments = bundle } - fun makeRoute(annotations: ArrayList, headAnnotationId: String, docSession: DocSession, apiValues: ApiValues, assigneeId: Long): Route { - val args = makeBundle(annotations, headAnnotationId, docSession, apiValues, assigneeId) + fun makeRoute(annotations: ArrayList, headAnnotationId: String, docSession: DocSession, apiValues: ApiValues, assigneeId: Long, canComment: Boolean): Route { + val args = makeBundle(annotations, headAnnotationId, docSession, apiValues, assigneeId, canComment) return Route(null, AnnotationCommentListFragment::class.java, null, args) } @@ -255,13 +257,14 @@ class AnnotationCommentListFragment : ParentFragment() { return AnnotationCommentListFragment().withArgs(route.arguments) } - fun makeBundle(annotations: ArrayList, headAnnotationId: String, docSession: DocSession, apiValues: ApiValues, assigneeId: Long): Bundle { + fun makeBundle(annotations: ArrayList, headAnnotationId: String, docSession: DocSession, apiValues: ApiValues, assigneeId: Long, canComment: Boolean): Bundle { val args = Bundle() args.putParcelableArrayList(ANNOTATIONS, annotations) args.putLong(ASSIGNEE_ID, assigneeId) args.putParcelable(DOC_SESSION, docSession) args.putParcelable(API_VALUES, apiValues) args.putString(HEAD_ANNOTATION_ID, headAnnotationId) + args.putBoolean(CAN_COMMENT, canComment) return args } } diff --git a/apps/student/src/main/java/com/instructure/student/activity/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 d39e0c2b4b..ea6c6fe56c 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 @@ -38,18 +38,20 @@ import com.instructure.student.BuildConfig import com.instructure.student.R import com.instructure.student.flutterChannels.FlutterComm import com.instructure.student.fragment.InboxFragment +import com.instructure.student.fragment.NotificationListFragment import com.instructure.student.service.StudentPageViewService import com.instructure.student.util.StudentPrefs import kotlinx.coroutines.Job import retrofit2.Call import retrofit2.Response -abstract class CallbackActivity : ParentActivity(), InboxFragment.OnUnreadCountInvalidated { +abstract class CallbackActivity : ParentActivity(), InboxFragment.OnUnreadCountInvalidated, NotificationListFragment.OnNotificationCountInvalidated { private var loadInitialDataJob: Job? = null abstract fun gotLaunchDefinitions(launchDefinitions: List?) - abstract fun updateUnreadCount(unreadCount: String) + abstract fun updateUnreadCount(unreadCount: Int) + abstract fun updateNotificationCount(notificationCount: Int) abstract fun initialCoreDataLoadingComplete() override fun onCreate(savedInstanceState: Bundle?) { @@ -60,8 +62,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,19 +117,16 @@ 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 getUnreadMessageCount() + getUnreadNotificationCount() + initialCoreDataLoadingComplete() } catch { initialCoreDataLoadingComplete() @@ -139,10 +136,19 @@ abstract class CallbackActivity : ParentActivity(), InboxFragment.OnUnreadCountI private suspend fun getUnreadMessageCount() { val unreadCount = awaitApi { UnreadCountManager.getUnreadConversationCount(it, true) } unreadCount.let { - updateUnreadCount(it.unreadCount!!) + val unreadCountInt = (it.unreadCount ?: "0").toInt() + updateUnreadCount(unreadCountInt) } } + private fun getUnreadNotificationCount() { + UnreadCountManager.getUnreadNotificationCount(object : StatusCallback>() { + override fun onResponse(data: Call>, response: Response>) { + updateNotificationCount(response.body()?.sumOf { it.unreadCount.orDefault() }.orDefault()) + } + }, true) + } + private val themeCallback = object : StatusCallback() { override fun onResponse(response: Response, linkHeaders: LinkHeaders, type: ApiType) { //store the theme @@ -207,6 +213,10 @@ abstract class CallbackActivity : ParentActivity(), InboxFragment.OnUnreadCountI } } + override fun invalidateNotificationCount() { + getUnreadNotificationCount() + } + /** * This will fetch the user forcing a network request */ 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/LoginLandingPageActivity.kt b/apps/student/src/main/java/com/instructure/student/activity/LoginLandingPageActivity.kt index 72700c9f86..2f4ad68d00 100644 --- a/apps/student/src/main/java/com/instructure/student/activity/LoginLandingPageActivity.kt +++ b/apps/student/src/main/java/com/instructure/student/activity/LoginLandingPageActivity.kt @@ -63,8 +63,8 @@ class LoginLandingPageActivity : BaseLoginLandingPageActivity() { return ContextCompat.getColor(this, R.color.login_studentAppTheme) } - override fun signInActivityIntent(snickerDoodle: SnickerDoodle): Intent { - return SignInActivity.createIntent(this, AccountDomain(snickerDoodle.domain)) + override fun signInActivityIntent(accountDomain: AccountDomain): Intent { + return SignInActivity.createIntent(this, accountDomain) } override fun loginWithQRCodeEnabled(): Boolean = true diff --git a/apps/student/src/main/java/com/instructure/student/activity/NavigationActivity.kt b/apps/student/src/main/java/com/instructure/student/activity/NavigationActivity.kt index 579c9f20da..8d32f87378 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 @@ -23,10 +23,8 @@ import android.content.Intent import android.content.pm.PackageManager import android.content.res.Configuration 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 @@ -34,6 +32,8 @@ import android.widget.CompoundButton import android.widget.ImageView import android.widget.TextView import android.widget.Toast +import androidx.annotation.IdRes +import androidx.annotation.PluralsRes import androidx.appcompat.app.ActionBarDrawerToggle import androidx.appcompat.app.AlertDialog import androidx.appcompat.widget.Toolbar @@ -45,9 +45,6 @@ import androidx.fragment.app.DialogFragment 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 @@ -66,15 +63,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.* @@ -103,7 +102,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" @@ -340,9 +338,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) @@ -419,23 +417,7 @@ class NavigationActivity : BaseRouterActivity(), Navigation, MasqueradingDialog. if (user != null) { navigationDrawerUserName.text = Pronouns.span(user.shortName, user.pronouns) navigationDrawerUserEmail.text = user.primaryEmail - - if(ProfileUtils.shouldLoadAltAvatarImage(user.avatarUrl)) { - val initials = ProfileUtils.getUserInitials(user.shortName ?: "") - val color = ContextCompat.getColor(context, R.color.textDark) - val drawable = TextDrawable.builder() - .beginConfig() - .height(context.resources.getDimensionPixelSize(R.dimen.profileAvatarSize)) - .width(context.resources.getDimensionPixelSize(R.dimen.profileAvatarSize)) - .toUpperCase() - .useFont(Typeface.DEFAULT_BOLD) - .textColor(color) - .endConfig() - .buildRound(initials, Color.WHITE) - navigationDrawerProfileImage.setImageDrawable(drawable) - } else { - Glide.with(context).load(user.avatarUrl).into(navigationDrawerProfileImage) - } + ProfileUtils.loadAvatarForUser(navigationDrawerProfileImage, user.shortName, user.avatarUrl) } } @@ -749,8 +731,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 @@ -1037,51 +1019,25 @@ class NavigationActivity : BaseRouterActivity(), Navigation, MasqueradingDialog. override fun canBookmark(): Boolean = navigationBehavior.visibleNavigationMenuItems.contains(NavigationMenuItem.BOOKMARKS) - override fun updateUnreadCount(unreadCount: String) { - // get the view - val bottomBarNavView = bottomBar?.getChildAt(0) - // get the inbox item - val view = (bottomBarNavView as BottomNavigationMenuView).getChildAt(4) - - // create the badge, set the text and color it - val unreadCountValue = unreadCount.toInt() - var unreadCountDisplay = unreadCount - if(unreadCountValue > 99) { - unreadCountDisplay = getString(R.string.moreThan99) - } else if(unreadCountValue <= 0) { - //don't set the badge or display it, remove any badge - if(view.children.size > 2 && view.children[2] is TextView) { - (view as BottomNavigationItemView).removeViewAt(2) - } - // update content description with no unread count number - bottomBar.menu.items.find { it.itemId == R.id.bottomNavigationInbox }.let { - val title = it?.title - MenuItemCompat.setContentDescription(it, title) - } - return - } - - // update content description - bottomBar.menu.items.find { it.itemId == R.id.bottomNavigationInbox }.let { - var title: String = it?.title as String - title += "$unreadCountValue " + getString(R.string.unread) - MenuItemCompat.setContentDescription(it, title) - } + override fun updateUnreadCount(unreadCount: Int) { + updateBottomBarBadge(R.id.bottomNavigationInbox, unreadCount, R.plurals.a11y_inboxUnreadCount) + } - // check to see if we already have a badge created - with((view as BottomNavigationItemView)) { - // first child is the imageView that we use for the bottom bar, second is a layout for the label - if(childCount > 2 && getChildAt(2) is TextView) { - (getChildAt(2) as TextView).text = unreadCountDisplay - } else { - // no badge, we need to create one - val badge = LayoutInflater.from(context) - .inflate(R.layout.unread_count, bottomBar, false) - (badge as TextView).text = unreadCountDisplay + override fun updateNotificationCount(notificationCount: Int) { + updateBottomBarBadge(R.id.bottomNavigationNotifications, notificationCount, R.plurals.a11y_notificationsUnreadCount) + } - ColorUtils.colorIt(ContextCompat.getColor(context, R.color.backgroundInfo), badge.background) - addView(badge) + private fun updateBottomBarBadge(@IdRes menuItemId: Int, count: Int, @PluralsRes quantityContentDescription: Int? = null) { + if (count > 0) { + bottomBar.getOrCreateBadge(menuItemId).number = count + bottomBar.getOrCreateBadge(menuItemId).backgroundColor = getColor(R.color.backgroundInfo) + bottomBar.getOrCreateBadge(menuItemId).badgeTextColor = getColor(R.color.white) + if (quantityContentDescription != null) { + bottomBar.getOrCreateBadge(menuItemId).setContentDescriptionQuantityStringsResource(quantityContentDescription) } + } else { + // Don't set the badge or display it, remove any badge + bottomBar.removeBadge(menuItemId) } } diff --git a/apps/student/src/main/java/com/instructure/student/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/adapter/NotificationListRecyclerAdapter.kt b/apps/student/src/main/java/com/instructure/student/adapter/NotificationListRecyclerAdapter.kt index b6d5845683..ff1cf533e7 100644 --- a/apps/student/src/main/java/com/instructure/student/adapter/NotificationListRecyclerAdapter.kt +++ b/apps/student/src/main/java/com/instructure/student/adapter/NotificationListRecyclerAdapter.kt @@ -303,7 +303,8 @@ class NotificationListRecyclerAdapter( notifyDataSetChanged() } } - } + }, + false ) } @@ -319,7 +320,10 @@ class NotificationListRecyclerAdapter( streamItem.id, object : StatusCallback() { override fun onResponse(response: Response, linkHeaders: LinkHeaders, type: ApiType) { - if (response.body()!!.isHidden) removeItem(streamItem) + if (response.body()!!.isHidden) { + removeItem(streamItem) + adapterToFragmentCallback.onItemRemoved() + } } override fun onFail(call: Call?, error: Throwable, response: Response<*>?) { diff --git a/apps/student/src/main/java/com/instructure/student/adapter/assignment/AssignmentListByDateRecyclerAdapter.kt b/apps/student/src/main/java/com/instructure/student/adapter/assignment/AssignmentListByDateRecyclerAdapter.kt index e05ea31f9d..51dc99259b 100644 --- a/apps/student/src/main/java/com/instructure/student/adapter/assignment/AssignmentListByDateRecyclerAdapter.kt +++ b/apps/student/src/main/java/com/instructure/student/adapter/assignment/AssignmentListByDateRecyclerAdapter.kt @@ -18,8 +18,11 @@ package com.instructure.student.adapter.assignment import android.content.Context -import com.instructure.canvasapi2.models.* -import com.instructure.canvasapi2.utils.* +import com.instructure.canvasapi2.models.Assignment +import com.instructure.canvasapi2.models.AssignmentGroup +import com.instructure.canvasapi2.models.CanvasContext +import com.instructure.canvasapi2.utils.filterWithQuery +import com.instructure.canvasapi2.utils.toDate import com.instructure.pandarecycler.util.GroupSortedList import com.instructure.pandarecycler.util.Types import com.instructure.student.R @@ -35,8 +38,9 @@ class AssignmentListByDateRecyclerAdapter( context: Context, canvasContext: CanvasContext, adapterToAssignmentsCallback: AdapterToAssignmentsCallback, - isTesting: Boolean = false -) : AssignmentListRecyclerAdapter(context, canvasContext, adapterToAssignmentsCallback, isTesting) { + isTesting: Boolean = false, + filter: AssignmentListFilter = AssignmentListFilter.ALL +) : AssignmentListRecyclerAdapter(context, canvasContext, adapterToAssignmentsCallback, isTesting, filter) { private val overdue = AssignmentGroup(name = context.getString(R.string.overdueAssignments), position = HEADER_POSITION_OVERDUE) private val upcoming = AssignmentGroup(name = context.getString(R.string.upcomingAssignments), position = HEADER_POSITION_UPCOMING) @@ -69,6 +73,15 @@ class AssignmentListByDateRecyclerAdapter( // endtodo assignmentGroup.assignments .filterWithQuery(searchQuery, Assignment::name) + .filter { + when (filter) { + AssignmentListFilter.ALL -> true + AssignmentListFilter.MISSING -> it.isMissing() + AssignmentListFilter.LATE -> it.submission?.late ?: false + AssignmentListFilter.GRADED -> it.submission?.isGraded ?: false + AssignmentListFilter.UPCOMING -> !it.isSubmitted && it.dueDate?.after(Date()) ?: false + } + } .forEach { assignment -> val dueAt = assignment.dueAt val submission = assignment.submission diff --git a/apps/student/src/main/java/com/instructure/student/adapter/assignment/AssignmentListByTypeRecyclerAdapter.kt b/apps/student/src/main/java/com/instructure/student/adapter/assignment/AssignmentListByTypeRecyclerAdapter.kt index 569115e6c1..15f21199ee 100644 --- a/apps/student/src/main/java/com/instructure/student/adapter/assignment/AssignmentListByTypeRecyclerAdapter.kt +++ b/apps/student/src/main/java/com/instructure/student/adapter/assignment/AssignmentListByTypeRecyclerAdapter.kt @@ -24,17 +24,30 @@ import com.instructure.canvasapi2.utils.filterWithQuery import com.instructure.pandarecycler.util.GroupSortedList import com.instructure.pandarecycler.util.Types import com.instructure.student.interfaces.AdapterToAssignmentsCallback +import java.util.* class AssignmentListByTypeRecyclerAdapter( context: Context, canvasContext: CanvasContext, adapterToAssignmentsCallback: AdapterToAssignmentsCallback, - isTesting: Boolean = false -) : AssignmentListRecyclerAdapter(context, canvasContext, adapterToAssignmentsCallback, isTesting) { + isTesting: Boolean = false, + filter: AssignmentListFilter = AssignmentListFilter.ALL +) : AssignmentListRecyclerAdapter(context, canvasContext, adapterToAssignmentsCallback, isTesting, filter) { override fun populateData() { - assignmentGroups.forEach { assignmentGroup -> - val filteredAssignments = assignmentGroup.assignments.filterWithQuery(searchQuery, Assignment::name) + assignmentGroups + .forEach { assignmentGroup -> + val filteredAssignments = assignmentGroup.assignments + .filterWithQuery(searchQuery, Assignment::name) + .filter { + when (filter) { + AssignmentListFilter.ALL -> true + AssignmentListFilter.MISSING -> it.isMissing() + AssignmentListFilter.LATE -> it.submission?.late ?: false + AssignmentListFilter.GRADED -> it.submission?.isGraded ?: false + AssignmentListFilter.UPCOMING -> !it.isSubmitted && it.dueDate?.after(Date()) ?: false + } + } addOrUpdateAllItems(assignmentGroup, filteredAssignments) } isAllPagesLoaded = true diff --git a/apps/student/src/main/java/com/instructure/student/adapter/assignment/AssignmentListRecyclerAdapter.kt b/apps/student/src/main/java/com/instructure/student/adapter/assignment/AssignmentListRecyclerAdapter.kt index 57620483b9..4118c9024c 100644 --- a/apps/student/src/main/java/com/instructure/student/adapter/assignment/AssignmentListRecyclerAdapter.kt +++ b/apps/student/src/main/java/com/instructure/student/adapter/assignment/AssignmentListRecyclerAdapter.kt @@ -23,7 +23,9 @@ import com.instructure.canvasapi2.StatusCallback import com.instructure.canvasapi2.managers.AssignmentManager import com.instructure.canvasapi2.managers.CourseManager import com.instructure.canvasapi2.models.* -import com.instructure.canvasapi2.utils.* +import com.instructure.canvasapi2.utils.ApiType +import com.instructure.canvasapi2.utils.LinkHeaders +import com.instructure.canvasapi2.utils.Logger import com.instructure.canvasapi2.utils.weave.WeaveJob import com.instructure.canvasapi2.utils.weave.awaitApi import com.instructure.canvasapi2.utils.weave.catch @@ -45,7 +47,8 @@ abstract class AssignmentListRecyclerAdapter ( context: Context, private val canvasContext: CanvasContext, private val adapterToAssignmentsCallback: AdapterToAssignmentsCallback, - isTesting: Boolean = false + isTesting: Boolean = false, + filter: AssignmentListFilter = AssignmentListFilter.ALL ) : ExpandableRecyclerAdapter( context, AssignmentGroup::class.java, @@ -57,6 +60,16 @@ abstract class AssignmentListRecyclerAdapter ( private var apiJob: WeaveJob? = null protected var assignmentGroups: List = emptyList() + var filter: AssignmentListFilter = AssignmentListFilter.ALL + set(value) { + field = value + if (isAllPagesLoaded) { + clear() + populateData() + onCallbackFinished(ApiType.CACHE) + } + } + var searchQuery: String = "" set(value) { field = value @@ -70,6 +83,7 @@ abstract class AssignmentListRecyclerAdapter ( init { isExpandedByDefault = true isDisplayEmptyCell = true + this.filter = filter if (!isTesting) loadData() } @@ -230,4 +244,12 @@ abstract class AssignmentListRecyclerAdapter ( super.cancel() apiJob?.cancel() } +} + +enum class AssignmentListFilter { + ALL, + LATE, + MISSING, + GRADED, + UPCOMING } \ No newline at end of file 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 dce655d913..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 @@ -23,13 +23,13 @@ import android.view.View import android.view.ViewGroup import androidx.appcompat.app.AlertDialog import androidx.fragment.app.Fragment -import com.instructure.canvasapi2.utils.APIHelper -import com.instructure.canvasapi2.utils.ApiPrefs +import com.instructure.canvasapi2.utils.* import com.instructure.canvasapi2.utils.pageview.PageView import com.instructure.loginapi.login.dialog.NoInternetConnectionDialog import com.instructure.pandautils.analytics.SCREEN_VIEW_APPLICATION_SETTINGS 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 @@ -95,7 +95,11 @@ class ApplicationSettingsFragment : ParentFragment() { } pushNotifications.onClick { - addFragment(NotificationPreferencesFragment.newInstance()) + addFragment(PushNotificationPreferencesFragment.newInstance()) + } + + emailNotifications.onClick { + addFragment(EmailNotificationPreferencesFragment.newInstance()) } about.onClick { @@ -117,6 +121,11 @@ class ApplicationSettingsFragment : ParentFragment() { ViewStyler.themeSwitch(requireContext(), elementaryViewSwitch, ThemePrefs.brandColor) elementaryViewSwitch.setOnCheckedChangeListener { _, isChecked -> ApiPrefs.elementaryDashboardEnabledOverride = isChecked + + val analyticsBundle = Bundle().apply { + putBoolean(AnalyticsParamConstants.MANUAL_C4E_STATE, isChecked) + } + Analytics.logEvent(AnalyticsEventConstants.CHANGED_C4E_MODE, analyticsBundle) } } diff --git a/apps/student/src/main/java/com/instructure/student/fragment/AssignmentListFragment.kt b/apps/student/src/main/java/com/instructure/student/fragment/AssignmentListFragment.kt index 91edffab83..a0b0b17c4d 100644 --- a/apps/student/src/main/java/com/instructure/student/fragment/AssignmentListFragment.kt +++ b/apps/student/src/main/java/com/instructure/student/fragment/AssignmentListFragment.kt @@ -20,13 +20,17 @@ package com.instructure.student.fragment import android.content.DialogInterface import android.content.res.Configuration import android.os.Bundle +import android.os.Handler import android.view.LayoutInflater +import android.view.MenuItem import android.view.View import android.view.ViewGroup import android.widget.AdapterView import androidx.annotation.StringRes import androidx.appcompat.app.AlertDialog import com.google.android.material.appbar.AppBarLayout +import com.google.android.material.badge.BadgeDrawable +import com.google.android.material.badge.BadgeUtils import com.instructure.canvasapi2.models.Assignment import com.instructure.canvasapi2.models.CanvasContext import com.instructure.canvasapi2.models.GradingPeriod @@ -44,6 +48,7 @@ import com.instructure.student.R import com.instructure.student.adapter.TermSpinnerAdapter import com.instructure.student.adapter.assignment.AssignmentListByDateRecyclerAdapter import com.instructure.student.adapter.assignment.AssignmentListByTypeRecyclerAdapter +import com.instructure.student.adapter.assignment.AssignmentListFilter import com.instructure.student.adapter.assignment.AssignmentListRecyclerAdapter import com.instructure.student.interfaces.AdapterToAssignmentsCallback import com.instructure.student.mobius.assignmentDetails.ui.AssignmentDetailsFragment @@ -51,6 +56,7 @@ import com.instructure.student.router.RouteMatcher import com.instructure.student.util.StudentPrefs import kotlinx.android.synthetic.main.assignment_list_layout.* +@com.google.android.material.badge.ExperimentalBadgeUtils @ScreenView(SCREEN_VIEW_ASSIGNMENT_LIST) @PageView(url = "{canvasContext}/assignments") class AssignmentListFragment : ParentFragment(), Bookmarkable { @@ -60,6 +66,11 @@ class AssignmentListFragment : ParentFragment(), Bookmarkable { private lateinit var recyclerAdapter: AssignmentListRecyclerAdapter private var termAdapter: TermSpinnerAdapter? = null + private var filterPosition = 0 + private var filter = AssignmentListFilter.ALL + + private var badgeDrawable: BadgeDrawable? = null + private var sortOrder: AssignmentsSortOrder get() { val preferenceKey = StudentPrefs.getString("sortBy_${canvasContext.contextId}", AssignmentsSortOrder.SORT_BY_TIME.preferenceKey) @@ -91,7 +102,7 @@ class AssignmentListFragment : ParentFragment(), Bookmarkable { } override fun onRefreshFinished() { - if(!isAdded) return // Refresh can finish after user has left screen, causing emptyView to be null + if (!isAdded) return // Refresh can finish after user has left screen, causing emptyView to be null setRefreshing(false) if (recyclerAdapter.size() == 0) { setEmptyView(emptyView, R.drawable.ic_panda_space, R.string.noAssignments, R.string.noAssignmentsSubtext) @@ -116,12 +127,12 @@ class AssignmentListFragment : ParentFragment(), Bookmarkable { sortByButton.contentDescription = getString(sortOrder.contentDescriptionRes) configureRecyclerView( - view, - requireContext(), - recyclerAdapter, - R.id.swipeRefreshLayout, - R.id.emptyView, - R.id.listView + view, + requireContext(), + recyclerAdapter, + R.id.swipeRefreshLayout, + R.id.emptyView, + R.id.listView ) appbar.addOnOffsetChangedListener(AppBarLayout.OnOffsetChangedListener { _, i -> @@ -136,11 +147,23 @@ class AssignmentListFragment : ParentFragment(), Bookmarkable { setupSortByButton() } + override fun onHiddenChanged(hidden: Boolean) { + super.onHiddenChanged(hidden) + if (!hidden) { + updateBadge() + } + } + + override fun onResume() { + super.onResume() + updateBadge() + } + private fun createRecyclerAdapter(): AssignmentListRecyclerAdapter { return if (sortOrder == AssignmentsSortOrder.SORT_BY_TIME) { - AssignmentListByDateRecyclerAdapter(requireContext(), canvasContext, adapterToAssignmentsCallback) + AssignmentListByDateRecyclerAdapter(requireContext(), canvasContext, adapterToAssignmentsCallback, filter = filter) } else { - AssignmentListByTypeRecyclerAdapter(requireContext(), canvasContext, adapterToAssignmentsCallback) + AssignmentListByTypeRecyclerAdapter(requireContext(), canvasContext, adapterToAssignmentsCallback, filter = filter) } } @@ -148,10 +171,10 @@ class AssignmentListFragment : ParentFragment(), Bookmarkable { sortByButton.onClick { val checkedItemIndex = sortOrder.index AlertDialog.Builder(requireContext(), R.style.AccentDialogTheme) - .setTitle(R.string.sortByDialogTitle) - .setSingleChoiceItems(R.array.assignmentsSortByOptions, checkedItemIndex, this@AssignmentListFragment::sortOrderSelected) - .setNegativeButton(R.string.sortByDialogCancel) { dialog, _ -> dialog.dismiss() } - .show() + .setTitle(R.string.sortByDialogTitle) + .setSingleChoiceItems(R.array.assignmentsSortByOptions, checkedItemIndex, this@AssignmentListFragment::sortOrderSelected) + .setNegativeButton(R.string.sortByDialogCancel) { dialog, _ -> dialog.dismiss() } + .show() } } @@ -169,8 +192,40 @@ class AssignmentListFragment : ParentFragment(), Bookmarkable { } } + private fun showAssignmentFilterDialog() { + AlertDialog.Builder(requireContext(), R.style.AccentDialogTheme) + .setTitle(R.string.filterAssignmentDialogTitle) + .setSingleChoiceItems(R.array.assignmentsFilterOptions, filterPosition, this@AssignmentListFragment::filterSelected) + .setNegativeButton(R.string.filterAssignmentsDialogCancel) { dialog, _ -> dialog.dismiss() } + .show() + } + + private fun filterSelected(dialog: DialogInterface, index: Int) { + dialog.dismiss() + filterPosition = index + filter = AssignmentListFilter.values()[index] + recyclerAdapter.filter = filter + updateBadge() + } + + private fun updateBadge() { + Handler().postDelayed({ + if (badgeDrawable == null) { + badgeDrawable = BadgeDrawable.create(requireContext()).apply { + backgroundColor = ThemePrefs.accentColor + } + } + if (filterPosition == 0) { + BadgeUtils.detachBadgeDrawable(badgeDrawable, toolbar, R.id.menu_filter_assignments) + } else { + BadgeUtils.attachBadgeDrawable(badgeDrawable!!, toolbar, R.id.menu_filter_assignments) + } + }, 100) + } + + override fun applyTheme() { - setupToolbarMenu(toolbar) + setupToolbarMenu(toolbar, R.menu.menu_assignment_list) toolbar.title = title() toolbar.setupAsBackButton(this) toolbar.addSearch(getString(R.string.searchAssignmentsHint)) { query -> @@ -184,13 +239,22 @@ class AssignmentListFragment : ParentFragment(), Bookmarkable { ViewStyler.themeToolbarColored(requireActivity(), toolbar, canvasContext) } + override fun onOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + R.id.menu_filter_assignments -> { + showAssignmentFilterDialog() + } + } + return super.onOptionsItemSelected(item) + } + private fun setupGradingPeriods(periods: List) { val hasGradingPeriods = periods.isNotEmpty() val adapter = TermSpinnerAdapter( - requireContext(), - android.R.layout.simple_spinner_dropdown_item, - periods + allTermsGradingPeriod, - hasGradingPeriods + requireContext(), + android.R.layout.simple_spinner_dropdown_item, + periods + allTermsGradingPeriod, + hasGradingPeriods ) termSpinner.isEnabled = hasGradingPeriods termAdapter = adapter @@ -227,12 +291,12 @@ class AssignmentListFragment : ParentFragment(), Bookmarkable { override fun onConfigurationChanged(newConfig: Configuration) { super.onConfigurationChanged(newConfig) configureRecyclerView( - requireView(), - requireContext(), - recyclerAdapter, - R.id.swipeRefreshLayout, - R.id.emptyView, - R.id.listView, + requireView(), + requireContext(), + recyclerAdapter, + R.id.swipeRefreshLayout, + R.id.emptyView, + R.id.listView, R.string.noAssignments ) if (recyclerAdapter.size() == 0) { @@ -274,18 +338,18 @@ class AssignmentListFragment : ParentFragment(), Bookmarkable { } enum class AssignmentsSortOrder( - val index: Int, - val preferenceKey: String, - @StringRes val buttonTextRes: Int, - @StringRes val contentDescriptionRes: Int, - @StringRes val orderSelectedAnnouncement: Int, - val analyticsKey: String) { + val index: Int, + val preferenceKey: String, + @StringRes val buttonTextRes: Int, + @StringRes val contentDescriptionRes: Int, + @StringRes val orderSelectedAnnouncement: Int, + val analyticsKey: String) { SORT_BY_TIME(0, "time", R.string.sortByTime, R.string.a11y_sortByTimeButton, - R.string.a11y_assignmentsSortedByTime, AnalyticsEventConstants.ASSIGNMENT_LIST_SORT_BY_TIME_SELECTED), + R.string.a11y_assignmentsSortedByTime, AnalyticsEventConstants.ASSIGNMENT_LIST_SORT_BY_TIME_SELECTED), SORT_BY_TYPE(1, "type", R.string.sortByType, R.string.a11y_sortByTypeButton, - R.string.a11y_assignmentsSortedByType, AnalyticsEventConstants.ASSIGNMENT_LIST_SORT_BY_TYPE_SELECTED); + R.string.a11y_assignmentsSortedByType, AnalyticsEventConstants.ASSIGNMENT_LIST_SORT_BY_TYPE_SELECTED); companion object { fun fromPreferenceKey(key: String?): AssignmentsSortOrder { diff --git a/apps/student/src/main/java/com/instructure/student/fragment/BasicQuizViewFragment.kt b/apps/student/src/main/java/com/instructure/student/fragment/BasicQuizViewFragment.kt index 3f7e2150f8..2466ecdfd1 100644 --- a/apps/student/src/main/java/com/instructure/student/fragment/BasicQuizViewFragment.kt +++ b/apps/student/src/main/java/com/instructure/student/fragment/BasicQuizViewFragment.kt @@ -116,7 +116,9 @@ class BasicQuizViewFragment : InternalWebviewFragment() { view.loadUrl(url, APIHelper.referrer) true } else { // It's content but not a quiz. Could link to a discussion (or whatever) in a quiz. Route - RouteMatcher.canRouteInternally(requireActivity(), url, ApiPrefs.domain, true) + activity?.let { + RouteMatcher.canRouteInternally(it, url, ApiPrefs.domain, true) + } ?: false }// Might need to log in to take the quiz -- the url would say domain/login. If we just use the AppRouter it will take the user // back to the dashboard. This check will keep them here and let them log in and take the quiz } diff --git a/apps/student/src/main/java/com/instructure/student/fragment/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/CreateAnnouncementFragment.kt b/apps/student/src/main/java/com/instructure/student/fragment/CreateAnnouncementFragment.kt index 026a1de8cc..299eb26e61 100644 --- a/apps/student/src/main/java/com/instructure/student/fragment/CreateAnnouncementFragment.kt +++ b/apps/student/src/main/java/com/instructure/student/fragment/CreateAnnouncementFragment.kt @@ -106,7 +106,7 @@ class CreateAnnouncementFragment : ParentFragment() { else -> null }?.let { imageUri -> // If the image Uri is not null, upload it - rceImageUploadJob = MediaUploadUtils.uploadRceImageJob(imageUri, canvasContext, requireActivity()) { text, alt -> announcementRCEView.insertImage(text, alt) } + rceImageUploadJob = MediaUploadUtils.uploadRceImageJob(imageUri, canvasContext, requireActivity()) { imageUrl -> announcementRCEView.insertImage(requireActivity(), imageUrl) } } } } diff --git a/apps/student/src/main/java/com/instructure/student/fragment/CreateDiscussionFragment.kt b/apps/student/src/main/java/com/instructure/student/fragment/CreateDiscussionFragment.kt index b7d1d89f62..2964157553 100644 --- a/apps/student/src/main/java/com/instructure/student/fragment/CreateDiscussionFragment.kt +++ b/apps/student/src/main/java/com/instructure/student/fragment/CreateDiscussionFragment.kt @@ -124,7 +124,7 @@ class CreateDiscussionFragment : ParentFragment() { else -> null }?.let { imageUri -> // If the image Uri is not null, upload it - rceImageJob = MediaUploadUtils.uploadRceImageJob(imageUri, canvasContext, requireActivity()) { text, alt -> descriptionRCEView.insertImage(text, alt) } + rceImageJob = MediaUploadUtils.uploadRceImageJob(imageUri, canvasContext, requireActivity()) { imageUrl -> descriptionRCEView.insertImage(requireActivity(), imageUrl) } } } } diff --git a/apps/student/src/main/java/com/instructure/student/fragment/DashboardFragment.kt b/apps/student/src/main/java/com/instructure/student/fragment/DashboardFragment.kt index 6acd19c5c4..b700bc17ec 100644 --- a/apps/student/src/main/java/com/instructure/student/fragment/DashboardFragment.kt +++ b/apps/student/src/main/java/com/instructure/student/fragment/DashboardFragment.kt @@ -134,7 +134,7 @@ class DashboardFragment : ParentFragment() { course.name = response.nickname!! course.originalName = response.name } - recyclerAdapter?.addOrUpdateItem(DashboardRecyclerAdapter.ItemType.COURSE_HEADER, course) + recyclerAdapter?.notifyDataSetChanged() } catch { toast(R.string.courseNicknameError) } diff --git a/apps/student/src/main/java/com/instructure/student/fragment/DiscussionDetailsFragment.kt b/apps/student/src/main/java/com/instructure/student/fragment/DiscussionDetailsFragment.kt index b54f39a311..71c4cbf429 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 @@ -387,7 +387,7 @@ class DiscussionDetailsFragment : ParentFragment(), Bookmarkable { // It won't work exactl when the page starts to load, because the html document is not yet created, // so we add a little delay to make sure the script can modify the document. if (addDarkTheme) { - webView.postDelayed({ webView.addDarkThemeToHtmlDocument() }, 50) + webView.postDelayed({ webView.addDarkThemeToHtmlDocument() }, 100) } } override fun onPageFinishedCallback(webView: WebView, url: String) { @@ -405,6 +405,7 @@ class DiscussionDetailsFragment : ParentFragment(), Bookmarkable { private fun setupHeaderWebView() { setupWebView(discussionTopicHeaderWebView) discussionTopicHeaderWebView.addJavascriptInterface(JSDiscussionHeaderInterface(), "accessor") + DiscussionManager.markDiscussionTopicRead(canvasContext, getTopicId(), object : StatusCallback() {}) } @SuppressLint("SetJavaScriptEnabled") @@ -544,7 +545,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/DiscussionListFragment.kt b/apps/student/src/main/java/com/instructure/student/fragment/DiscussionListFragment.kt index 928f671281..b020383bba 100644 --- a/apps/student/src/main/java/com/instructure/student/fragment/DiscussionListFragment.kt +++ b/apps/student/src/main/java/com/instructure/student/fragment/DiscussionListFragment.kt @@ -88,7 +88,6 @@ open class DiscussionListFragment : ParentFragment(), Bookmarkable { super.onCreate(savedInstanceState) checkForPermission() checkFeatureFlags() - retainInstance = true } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { @@ -122,10 +121,12 @@ open class DiscussionListFragment : ParentFragment(), Bookmarkable { // Show the FAB. if(canPost) createNewDiscussion?.show() if (recyclerAdapter.size() == 0) { - if (isAnnouncement) { - setEmptyView(emptyView, R.drawable.ic_panda_noannouncements, R.string.noAnnouncements, R.string.noAnnouncementsSubtext) - } else { - setEmptyView(emptyView, R.drawable.ic_panda_nodiscussions, R.string.noDiscussions, R.string.noDiscussionsSubtext) + emptyView?.let { + if (isAnnouncement) { + setEmptyView(it, R.drawable.ic_panda_noannouncements, R.string.noAnnouncements, R.string.noAnnouncementsSubtext) + } else { + setEmptyView(it, R.drawable.ic_panda_nodiscussions, R.string.noDiscussions, R.string.noDiscussionsSubtext) + } } } } @@ -235,6 +236,7 @@ open class DiscussionListFragment : ParentFragment(), Bookmarkable { super.onDestroyView() permissionJob?.cancel() featureFlagsJob?.cancel() + groupsJob?.cancel() recyclerAdapter.cancel() } //endregion @@ -333,6 +335,7 @@ open class DiscussionListFragment : ParentFragment(), Bookmarkable { @Suppress("unused") @Subscribe(threadMode = ThreadMode.MAIN, sticky = true) fun onDiscussionTopicCountChange(event: DiscussionTopicHeaderEvent) { + if (isAnnouncement) return event.get { // Gets written over on phones - added also to {@link #onRefreshFinished()} when { diff --git a/apps/student/src/main/java/com/instructure/student/fragment/DiscussionsReplyFragment.kt b/apps/student/src/main/java/com/instructure/student/fragment/DiscussionsReplyFragment.kt index 1fa11d9664..a1bb95d43e 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,9 @@ 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.features.file.upload.FileUploadDialogParent import com.instructure.pandautils.utils.* import com.instructure.pandautils.views.AttachmentView import com.instructure.student.R @@ -47,7 +48,7 @@ import retrofit2.Response import java.io.File @ScreenView(SCREEN_VIEW_DISCUSSIONS_REPLY) -class DiscussionsReplyFragment : ParentFragment() { +class DiscussionsReplyFragment : ParentFragment(), FileUploadDialogParent { private var canvasContext: CanvasContext by ParcelableArg(key = Const.CANVAS_CONTEXT) @@ -76,12 +77,8 @@ 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) { - handleAttachment(attachment) - } - } + val bundle = FileUploadDialogFragment.createDiscussionsBundle(attachments) + FileUploadDialogFragment.newInstance(bundle).show(childFragmentManager, FileUploadDialogFragment.TAG) } else { NoInternetConnectionDialog.show(requireFragmentManager()) } @@ -89,6 +86,12 @@ class DiscussionsReplyFragment : ParentFragment() { } } + override fun attachmentCallback(event: Int, attachment: FileSubmitObject?) { + if (event == FileUploadDialogFragment.EVENT_ON_FILE_SELECTED) { + handleAttachment(attachment) + } + } + //region Fragment Lifecycle Overrides override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -119,7 +122,7 @@ class DiscussionsReplyFragment : ParentFragment() { else -> null }?.let { imageUri -> // If the image Uri is not null, upload it - MediaUploadUtils.uploadRceImageJob(imageUri, canvasContext, requireActivity()) { text, alt -> rceTextEditor.insertImage(text, alt) } + MediaUploadUtils.uploadRceImageJob(imageUri, canvasContext, requireActivity()) { imageUrl -> rceTextEditor.insertImage(requireActivity(), imageUrl) } } } } @@ -187,7 +190,7 @@ class DiscussionsReplyFragment : ParentFragment() { } } } catch { - if (isAdded && (it as StatusCallbackError).response?.code() != 400) messageFailure() + if (isVisible && (it as StatusCallbackError).response?.code() != 400) messageFailure() } } @@ -213,16 +216,16 @@ class DiscussionsReplyFragment : ParentFragment() { } else { // Post failure // 400 will be handled elsewhere. it means the quota has been reached - if (response.code() != 400 && isAdded) { + if (response.code() != 400 && isVisible) { messageFailure() } } } private fun messageFailure() { - toolbar.menu.findItem(R.id.menu_send).isVisible = true - toolbar.menu.findItem(R.id.menu_attachment).isVisible = true - savingProgressBar.visibility = View.GONE + toolbar.menu.findItem(R.id.menu_send)?.isVisible = true + toolbar.menu.findItem(R.id.menu_attachment)?.isVisible = true + savingProgressBar?.visibility = View.GONE toast(R.string.utils_discussionSentFailure) } //endregion diff --git a/apps/student/src/main/java/com/instructure/student/fragment/EditPageDetailsFragment.kt b/apps/student/src/main/java/com/instructure/student/fragment/EditPageDetailsFragment.kt index 1943616e0d..8135eb2e46 100644 --- a/apps/student/src/main/java/com/instructure/student/fragment/EditPageDetailsFragment.kt +++ b/apps/student/src/main/java/com/instructure/student/fragment/EditPageDetailsFragment.kt @@ -95,7 +95,7 @@ class EditPageDetailsFragment : ParentFragment() { else -> null }?.let { imageUri -> // If the image Uri is not null, upload it - rceImageJob = MediaUploadUtils.uploadRceImageJob(imageUri, canvasContext, requireActivity()) { text, alt -> pageRCEView.insertImage(text, alt) } + rceImageJob = MediaUploadUtils.uploadRceImageJob(imageUri, canvasContext, requireActivity()) { imageUrl -> pageRCEView.insertImage(requireActivity(), imageUrl) } } } } diff --git a/apps/student/src/main/java/com/instructure/student/fragment/FileListFragment.kt b/apps/student/src/main/java/com/instructure/student/fragment/FileListFragment.kt index 198877c353..3035098881 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,8 @@ 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.features.file.upload.FileUploadDialogParent import com.instructure.pandautils.utils.* import com.instructure.student.R import com.instructure.student.adapter.FileFolderCallback @@ -58,10 +61,11 @@ 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 -class FileListFragment : ParentFragment(), Bookmarkable { +class FileListFragment : ParentFragment(), Bookmarkable, FileUploadDialogParent { private var canvasContext by ParcelableArg(key = Const.CANVAS_CONTEXT) @@ -406,8 +410,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).show(childFragmentManager, FileUploadDialogFragment.TAG) + } + } + + override 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..d9d2f032b9 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,10 @@ 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.FileUploadDialogParent +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,10 +55,11 @@ 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() { +class InboxComposeMessageFragment : ParentFragment(), FileUploadDialogParent { private val conversation by NullableParcelableArg(key = Const.CONVERSATION) private val participants by ParcelableArrayListArg(key = PARTICIPANTS) @@ -279,8 +284,8 @@ class InboxComposeMessageFragment : ParentFragment() { sendMessage() } R.id.menu_attachment -> { - val bundle = UploadFilesDialog.createMessageAttachmentsBundle(arrayListOf()) - UploadFilesDialog.show(fragmentManager, bundle, { _ -> }) + val bundle = FileUploadDialogFragment.createMessageAttachmentsBundle(arrayListOf()) + FileUploadDialogFragment.newInstance(bundle).show(childFragmentManager, FileUploadDialogFragment.TAG) } else -> return@setOnMenuItemClickListener false } @@ -438,16 +443,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() + override fun workInfoLiveDataCallback(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/InboxFragment.kt b/apps/student/src/main/java/com/instructure/student/fragment/InboxFragment.kt index 2721d36458..7ce2c4d92f 100644 --- a/apps/student/src/main/java/com/instructure/student/fragment/InboxFragment.kt +++ b/apps/student/src/main/java/com/instructure/student/fragment/InboxFragment.kt @@ -131,6 +131,8 @@ class InboxFragment : ParentFragment() { } } }) + + applyTheme() } override fun onConfigurationChanged(newConfig: Configuration) { diff --git a/apps/student/src/main/java/com/instructure/student/fragment/NotificationListFragment.kt b/apps/student/src/main/java/com/instructure/student/fragment/NotificationListFragment.kt index a0fd308b16..9eeaabb39b 100644 --- a/apps/student/src/main/java/com/instructure/student/fragment/NotificationListFragment.kt +++ b/apps/student/src/main/java/com/instructure/student/fragment/NotificationListFragment.kt @@ -24,6 +24,7 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.Toast +import androidx.fragment.app.FragmentManager import com.instructure.canvasapi2.models.* import com.instructure.canvasapi2.models.StreamItem.Type.* import com.instructure.canvasapi2.utils.ApiPrefs @@ -47,7 +48,7 @@ import kotlinx.android.synthetic.main.panda_recycler_refresh_layout.* @ScreenView(SCREEN_VIEW_NOTIFICATION_LIST) @PageView -class NotificationListFragment : ParentFragment(), Bookmarkable { +class NotificationListFragment : ParentFragment(), Bookmarkable, FragmentManager.OnBackStackChangedListener { @PageViewUrl @Suppress("unused") @@ -79,6 +80,7 @@ class NotificationListFragment : ParentFragment(), Bookmarkable { emptyView.setGuidelines(.28f,.6f,.73f,.12f,.88f) } } + (activity as? OnNotificationCountInvalidated)?.invalidateNotificationCount() } override fun onShowEditView(isVisible: Boolean) { @@ -88,12 +90,15 @@ class NotificationListFragment : ParentFragment(), Bookmarkable { override fun onShowErrorCrouton(message: Int) { showToast(message) } + + override fun onItemRemoved() { + (activity as? OnNotificationCountInvalidated)?.invalidateNotificationCount() + } } // Used to help determine if the bottom bar should be highlighted fun isCourseOrGroup(): Boolean = canvasContext.isCourseOrGroup - override fun title(): String = getString(if (canvasContext.isCourse || canvasContext.isGroup) R.string.homePageIdForNotifications else R.string.notifications) override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? @@ -119,10 +124,25 @@ class NotificationListFragment : ParentFragment(), Bookmarkable { cancelButton.setOnClickListener { recyclerAdapter.cancelButtonClicked() } applyTheme() + + activity?.supportFragmentManager?.addOnBackStackChangedListener(this) + } + + private var shouldRefreshOnResume = false + + override fun onBackStackChanged() { + if (activity?.supportFragmentManager?.fragments?.lastOrNull()?.javaClass == this.javaClass) { + if (shouldRefreshOnResume) { + swipeRefreshLayout.isRefreshing = true + recyclerAdapter.refresh() + shouldRefreshOnResume = false + } + } } override fun onDestroyView() { recyclerAdapter.cancel() + activity?.supportFragmentManager?.removeOnBackStackChangedListener(this) super.onDestroyView() } @@ -184,12 +204,17 @@ class NotificationListFragment : ParentFragment(), Bookmarkable { return false } addFragmentForStreamItem(streamItem, activity as ParentActivity, false) + shouldRefreshOnResume = !streamItem.isReadState return true } override val bookmark: Bookmarker get() = Bookmarker(canvasContext.isCourseOrGroup, canvasContext) + interface OnNotificationCountInvalidated { + fun invalidateNotificationCount() + } + companion object { fun addFragmentForStreamItem(streamItem: StreamItem, context: Context, fromWidget: Boolean) { if (fromWidget) { 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/fragment/PeopleDetailsFragment.kt b/apps/student/src/main/java/com/instructure/student/fragment/PeopleDetailsFragment.kt index 80d28715af..27921afa79 100644 --- a/apps/student/src/main/java/com/instructure/student/fragment/PeopleDetailsFragment.kt +++ b/apps/student/src/main/java/com/instructure/student/fragment/PeopleDetailsFragment.kt @@ -116,7 +116,7 @@ class PeopleDetailsFragment : ParentFragment(), Bookmarkable { private fun setupUserViews() { user?.let { u -> - ProfileUtils.loadAvatarForUser(avatar, u) + ProfileUtils.loadAvatarForUser(avatar, u.name, u.avatarUrl) userName.text = Pronouns.span(u.name, u.pronouns) userRole.text = u.enrollments.distinctBy { it.displayType }.joinToString { it.displayType } userBackground.setBackgroundColor(ColorKeeper.getOrGenerateColor(canvasContext)) diff --git a/apps/student/src/main/java/com/instructure/student/holders/InboxMessageHolder.kt b/apps/student/src/main/java/com/instructure/student/holders/InboxMessageHolder.kt index 0ea1ea38e7..a4fac3b5c3 100644 --- a/apps/student/src/main/java/com/instructure/student/holders/InboxMessageHolder.kt +++ b/apps/student/src/main/java/com/instructure/student/holders/InboxMessageHolder.kt @@ -37,7 +37,7 @@ import com.instructure.student.interfaces.MessageAdapterCallback import com.instructure.student.view.ViewUtils import kotlinx.android.synthetic.main.viewholder_message.view.* import java.text.SimpleDateFormat -import java.util.Locale +import java.util.* class InboxMessageHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { @@ -52,7 +52,7 @@ class InboxMessageHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { // Set author info if (author != null) { authorName.text = getAuthorTitle(author.id, conversation, message) - ProfileUtils.loadAvatarForUser(authorAvatar, author) + ProfileUtils.loadAvatarForUser(authorAvatar, author.name, author.avatarUrl) authorAvatar.setupAvatarA11y(author.name) authorAvatar.onClick { callback.onAvatarClicked(author) } } else { diff --git a/apps/student/src/main/java/com/instructure/student/holders/NotificationViewHolder.kt b/apps/student/src/main/java/com/instructure/student/holders/NotificationViewHolder.kt index 10c978c857..17e2fa47bb 100644 --- a/apps/student/src/main/java/com/instructure/student/holders/NotificationViewHolder.kt +++ b/apps/student/src/main/java/com/instructure/student/holders/NotificationViewHolder.kt @@ -25,14 +25,11 @@ import androidx.core.content.ContextCompat import androidx.recyclerview.widget.RecyclerView import com.instructure.canvasapi2.models.CanvasContext import com.instructure.canvasapi2.models.StreamItem -import com.instructure.pandautils.utils.ColorKeeper -import com.instructure.pandautils.utils.ThemePrefs -import com.instructure.pandautils.utils.setGone -import com.instructure.pandautils.utils.setVisible +import com.instructure.pandautils.utils.* import com.instructure.student.R import com.instructure.student.adapter.NotificationListRecyclerAdapter -import com.instructure.student.util.BinderUtils import com.instructure.student.interfaces.NotificationAdapterToFragmentCallback +import com.instructure.student.util.BinderUtils import kotlinx.android.synthetic.main.viewholder_notification.view.* class NotificationViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { @@ -155,8 +152,11 @@ class NotificationViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) // Read/Unread if (item.isReadState) { title.setTypeface(null, Typeface.NORMAL) + unreadMark.setInvisible() } else { title.setTypeface(null, Typeface.BOLD) + unreadMark.setVisible() + unreadMark.setImageDrawable(ColorUtils.colorIt(ThemePrefs.accentColor, unreadMark.drawable)) } } diff --git a/apps/student/src/main/java/com/instructure/student/holders/PeopleViewHolder.kt b/apps/student/src/main/java/com/instructure/student/holders/PeopleViewHolder.kt index d9ccbff790..0d6c00c201 100644 --- a/apps/student/src/main/java/com/instructure/student/holders/PeopleViewHolder.kt +++ b/apps/student/src/main/java/com/instructure/student/holders/PeopleViewHolder.kt @@ -16,6 +16,7 @@ */ package com.instructure.student.holders +import android.content.res.ColorStateList import android.view.View import androidx.recyclerview.widget.RecyclerView import com.instructure.canvasapi2.models.User @@ -25,8 +26,8 @@ import com.instructure.pandautils.utils.ProfileUtils import com.instructure.pandautils.utils.setGone import com.instructure.pandautils.utils.setVisible import com.instructure.student.R -import com.instructure.student.util.BinderUtils import com.instructure.student.interfaces.AdapterToFragmentCallback +import com.instructure.student.util.BinderUtils import kotlinx.android.synthetic.main.viewholder_people.view.* class PeopleViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { @@ -37,11 +38,11 @@ class PeopleViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { isFirstItem: Boolean, isLastItem: Boolean ) = with(itemView) { - ProfileUtils.loadAvatarForUser(icon, item) + ProfileUtils.loadAvatarForUser(icon, item.name, item.avatarUrl, 0) + icon.backgroundTintList = ColorStateList.valueOf(courseColor) itemView.setOnClickListener { adapterToFragmentCallback.onRowClicked(item, adapterPosition, true) } - icon.borderColor = courseColor title.text = Pronouns.span(item.name, item.pronouns) val enrollmentIndex = item.enrollmentIndex diff --git a/apps/student/src/main/java/com/instructure/student/holders/RecipientViewHolder.kt b/apps/student/src/main/java/com/instructure/student/holders/RecipientViewHolder.kt index d7f66f2f65..f40440525b 100644 --- a/apps/student/src/main/java/com/instructure/student/holders/RecipientViewHolder.kt +++ b/apps/student/src/main/java/com/instructure/student/holders/RecipientViewHolder.kt @@ -16,9 +16,10 @@ package com.instructure.student.holders import android.content.Context +import android.content.res.ColorStateList import android.graphics.Color -import android.graphics.drawable.ColorDrawable import android.view.View +import androidx.core.content.ContextCompat import androidx.recyclerview.widget.RecyclerView import com.instructure.canvasapi2.models.Recipient import com.instructure.canvasapi2.utils.Pronouns @@ -47,7 +48,9 @@ class RecipientViewHolder(view: View) : RecyclerView.ViewHolder(view) { fun setChecked(isChecked: Boolean = true) { if (isChecked) { setBackgroundColor(selectionColor and selectionTransparencyMask) - avatar.setImageDrawable(ColorDrawable(selectionColor)) + avatar.setImageDrawable(ContextCompat.getDrawable(context, R.drawable.ic_circle)?.apply { + mutate().setTintList(ColorStateList.valueOf(selectionColor)) + }) checkMarkImageView.setVisible() ColorUtils.colorIt(Color.WHITE, checkMarkImageView) } else { diff --git a/apps/student/src/main/java/com/instructure/student/interfaces/NotificationAdapterToFragmentCallback.kt b/apps/student/src/main/java/com/instructure/student/interfaces/NotificationAdapterToFragmentCallback.kt index 0171efd8cf..068a2a7fbf 100644 --- a/apps/student/src/main/java/com/instructure/student/interfaces/NotificationAdapterToFragmentCallback.kt +++ b/apps/student/src/main/java/com/instructure/student/interfaces/NotificationAdapterToFragmentCallback.kt @@ -21,4 +21,5 @@ interface NotificationAdapterToFragmentCallback { fun onRefreshFinished() fun onShowEditView(isVisible: Boolean) fun onShowErrorCrouton(message: Int) + fun onItemRemoved() = Unit } 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..90f466d5a1 100644 --- a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/AssignmentDetailsPresenter.kt +++ b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/AssignmentDetailsPresenter.kt @@ -17,10 +17,14 @@ package com.instructure.student.mobius.assignmentDetails import android.content.Context -import android.view.accessibility.AccessibilityManager import androidx.core.content.ContextCompat +import com.instructure.canvasapi2.StatusCallback +import com.instructure.canvasapi2.managers.SubmissionManager import com.instructure.canvasapi2.models.* -import com.instructure.canvasapi2.utils.* +import com.instructure.canvasapi2.utils.DateHelper +import com.instructure.canvasapi2.utils.NumberHelper +import com.instructure.canvasapi2.utils.isRtl +import com.instructure.canvasapi2.utils.isValid import com.instructure.pandautils.utils.AssignmentUtils2 import com.instructure.student.R import com.instructure.student.Submission @@ -31,8 +35,7 @@ import com.instructure.student.mobius.assignmentDetails.ui.QuizDescriptionViewSt import com.instructure.student.mobius.assignmentDetails.ui.gradeCell.GradeCellViewState import com.instructure.student.mobius.common.ui.Presenter import java.text.DateFormat -import java.util.Date -import java.util.Locale +import java.util.* object AssignmentDetailsPresenter : Presenter { override fun present(model: AssignmentDetailsModel, context: Context): AssignmentDetailsViewState { @@ -55,10 +58,19 @@ object AssignmentDetailsPresenter : Presenter() {}) + } + private fun presentLoadedState( assignment: Assignment, quiz: Quiz?, @@ -248,7 +260,8 @@ object AssignmentDetailsPresenter : Presenter, headAnnotationId: String, docSession: DocSession, apiValues: ApiValues) { - if (isAttachedToWindow) RouteMatcher.route(context, AnnotationCommentListFragment.makeRoute(commentList, headAnnotationId, docSession, apiValues, ApiPrefs.user!!.id)) + if (isAttachedToWindow) RouteMatcher.route(context, AnnotationCommentListFragment.makeRoute(commentList, headAnnotationId, docSession, apiValues, ApiPrefs.user!!.id, !studentAnnotationView)) } override fun showFileError() { @@ -99,7 +100,7 @@ class PdfStudentSubmissionView( override fun configureCommentView(commentsButton: ImageView) { // If we are making annotations position the comments button as we would position in the teacher. - if (studentAnnotation) { + if (studentAnnotationSubmit) { super.configureCommentView(commentsButton) return } @@ -151,7 +152,7 @@ class PdfStudentSubmissionView( override fun attachDocListener() { // We need to add this flag, because we want to show the toolbar in the student annotation, but hide when // we open an already submitted file submission with a teacher's annotations. - if (!studentAnnotation) { + if (!studentAnnotationSubmit) { // Modify the session data permissions to make sure students can't annotate already submitted assignments if (docSession.annotationMetadata?.canWrite() == true) { docSession.annotationMetadata?.permissions = "read" diff --git a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/drawer/comments/ui/views/CommentView.kt b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/drawer/comments/ui/views/CommentView.kt index d6808caac4..bb56a568c3 100644 --- a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/drawer/comments/ui/views/CommentView.kt +++ b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/drawer/comments/ui/views/CommentView.kt @@ -57,7 +57,7 @@ class CommentView @JvmOverloads constructor( fun setCommentBubbleColor(@ColorInt color: Int) = commentTextView.setBubbleColor(color) fun setAvatar(avatarUrl: String?, userName: String) { - ProfileUtils.loadAvatarForUser(avatarView, userName, avatarUrl ?: "") + ProfileUtils.loadAvatarForUser(avatarView, userName, avatarUrl.orEmpty()) } var commentTextColor: Int diff --git a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/ui/SubmissionDetailsView.kt b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/ui/SubmissionDetailsView.kt index ce7174fac7..97ee454424 100644 --- a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/ui/SubmissionDetailsView.kt +++ b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/ui/SubmissionDetailsView.kt @@ -76,6 +76,7 @@ class SubmissionDetailsView( if (slidingUpPanelLayout?.panelState == SlidingUpPanelLayout.PanelState.COLLAPSED) { slidingUpPanelLayout?.panelState = SlidingUpPanelLayout.PanelState.ANCHORED } + drawerViewPager.hideKeyboard() logTabSelected(tab?.position) } } diff --git a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/ui/AssignmentDetailsView.kt b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/ui/AssignmentDetailsView.kt index 737bf19719..8fabfa4827 100644 --- a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/ui/AssignmentDetailsView.kt +++ b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/ui/AssignmentDetailsView.kt @@ -37,13 +37,13 @@ import com.instructure.canvasapi2.utils.Pronouns import com.instructure.canvasapi2.utils.exhaustive import com.instructure.interactions.Navigation import com.instructure.interactions.router.Route +import com.instructure.pandautils.features.shareextension.ShareFileSubmissionTarget import com.instructure.pandautils.utils.* import com.instructure.pandautils.views.CanvasWebView import com.instructure.pandautils.views.RecordingMediaType import com.instructure.student.R import com.instructure.student.activity.BaseRouterActivity import com.instructure.student.activity.InternalWebViewActivity -import com.instructure.student.activity.ShareFileSubmissionTarget import com.instructure.student.fragment.* import com.instructure.student.mobius.assignmentDetails.AssignmentDetailsEvent import com.instructure.student.mobius.assignmentDetails.submission.annnotation.AnnotationSubmissionUploadFragment @@ -217,11 +217,14 @@ class AssignmentDetailsView( submitButton.text = state.submitButtonText if (state.visibilities.description) { descriptionLabel.text = state.descriptionLabel - loadHtmlJob = descriptionWebView.loadHtmlWithIframes(context, false, state.description, ::loadDescriptionHtml,{ - 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) + loadHtmlJob = descriptionWebView.loadHtmlWithIframes(context, false, state.description, + { html, contentDescription -> 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/mobius/syllabus/SyllabusPresenter.kt b/apps/student/src/main/java/com/instructure/student/mobius/syllabus/SyllabusPresenter.kt index 7f97c7209e..ec8f72acb8 100644 --- a/apps/student/src/main/java/com/instructure/student/mobius/syllabus/SyllabusPresenter.kt +++ b/apps/student/src/main/java/com/instructure/student/mobius/syllabus/SyllabusPresenter.kt @@ -29,7 +29,7 @@ import com.instructure.student.mobius.syllabus.ui.EventsViewState import com.instructure.student.mobius.syllabus.ui.ScheduleItemViewState import com.instructure.student.mobius.syllabus.ui.SyllabusViewState import com.instructure.student.util.toDueAtString -import java.util.Date +import java.util.* object SyllabusPresenter : Presenter { override fun present(model: SyllabusModel, context: Context): SyllabusViewState { @@ -56,7 +56,7 @@ object SyllabusPresenter : Presenter { eventsResult.isFail -> EventsViewState.Error eventsResult.dataOrNull.isNullOrEmpty() -> EventsViewState.Empty else -> { - EventsViewState.Loaded(eventsResult.dataOrThrow.map { + EventsViewState.Loaded(eventsResult.dataOrThrow.filter { it.isHidden.not() }.map { ScheduleItemViewState( it.itemId, it.title ?: "", 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..c50a961d90 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 @@ -84,6 +84,7 @@ object FileUtils { pspdfActivityConfiguration = PdfActivityConfiguration.Builder(context) .scrollDirection(PageScrollDirection.HORIZONTAL) .showThumbnailGrid() + .setDocumentInfoViewSeparated(false) .setThumbnailBarMode(ThumbnailBarMode.THUMBNAIL_BAR_MODE_PINNED) .enableDocumentEditor() .enabledAnnotationTools(annotationCreationList) 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/view/AttachmentView.kt b/apps/student/src/main/java/com/instructure/student/view/AttachmentView.kt index c6f0cc0063..987660b3bd 100644 --- a/apps/student/src/main/java/com/instructure/student/view/AttachmentView.kt +++ b/apps/student/src/main/java/com/instructure/student/view/AttachmentView.kt @@ -16,19 +16,18 @@ package com.instructure.student.view import android.content.Context -import android.graphics.Bitmap -import android.graphics.Canvas -import androidx.core.content.ContextCompat +import android.graphics.PorterDuff import android.view.LayoutInflater import android.view.ViewGroup import android.widget.FrameLayout import android.widget.ImageView -import com.instructure.student.R +import androidx.core.content.ContextCompat +import com.bumptech.glide.Glide +import com.bumptech.glide.request.RequestOptions import com.instructure.canvasapi2.models.Attachment import com.instructure.canvasapi2.models.RemoteFile import com.instructure.pandautils.utils.onClick -import com.squareup.picasso.Picasso -import com.squareup.picasso.Transformation +import com.instructure.student.R import kotlinx.android.synthetic.main.view_attachment.view.* import java.io.File @@ -93,27 +92,17 @@ class AttachmentView(context: Context) : FrameLayout(context) { private fun setThumbnail(path: String?) { if (path.isNullOrBlank()) return val file = File(path) - val picasso = Picasso.with(context) - val creator = if (file.exists() && file.isFile) picasso.load(file) else picasso.load(path) - creator.fit().centerCrop().transform(ATTACHMENT_PREVIEW_TRANSFORMER).into(previewImage) + Glide.with(context) + .load(if (file.exists() && file.isFile) file else path) + .apply(RequestOptions.centerCropTransform()) + .into(previewImage) + previewImage.setColorFilter( + 0xBB9B9B9B.toInt(), + PorterDuff.Mode.SRC_OVER + ) } companion object { - /** Picasso transformation to apply gray overlay on thumbnail */ - @JvmField - val ATTACHMENT_PREVIEW_TRANSFORMER: Transformation = object : Transformation { - override fun transform(source: Bitmap?): Bitmap? { - if (source == null) return null - val mutableSource = source.copy(source.config, true) - source.recycle() - val canvas = Canvas(mutableSource) - canvas.drawColor(0xBB9B9B9B.toInt()) - return mutableSource - } - - override fun key(): String = "gray-overlay" - } - fun setColorAndIcon( context: Context, contentType: String?, diff --git a/apps/student/src/main/java/com/instructure/student/widget/BaseRemoteViewsService.kt b/apps/student/src/main/java/com/instructure/student/widget/BaseRemoteViewsService.kt index 1863b747ee..a9d238e3f1 100644 --- a/apps/student/src/main/java/com/instructure/student/widget/BaseRemoteViewsService.kt +++ b/apps/student/src/main/java/com/instructure/student/widget/BaseRemoteViewsService.kt @@ -32,7 +32,7 @@ abstract class BaseRemoteViewsService : RemoteViewsService() { fun getWidgetTextColor(widgetId: Int, context: Context): Int { val widgetBackgroundPref = getWidgetBackgroundPref(widgetId) return if (widgetBackgroundPref.equals(WidgetSetupActivity.WIDGET_BACKGROUND_COLOR_LIGHT, ignoreCase = true)) - ContextCompat.getColor(context, R.color.textDarkest) else ContextCompat.getColor(context, R.color.textLightest) + ContextCompat.getColor(context, R.color.licorice) else ContextCompat.getColor(context, R.color.white) } fun getWidgetBackgroundResourceId(widgetId: Int): Int { @@ -40,6 +40,12 @@ abstract class BaseRemoteViewsService : RemoteViewsService() { return if (widgetBackgroundPref.equals(WidgetSetupActivity.WIDGET_BACKGROUND_COLOR_LIGHT, ignoreCase = true)) R.drawable.widget_light_bg else R.drawable.widget_dark_bg } + fun getWidgetSecondaryTextColor(widgetId: Int, context: Context): Int { + val widgetBackgroundPref = getWidgetBackgroundPref(widgetId) + return if (widgetBackgroundPref.equals(WidgetSetupActivity.WIDGET_BACKGROUND_COLOR_LIGHT, ignoreCase = true)) + ContextCompat.getColor(context, R.color.ash) else ContextCompat.getColor(context, R.color.tiara) + } + fun shouldHideDetails(appWidgetId: Int): Boolean { return StudentPrefs.getBoolean(WidgetSetupActivity.WIDGET_DETAILS_PREFIX + appWidgetId) } 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/GradesViewWidgetService.kt b/apps/student/src/main/java/com/instructure/student/widget/GradesViewWidgetService.kt index bfa3a06c96..acb854a860 100644 --- a/apps/student/src/main/java/com/instructure/student/widget/GradesViewWidgetService.kt +++ b/apps/student/src/main/java/com/instructure/student/widget/GradesViewWidgetService.kt @@ -79,6 +79,7 @@ class GradesViewWidgetService : BaseRemoteViewsService(), Serializable { if (!BaseRemoteViewsService.shouldHideDetails(appWidgetId)) { row.setTextViewText(R.id.courseTerm, streamItem.term?.name) + row.setTextColor(R.id.courseTerm, BaseRemoteViewsService.getWidgetSecondaryTextColor(appWidgetId, applicationContext)) } if (streamItem.isTeacher || streamItem.isTA) { 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 6c63b27a39..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 @@ -21,7 +21,6 @@ import android.appwidget.AppWidgetManager import android.content.Context import android.content.Intent import android.net.Uri -import android.os.Build import android.text.Html import android.view.View import android.widget.RemoteViews @@ -89,6 +88,7 @@ class NotificationViewWidgetService : BaseRemoteViewsService(), Serializable { if (!BaseRemoteViewsService.shouldHideDetails(appWidgetId)) { if (streamItem.getMessage(ContextKeeper.appContext) != null) { row.setTextViewText(R.id.message, StringUtilities.simplifyHTML(Html.fromHtml(streamItem.getMessage(ContextKeeper.appContext), Html.FROM_HTML_MODE_LEGACY))) + row.setTextColor(R.id.message, BaseRemoteViewsService.getWidgetSecondaryTextColor(appWidgetId, applicationContext)) } else { row.setTextViewText(R.id.message, "") row.setViewVisibility(R.id.message, View.GONE) @@ -101,6 +101,7 @@ class NotificationViewWidgetService : BaseRemoteViewsService(), Serializable { } courseAndDate += DateHelper.getDateTimeString(ContextKeeper.appContext, streamItem.updatedDate) row.setTextViewText(R.id.course_and_date, courseAndDate) + row.setTextColor(R.id.course_and_date, BaseRemoteViewsService.getWidgetSecondaryTextColor(appWidgetId, applicationContext)) row.setOnClickFillInIntent(R.id.widget_root, createIntent(streamItem)) @@ -140,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/TodoViewWidgetService.kt b/apps/student/src/main/java/com/instructure/student/widget/TodoViewWidgetService.kt index 0e5eb57d5f..6ceec1abe0 100644 --- a/apps/student/src/main/java/com/instructure/student/widget/TodoViewWidgetService.kt +++ b/apps/student/src/main/java/com/instructure/student/widget/TodoViewWidgetService.kt @@ -87,6 +87,7 @@ class TodoViewWidgetService : BaseRemoteViewsService(), Serializable { val formattedDueDate = DateHelper.getDateTimeString(ContextKeeper.appContext, streamItem.dueDate) row.setTextViewText(R.id.message, formattedDueDate) row.setViewVisibility(R.id.message, View.VISIBLE) + row.setTextColor(R.id.message, BaseRemoteViewsService.getWidgetSecondaryTextColor(appWidgetId, applicationContext)) } else { row.setViewVisibility(R.id.message, View.GONE) } @@ -95,6 +96,7 @@ class TodoViewWidgetService : BaseRemoteViewsService(), Serializable { if (!TextUtils.isEmpty(message)) { row.setTextViewText(R.id.message, message) row.setViewVisibility(R.id.message, View.VISIBLE) + row.setTextColor(R.id.message, BaseRemoteViewsService.getWidgetSecondaryTextColor(appWidgetId, applicationContext)) } else { row.setViewVisibility(R.id.message, View.GONE) } @@ -104,6 +106,7 @@ class TodoViewWidgetService : BaseRemoteViewsService(), Serializable { val formattedDueDate = DateHelper.getDateTimeString(ContextKeeper.appContext, streamItem.dueDate) row.setTextViewText(R.id.course_and_date, formattedDueDate) row.setViewVisibility(R.id.course_and_date, View.VISIBLE) + row.setTextColor(R.id.course_and_date, BaseRemoteViewsService.getWidgetSecondaryTextColor(appWidgetId, applicationContext)) } else { row.setViewVisibility(R.id.course_and_date, View.GONE) } 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/drawable/widget_dark_bg.xml b/apps/student/src/main/res/drawable/widget_dark_bg.xml index 35b1292a2e..b3ec22f981 100644 --- a/apps/student/src/main/res/drawable/widget_dark_bg.xml +++ b/apps/student/src/main/res/drawable/widget_dark_bg.xml @@ -18,7 +18,7 @@ - + - + @@ -63,7 +63,7 @@ android:layout_height="wrap_content" android:layout_gravity="center" android:text="@string/light" - android:textColor="@color/textDarkest" /> + android:textColor="@color/licorice" /> @@ -73,7 +73,7 @@ android:layout_height="wrap_content" android:layout_weight="1" android:foreground="?android:attr/selectableItemBackground" - app:cardBackgroundColor="@color/backgroundDarkest" + app:cardBackgroundColor="@color/licorice" app:cardElevation="8dp" app:cardUseCompatPadding="true" app:contentPadding="8dp"> @@ -83,7 +83,7 @@ android:layout_height="wrap_content" android:layout_gravity="center" android:text="@string/dark" - android:textColor="@color/backgroundLightest" /> + android:textColor="@color/white" /> diff --git a/apps/student/src/main/res/layout/dialog_file_upload.xml b/apps/student/src/main/res/layout/dialog_file_upload.xml index ad2de6981d..cadcbd4893 100644 --- a/apps/student/src/main/res/layout/dialog_file_upload.xml +++ b/apps/student/src/main/res/layout/dialog_file_upload.xml @@ -37,12 +37,13 @@ android:gravity="center_vertical" android:paddingBottom="16dp"> - + android:background="@drawable/ic_circle" + android:visibility="invisible" /> + + - + android:orientation="horizontal" + android:id="@+id/statusDetails"> - - + android:layout_marginTop="16dp" + android:background="@android:color/transparent" /> - + android:importantForAccessibility="no" /> diff --git a/apps/student/src/main/res/layout/unread_count.xml b/apps/student/src/main/res/layout/unread_count.xml deleted file mode 100644 index 6688c6fdee..0000000000 --- a/apps/student/src/main/res/layout/unread_count.xml +++ /dev/null @@ -1,36 +0,0 @@ - - - diff --git a/apps/student/src/main/res/layout/upload_file_destination.xml b/apps/student/src/main/res/layout/upload_file_destination.xml deleted file mode 100644 index 2cc4bc03f3..0000000000 --- a/apps/student/src/main/res/layout/upload_file_destination.xml +++ /dev/null @@ -1,192 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/apps/student/src/main/res/layout/view_comment.xml b/apps/student/src/main/res/layout/view_comment.xml index cb38fedc87..04657f3dd4 100644 --- a/apps/student/src/main/res/layout/view_comment.xml +++ b/apps/student/src/main/res/layout/view_comment.xml @@ -27,7 +27,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content"> - - - + style="@style/AdapterItem"> + + diff --git a/apps/student/src/main/res/layout/viewholder_people.xml b/apps/student/src/main/res/layout/viewholder_people.xml index a6ae91fea4..9baa1bc88f 100644 --- a/apps/student/src/main/res/layout/viewholder_people.xml +++ b/apps/student/src/main/res/layout/viewholder_people.xml @@ -25,12 +25,13 @@ android:layout_width="match_parent" android:layout_height="wrap_content"> - - + android:layout_height="40dp" /> + + + + \ No newline at end of file diff --git a/apps/student/src/main/res/values/arrays.xml b/apps/student/src/main/res/values/arrays.xml index f11c9955c2..264de5aee2 100644 --- a/apps/student/src/main/res/values/arrays.xml +++ b/apps/student/src/main/res/values/arrays.xml @@ -20,4 +20,12 @@ @string/sortByDialogTimeOption @string/sortByDialogTypeOption + + + @string/filterAssignmentAll + @string/filterAssignmentLate + @string/filterAssignmentMissing + @string/filterAssignmentGraded + @string/filterAssignmentUpcoming + \ No newline at end of file diff --git a/apps/student/src/main/res/values/styles.xml b/apps/student/src/main/res/values/styles.xml index 94fd5a9aa0..6b679b34e4 100644 --- a/apps/student/src/main/res/values/styles.xml +++ b/apps/student/src/main/res/values/styles.xml @@ -84,7 +84,7 @@ @@ -110,7 +110,7 @@ diff --git a/apps/teacher/src/test/java/com/instructure/teacher/features/syllabus/SyllabusPresenterTest.kt b/apps/teacher/src/test/java/com/instructure/teacher/features/syllabus/SyllabusPresenterTest.kt index 91da53c104..9c71e19974 100644 --- a/apps/teacher/src/test/java/com/instructure/teacher/features/syllabus/SyllabusPresenterTest.kt +++ b/apps/teacher/src/test/java/com/instructure/teacher/features/syllabus/SyllabusPresenterTest.kt @@ -79,6 +79,14 @@ class SyllabusPresenterTest { assignment = Assignment(id = 125L, submissionTypesRaw = listOf("discussion_topic")), startAt = null, itemType = ScheduleItem.Type.TYPE_ASSIGNMENT + ), + ScheduleItem( + itemId = "4", + title = "discussion", + assignment = Assignment(id = 126L, submissionTypesRaw = listOf("discussion_topic")), + startAt = null, + itemType = ScheduleItem.Type.TYPE_ASSIGNMENT, + isHidden = true ) ) diff --git a/automation/dataseedingapi/src/main/kotlin/com/instructure/dataseeding/api/ConferencesApi.kt b/automation/dataseedingapi/src/main/kotlin/com/instructure/dataseeding/api/ConferencesApi.kt new file mode 100644 index 0000000000..3359cf5004 --- /dev/null +++ b/automation/dataseedingapi/src/main/kotlin/com/instructure/dataseeding/api/ConferencesApi.kt @@ -0,0 +1,35 @@ +package com.instructure.dataseeding.api + +import com.instructure.dataseeding.model.ConferencesRequestApiModel +import com.instructure.dataseeding.model.ConferencesResponseApiModel +import com.instructure.dataseeding.model.WebConferenceWrapper +import com.instructure.dataseeding.util.CanvasNetworkAdapter +import retrofit2.Call +import retrofit2.http.Body +import retrofit2.http.POST +import retrofit2.http.Path + +object ConferencesApi { + interface ConferencesService { + + @POST("courses/{courseId}/conferences") + fun createCourseConference(@Path("courseId") courseId: Long, @Body createConference: WebConferenceWrapper): Call + } + + private fun conferencesService(token: String): ConferencesService + = CanvasNetworkAdapter.retrofitWithToken(token).create(ConferencesService::class.java) + + fun createCourseConference(token: String, title: String, description: String, conferenceType: String, longRunning: Boolean, duration: Int, userIds: List, courseId: Long): ConferencesResponseApiModel { + val conference = WebConferenceWrapper(webConference = ConferencesRequestApiModel( + title, + description, + conferenceType, + longRunning, + duration, + userIds) + ) + + return conferencesService(token).createCourseConference(courseId, conference).execute().body()!! + } + +} diff --git a/automation/dataseedingapi/src/main/kotlin/com/instructure/dataseeding/model/ConferencesRequestApiModel.kt b/automation/dataseedingapi/src/main/kotlin/com/instructure/dataseeding/model/ConferencesRequestApiModel.kt new file mode 100644 index 0000000000..f76dea60af --- /dev/null +++ b/automation/dataseedingapi/src/main/kotlin/com/instructure/dataseeding/model/ConferencesRequestApiModel.kt @@ -0,0 +1,46 @@ +// +// Copyright (C) 2022-present Instructure, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + + +package com.instructure.dataseeding.model + +import com.google.gson.annotations.SerializedName + +/** + * Used to create conferences. + */ +data class ConferencesRequestApiModel( + @SerializedName("title") + val title: String = "", + @SerializedName("description") + val description: String? = null, + @SerializedName("conference_type") + val conferenceType: String = "", + @SerializedName("long_running") + val longRunning: Boolean = false, + @SerializedName("duration") + val duration: Int = 60, + @SerializedName("users") + val userIds: List? = null +) + +/** + * Wrapper class above ConferencesRequestApiModel because it is wrapped within a 'web_conference' object in the request. + */ +data class WebConferenceWrapper( + @SerializedName("web_conference") + val webConference: ConferencesRequestApiModel +) \ No newline at end of file diff --git a/automation/dataseedingapi/src/main/kotlin/com/instructure/dataseeding/model/ConferencesResponseApiModel.kt b/automation/dataseedingapi/src/main/kotlin/com/instructure/dataseeding/model/ConferencesResponseApiModel.kt new file mode 100644 index 0000000000..945f87bdd5 --- /dev/null +++ b/automation/dataseedingapi/src/main/kotlin/com/instructure/dataseeding/model/ConferencesResponseApiModel.kt @@ -0,0 +1,38 @@ +// +// Copyright (C) 2022-present Instructure, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + + +package com.instructure.dataseeding.model + +import com.google.gson.annotations.SerializedName + +/** + * Used to get conferences. + */ +data class ConferencesResponseApiModel( + @SerializedName("id") + val id: Long, + @SerializedName("description") + val description: String = "", + @SerializedName("conference_type") + val conferenceType: String = "", + @SerializedName("long_running") + val longRunning: Int? = null, + @SerializedName("duration") + val duration: Int, + @SerializedName("user_ids") + val userIds: List = listOf() +) \ No newline at end of file diff --git a/automation/dataseedingapi/src/main/kotlin/com/instructure/dataseeding/util/RestRetryInterceptor.kt b/automation/dataseedingapi/src/main/kotlin/com/instructure/dataseeding/util/RestRetryInterceptor.kt index a68d6b1ae9..c4d65eb0d7 100644 --- a/automation/dataseedingapi/src/main/kotlin/com/instructure/dataseeding/util/RestRetryInterceptor.kt +++ b/automation/dataseedingapi/src/main/kotlin/com/instructure/dataseeding/util/RestRetryInterceptor.kt @@ -33,6 +33,7 @@ object RestRetryInterceptor : Interceptor { var response = chain.proceed(request) while (response.failed && attempt <= MAX_RETRIES) { RetryBackoff.wait(attempt) + response.close() response = chain.proceed(request) attempt += 1 } diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/StubMultiAPILevel.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/StubMultiAPILevel.kt new file mode 100644 index 0000000000..90080e9df1 --- /dev/null +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/StubMultiAPILevel.kt @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2022 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.instructure.canvas.espresso + +// Apply on a test method which is failing on Firebase Test Lab (FTL) on some API levels. Write the failing API Levels into the parameter. +@Target(AnnotationTarget.FUNCTION) +@Retention(AnnotationRetention.RUNTIME) +annotation class StubMultiAPILevel(val failedApiLevels: String = "") \ No newline at end of file diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/MockCanvas.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/MockCanvas.kt index 177258bc1a..ba6744f500 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/MockCanvas.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/MockCanvas.kt @@ -19,7 +19,6 @@ package com.instructure.canvas.espresso.mockCanvas import android.util.Log -import com.github.javafaker.Bool import com.github.javafaker.Faker import com.instructure.canvas.espresso.mockCanvas.utils.Randomizer import com.instructure.canvasapi2.apis.EnrollmentAPI @@ -1360,9 +1359,9 @@ fun MockCanvas.addDiscussionTopicToCourse( var topicHeader = prePopulatedTopicHeader if(topicHeader == null) { topicHeader = DiscussionTopicHeader( - title = topicTitle, - discussionType = "side_comment", - message = topicDescription + title = topicTitle, + discussionType = "side_comment", + message = topicDescription ) } @@ -1374,7 +1373,7 @@ fun MockCanvas.addDiscussionTopicToCourse( topicHeader.id = newItemId() topicHeader.postedDate = Calendar.getInstance().time if(attachment != null) { - topicHeader.attachments = mutableListOf(attachment) + topicHeader.attachments = mutableListOf(attachment) } topicHeader.announcement = isAnnouncement topicHeader.sections = sections @@ -1383,7 +1382,7 @@ fun MockCanvas.addDiscussionTopicToCourse( var topicHeaderList = if(groupId != null) groupDiscussionTopicHeaders[groupId] else courseDiscussionTopicHeaders[course.id] if(topicHeaderList == null) { - topicHeaderList = mutableListOf() + topicHeaderList = mutableListOf() if(groupId != null) { groupDiscussionTopicHeaders[groupId] = topicHeaderList } @@ -1395,9 +1394,9 @@ fun MockCanvas.addDiscussionTopicToCourse( topicHeaderList.add(topicHeader) val topic = DiscussionTopic( - participants = mutableListOf( - DiscussionParticipant(id = user.id, displayName = user.name) - ) + participants = mutableListOf( + DiscussionParticipant(id = user.id, displayName = user.name) + ) ) discussionTopics[topicHeader.id] = topic @@ -1511,6 +1510,11 @@ fun MockCanvas.addItemToModule( itemTitle = item itemUrl = item } + is LTITool -> { + itemType = ModuleItem.Type.ExternalTool + itemTitle = item.name + itemUrl = item.url + } else -> { throw Exception("Unknown item type: ${item::class.java.simpleName}") } @@ -1771,6 +1775,8 @@ fun MockCanvas.addGroupToCourse( isFavorite = isFavorite ) + result.permissions = CanvasContextPermission(canCreateAnnouncement = true) + groups[result.id] = result return result diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/endpoints/ApiEndpoint.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/endpoints/ApiEndpoint.kt index 349196d55c..62d81e572c 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/endpoints/ApiEndpoint.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/endpoints/ApiEndpoint.kt @@ -17,7 +17,9 @@ package com.instructure.canvas.espresso.mockCanvas.endpoints import android.util.Log +import com.google.gson.Gson import com.instructure.canvas.espresso.mockCanvas.Endpoint +import com.instructure.canvas.espresso.mockCanvas.addDiscussionTopicToCourse import com.instructure.canvas.espresso.mockCanvas.endpoint import com.instructure.canvas.espresso.mockCanvas.utils.* import com.instructure.canvasapi2.models.* @@ -218,6 +220,30 @@ object GroupsEndpoint : Endpoint ( request.unauthorizedResponse() } } + POST { + val jsonObject = grabJsonFromMultiPartBody(request.body!!) + var newHeader = Gson().fromJson(jsonObject, DiscussionTopicHeader::class.java) + var group = data.groups.values.find { it.id == pathVars.groupId } + var course = data.courses.values.find {it.id == group!!.courseId} + var user = request.user!! + + newHeader = data.addDiscussionTopicToCourse( + course = course!!, + groupId = group!!.id, + user = user, + prePopulatedTopicHeader = newHeader, + topicTitle = newHeader.title!!, + topicDescription = newHeader.message!!, + allowRating = data.discussionRatingsEnabled, + allowReplies = data.discussionRepliesEnabled, + allowAttachments = data.discussionAttachmentsEnabled, + isAnnouncement = newHeader.announcement + ) + Log.d("<--", "new discussion topic request body: $jsonObject") + Log.d("<--", "new header: $newHeader") + + request.successResponse(newHeader) + } } ), diff --git a/automation/espresso/src/main/kotlin/com/instructure/espresso/CustomViewAssertions.kt b/automation/espresso/src/main/kotlin/com/instructure/espresso/CustomViewAssertions.kt index d063d0c46f..a6248e7e21 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/espresso/CustomViewAssertions.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/espresso/CustomViewAssertions.kt @@ -16,14 +16,18 @@ */ package com.instructure.espresso +import android.graphics.Color +import android.view.View +import android.widget.TextView +import androidx.annotation.IdRes +import androidx.recyclerview.widget.RecyclerView import androidx.test.espresso.NoMatchingViewException import androidx.test.espresso.ViewAssertion import androidx.test.espresso.matcher.ViewMatchers import androidx.viewpager.widget.ViewPager -import androidx.recyclerview.widget.RecyclerView -import android.view.View +import com.google.android.material.bottomnavigation.BottomNavigationView import org.hamcrest.Matchers -import java.lang.ClassCastException +import org.junit.Assert.assertEquals class RecyclerViewItemCountAssertion(private val expectedCount: Int) : ViewAssertion { override fun check(view: View, noViewFoundException: NoMatchingViewException?) { @@ -51,3 +55,22 @@ class ViewPagerItemCountAssertion(private val expectedCount: Int) : ViewAssertio ViewMatchers.assertThat(count, Matchers.`is`(expectedCount)) } } + +class TextViewColorAssertion(private val colorHexCode: String) : ViewAssertion { + override fun check(view: View, noViewFoundException: NoMatchingViewException?) { + noViewFoundException?.let { throw it } + val item = (view as? TextView) + ?: throw ClassCastException("View of type ${view.javaClass.simpleName} must be a TextView") + assertEquals(item.currentTextColor, Color.parseColor(colorHexCode)) + } +} + +class NotificationBadgeAssertion(@IdRes private val menuItemId: Int, private val expectedCount: Int) : ViewAssertion { + override fun check(view: View, noViewFoundException: NoMatchingViewException?) { + noViewFoundException?.let { throw it } + val bottomNavigationView = (view as? BottomNavigationView) + ?: throw ClassCastException("View of type ${view.javaClass.simpleName} must be a BottomNavigationView") + val badgeCount = bottomNavigationView.getBadge(menuItemId)?.number ?: -1 + assertEquals(badgeCount, expectedCount) + } +} diff --git a/automation/espresso/src/main/kotlin/com/instructure/espresso/ScreenshotTestRule.kt b/automation/espresso/src/main/kotlin/com/instructure/espresso/ScreenshotTestRule.kt index d89c46dc97..aeeb44a087 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/espresso/ScreenshotTestRule.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/espresso/ScreenshotTestRule.kt @@ -18,17 +18,15 @@ package com.instructure.espresso -import androidx.test.platform.app.InstrumentationRegistry import androidx.test.espresso.Espresso import androidx.test.espresso.base.DefaultFailureHandler +import androidx.test.platform.app.InstrumentationRegistry import com.instructure.espresso.matchers.WaitForCheckMatcher import com.instructure.espresso.matchers.WaitForViewMatcher - import org.junit.rules.TestRule import org.junit.runner.Description import org.junit.runners.model.Statement - -import java.util.concurrent.atomic.AtomicBoolean +import java.util.concurrent.atomic.* class ScreenshotTestRule : TestRule { diff --git a/buildSrc/src/main/java/GlobalDependencies.kt b/buildSrc/src/main/java/GlobalDependencies.kt index 1116329649..830779cea2 100644 --- a/buildSrc/src/main/java/GlobalDependencies.kt +++ b/buildSrc/src/main/java/GlobalDependencies.kt @@ -2,9 +2,9 @@ object Versions { /* SDK Versions */ - const val COMPILE_SDK = 30 + const val COMPILE_SDK = 31 const val MIN_SDK = 26 - const val TARGET_SDK = 30 + const val TARGET_SDK = 31 /* Build/tooling */ const val ANDROID_GRADLE_TOOLS = "3.5.1" @@ -30,15 +30,14 @@ object Versions { /* Others */ const val APOLLO = "2.5.9" const val CRASHLYTICS = "17.2.1" - const val FIREBASE_ANALYTICS = "17.4.1" - const val PSPDFKIT = "8.1.0" + const val PSPDFKIT = "8.3.0" const val PHOTO_VIEW = "2.3.0" const val MOBIUS = "1.2.1" const val SQLDELIGHT = "1.4.3" const val HILT = "2.38.1" const val LIFECYCLE = "2.3.1" const val FRAGMENT = "1.3.6" - const val WORK_MANAGER = "2.6.0" + const val WORK_MANAGER = "2.7.1" const val GLIDE_VERSION = "4.12.0" const val RETROFIT = "2.9.0" const val OKHTTP = "4.9.1" @@ -78,7 +77,6 @@ object Libs { /* Firebase */ const val FIREBASE_BOM = "com.google.firebase:firebase-bom:29.3.0" - const val FIREBASE_ANALYTICS = "com.google.firebase:firebase-analytics" const val FIREBASE_CRASHLYTICS = "com.google.firebase:firebase-crashlytics" const val FIREBASE_MESSAGING = "com.google.firebase:firebase-messaging" const val FIREBASE_CORE = "com.google.firebase:firebase-core" @@ -86,7 +84,6 @@ object Libs { const val FIREBASE_CRASHLYTICS_NDK = "com.google.firebase:firebase-crashlytics-ndk" /* Play Services */ - const val PLAY_SERVICES_ANALYTICS = "com.google.android.gms:play-services-analytics:18.0.1" const val PLAY_CORE = "com.google.android.play:core:${Versions.PLAY_CORE}" const val PLAY_CORE_KTX = "com.google.android.play:core-ktx:${Versions.PLAY_CORE_KTX}" const val FLEXBOX_LAYOUT = "com.google.android.flexbox:flexbox:3.0.0" @@ -154,7 +151,7 @@ object Libs { const val PAPERDB = "io.github.pilgr:paperdb:2.7.1" const val KEYBOARD_VISIBILITY_LISTENER = "net.yslibrary.keyboardvisibilityevent:keyboardvisibilityevent:2.2.1" const val APACHE_COMMONS_TEXT = "org.apache.commons:commons-text:1.6" - const val WONDERKILN_CAMERA_KIT = "com.github.CameraKit:camerakit-android:v0.13.4" + const val CAMERA_VIEW = "com.otaliastudios:cameraview:2.7.2" } object Plugins { diff --git a/libs/annotations/src/main/java/com/instructure/annotations/PdfSubmissionView.kt b/libs/annotations/src/main/java/com/instructure/annotations/PdfSubmissionView.kt index 600246d2af..742a899645 100644 --- a/libs/annotations/src/main/java/com/instructure/annotations/PdfSubmissionView.kt +++ b/libs/annotations/src/main/java/com/instructure/annotations/PdfSubmissionView.kt @@ -39,7 +39,11 @@ import com.instructure.canvasapi2.models.ApiValues import com.instructure.canvasapi2.models.DocSession import com.instructure.canvasapi2.models.canvadocs.CanvaDocAnnotation import com.instructure.canvasapi2.models.canvadocs.CanvaDocAnnotationResponse -import com.instructure.canvasapi2.utils.* +import com.instructure.canvasapi2.utils.APIHelper +import com.instructure.canvasapi2.utils.ApiPrefs +import com.instructure.canvasapi2.utils.extractCanvaDocsDomain +import com.instructure.canvasapi2.utils.extractSessionId +import com.instructure.canvasapi2.utils.isValid import com.instructure.canvasapi2.utils.weave.StatusCallbackError import com.instructure.canvasapi2.utils.weave.awaitApi import com.instructure.canvasapi2.utils.weave.catch @@ -84,7 +88,7 @@ import java.io.File import java.util.* @SuppressLint("ViewConstructor") -abstract class PdfSubmissionView(context: Context) : FrameLayout(context), AnnotationManager.OnAnnotationCreationModeChangeListener, AnnotationManager.OnAnnotationEditingModeChangeListener { +abstract class PdfSubmissionView(context: Context, private val studentAnnotationView: Boolean = false) : FrameLayout(context), AnnotationManager.OnAnnotationCreationModeChangeListener, AnnotationManager.OnAnnotationEditingModeChangeListener { protected lateinit var docSession: DocSession protected lateinit var apiValues: ApiValues @@ -298,6 +302,10 @@ abstract class PdfSubmissionView(context: Context) : FrameLayout(context), Annot protected fun openComments() { // Get current annotation in both forms + if (pdfFragment?.selectedAnnotations?.isNullOrEmpty() == true) { + toast(R.string.noAnnotationSelected) + return + } val currentPdfAnnotation = pdfFragment?.selectedAnnotations?.get(0) val currentAnnotation = currentPdfAnnotation?.convertPDFAnnotationToCanvaDoc(docSession.documentId) // Assuming neither is null, continue @@ -461,14 +469,15 @@ abstract class PdfSubmissionView(context: Context) : FrameLayout(context), Annot } private fun handlePageRotation(pdfDocument: PdfDocument, rotationMap: HashMap) { + // Removing the listener prevents an infinite loop with onDocumentLoaded, which is triggered + // by the calls to setRotationOffset() + pdfFragment?.removeDocumentListener(documentListener) + rotationMap.forEach { pageRotation -> pageRotation.key.toIntOrNull()?.let { pageIndex -> pdfDocument.setRotationOffset(calculateRotationOffset(pdfDocument.getPageRotation(pageIndex), pageRotation.value), pageIndex) } } - // Removing the listener prevents an infinite loop with onDocumentLoaded, which is triggered - // by the calls to setRotationOffset() - pdfFragment?.removeDocumentListener(documentListener) } @Suppress("EXPERIMENTAL_FEATURE_WARNING") @@ -595,7 +604,7 @@ abstract class PdfSubmissionView(context: Context) : FrameLayout(context), Annot setIsCurrentlyAnnotating(true) } - if (annotation.type != AnnotationType.FREETEXT && annotation.name.isValid()) { + if (annotation.type != AnnotationType.FREETEXT && annotation.name.isValid() && (!studentAnnotationView || hasComments(annotation))) { // if the annotation is an existing annotation (has an ID) and is NOT freetext // we want to display the button to view/make comments commentsButton.setVisible() @@ -605,6 +614,13 @@ abstract class PdfSubmissionView(context: Context) : FrameLayout(context), Annot } } + private fun hasComments(annotation: Annotation): Boolean { + val currentAnnotation = annotation.convertPDFAnnotationToCanvaDoc(docSession.documentId) + return currentAnnotation != null + && commentRepliesHashMap[currentAnnotation.annotationId] != null + && commentRepliesHashMap[currentAnnotation.annotationId]?.isNotEmpty() == true + } + val mAnnotationDeselectedListener = AnnotationManager.OnAnnotationDeselectedListener { _, _ -> commentsButton.setGone() } diff --git a/libs/annotations/src/main/res/values-ar/strings.xml b/libs/annotations/src/main/res/values-ar/strings.xml index 3e25e5b89e..27ab666226 100644 --- a/libs/annotations/src/main/res/values-ar/strings.xml +++ b/libs/annotations/src/main/res/values-ar/strings.xml @@ -37,5 +37,6 @@ جارٍ الإرسال لون الملاحظة تمت إزالة %1$s بواسطة %2$s + diff --git a/libs/annotations/src/main/res/values-b+da+instk12/strings.xml b/libs/annotations/src/main/res/values-b+da+instk12/strings.xml index f47ffbc676..0f0e98ee0e 100644 --- a/libs/annotations/src/main/res/values-b+da+instk12/strings.xml +++ b/libs/annotations/src/main/res/values-b+da+instk12/strings.xml @@ -37,5 +37,6 @@ Sender Bemærkningens farve Fjernet %1$s af %2$s + diff --git a/libs/annotations/src/main/res/values-b+en+AU+unimelb/strings.xml b/libs/annotations/src/main/res/values-b+en+AU+unimelb/strings.xml index 1f0bdefc5e..d220a46a5d 100644 --- a/libs/annotations/src/main/res/values-b+en+AU+unimelb/strings.xml +++ b/libs/annotations/src/main/res/values-b+en+AU+unimelb/strings.xml @@ -38,5 +38,6 @@ Note Colour Removed %1$s by %2$s + diff --git a/libs/annotations/src/main/res/values-b+en+GB+instukhe/strings.xml b/libs/annotations/src/main/res/values-b+en+GB+instukhe/strings.xml index d8596c0679..a163fff555 100644 --- a/libs/annotations/src/main/res/values-b+en+GB+instukhe/strings.xml +++ b/libs/annotations/src/main/res/values-b+en+GB+instukhe/strings.xml @@ -36,5 +36,6 @@ Sending Note colour Removed %1$s by %2$s + diff --git a/libs/annotations/src/main/res/values-b+nb+instk12/strings.xml b/libs/annotations/src/main/res/values-b+nb+instk12/strings.xml index 75023d68ee..a7a39aced7 100644 --- a/libs/annotations/src/main/res/values-b+nb+instk12/strings.xml +++ b/libs/annotations/src/main/res/values-b+nb+instk12/strings.xml @@ -37,5 +37,6 @@ Sender Notatfarge Fjernet %1$s av %2$s + diff --git a/libs/annotations/src/main/res/values-b+sv+instk12/strings.xml b/libs/annotations/src/main/res/values-b+sv+instk12/strings.xml index d104c77450..43adaf6db0 100644 --- a/libs/annotations/src/main/res/values-b+sv+instk12/strings.xml +++ b/libs/annotations/src/main/res/values-b+sv+instk12/strings.xml @@ -36,5 +36,6 @@ Skickar Anteckningsfärg Borttagen %1$s av %2$s + diff --git a/libs/annotations/src/main/res/values-zh-rHK/strings.xml b/libs/annotations/src/main/res/values-b+zh+HK/strings.xml similarity index 97% rename from libs/annotations/src/main/res/values-zh-rHK/strings.xml rename to libs/annotations/src/main/res/values-b+zh+HK/strings.xml index 9b8c7db567..afcd217149 100644 --- a/libs/annotations/src/main/res/values-zh-rHK/strings.xml +++ b/libs/annotations/src/main/res/values-b+zh+HK/strings.xml @@ -38,5 +38,6 @@ 注釋顏色 %1$s 由 %2$s移除 + diff --git a/libs/annotations/src/main/res/values-b+zh+Hans/strings.xml b/libs/annotations/src/main/res/values-b+zh+Hans/strings.xml index 0e33334040..a10a031b4c 100644 --- a/libs/annotations/src/main/res/values-b+zh+Hans/strings.xml +++ b/libs/annotations/src/main/res/values-b+zh+Hans/strings.xml @@ -38,5 +38,6 @@ 注释颜色 移除%1$s者 %2$s + diff --git a/libs/annotations/src/main/res/values-b+zh+Hant/strings.xml b/libs/annotations/src/main/res/values-b+zh+Hant/strings.xml index 9b8c7db567..afcd217149 100644 --- a/libs/annotations/src/main/res/values-b+zh+Hant/strings.xml +++ b/libs/annotations/src/main/res/values-b+zh+Hant/strings.xml @@ -38,5 +38,6 @@ 注釋顏色 %1$s 由 %2$s移除 + diff --git a/libs/annotations/src/main/res/values-ca/strings.xml b/libs/annotations/src/main/res/values-ca/strings.xml index 977537b1e5..057c242d06 100644 --- a/libs/annotations/src/main/res/values-ca/strings.xml +++ b/libs/annotations/src/main/res/values-ca/strings.xml @@ -37,5 +37,6 @@ S\'està enviant Color de la nota Eliminat el %1$s per %2$s + diff --git a/libs/annotations/src/main/res/values-cy/strings.xml b/libs/annotations/src/main/res/values-cy/strings.xml index f4861d16bf..4d9d848ff9 100644 --- a/libs/annotations/src/main/res/values-cy/strings.xml +++ b/libs/annotations/src/main/res/values-cy/strings.xml @@ -37,5 +37,6 @@ Wrthi’n anfon Lliw y Nodyn %2$s wedi tynnu %1$s + diff --git a/libs/annotations/src/main/res/values-da/strings.xml b/libs/annotations/src/main/res/values-da/strings.xml index a22a87e323..0ce90ee7a3 100644 --- a/libs/annotations/src/main/res/values-da/strings.xml +++ b/libs/annotations/src/main/res/values-da/strings.xml @@ -38,5 +38,6 @@ Bemærkningens farve Fjernet %1$s af %2$s + diff --git a/libs/annotations/src/main/res/values-de/strings.xml b/libs/annotations/src/main/res/values-de/strings.xml index ddf968d659..4a3fedae2c 100644 --- a/libs/annotations/src/main/res/values-de/strings.xml +++ b/libs/annotations/src/main/res/values-de/strings.xml @@ -38,5 +38,6 @@ Hinweisfarbe %1$s entfernt von %2$s + diff --git a/libs/annotations/src/main/res/values-en-rAU/strings.xml b/libs/annotations/src/main/res/values-en-rAU/strings.xml index 1f0bdefc5e..d220a46a5d 100644 --- a/libs/annotations/src/main/res/values-en-rAU/strings.xml +++ b/libs/annotations/src/main/res/values-en-rAU/strings.xml @@ -38,5 +38,6 @@ Note Colour Removed %1$s by %2$s + diff --git a/libs/annotations/src/main/res/values-en-rCA/strings.xml b/libs/annotations/src/main/res/values-en-rCA/strings.xml index 44e3188fec..9fc2b43536 100644 --- a/libs/annotations/src/main/res/values-en-rCA/strings.xml +++ b/libs/annotations/src/main/res/values-en-rCA/strings.xml @@ -36,5 +36,6 @@ Sending Note Color Removed %1$s by %2$s + diff --git a/libs/annotations/src/main/res/values-en-rCY/strings.xml b/libs/annotations/src/main/res/values-en-rCY/strings.xml index d8596c0679..a163fff555 100644 --- a/libs/annotations/src/main/res/values-en-rCY/strings.xml +++ b/libs/annotations/src/main/res/values-en-rCY/strings.xml @@ -36,5 +36,6 @@ Sending Note colour Removed %1$s by %2$s + diff --git a/libs/annotations/src/main/res/values-en-rGB/strings.xml b/libs/annotations/src/main/res/values-en-rGB/strings.xml index bdcd9eca2e..2900a79513 100644 --- a/libs/annotations/src/main/res/values-en-rGB/strings.xml +++ b/libs/annotations/src/main/res/values-en-rGB/strings.xml @@ -38,5 +38,6 @@ Note colour Removed %1$s by %2$s + diff --git a/libs/annotations/src/main/res/values-es-rES/strings.xml b/libs/annotations/src/main/res/values-es-rES/strings.xml index f4025e3bb2..9389dd259f 100644 --- a/libs/annotations/src/main/res/values-es-rES/strings.xml +++ b/libs/annotations/src/main/res/values-es-rES/strings.xml @@ -37,5 +37,6 @@ Enviando Color de la nota Eliminado %1$s por %2$s + diff --git a/libs/annotations/src/main/res/values-es/strings.xml b/libs/annotations/src/main/res/values-es/strings.xml index 632af30961..1f3ae8cc72 100644 --- a/libs/annotations/src/main/res/values-es/strings.xml +++ b/libs/annotations/src/main/res/values-es/strings.xml @@ -37,5 +37,6 @@ Enviando Color de la nota Eliminado el %1$s por %2$s + diff --git a/libs/annotations/src/main/res/values-fi/strings.xml b/libs/annotations/src/main/res/values-fi/strings.xml index f4b993cda0..1e86351397 100644 --- a/libs/annotations/src/main/res/values-fi/strings.xml +++ b/libs/annotations/src/main/res/values-fi/strings.xml @@ -36,5 +36,6 @@ Lähetetään Huomautuksen väri Kohteen %1$s poistaja %2$s + diff --git a/libs/annotations/src/main/res/values-fr-rCA/strings.xml b/libs/annotations/src/main/res/values-fr-rCA/strings.xml index 009090f78a..4fe13175fa 100644 --- a/libs/annotations/src/main/res/values-fr-rCA/strings.xml +++ b/libs/annotations/src/main/res/values-fr-rCA/strings.xml @@ -38,5 +38,6 @@ Couleur de note Retiré %1$s par %2$s + diff --git a/libs/annotations/src/main/res/values-fr/strings.xml b/libs/annotations/src/main/res/values-fr/strings.xml index 32f45da7da..f751bb5b31 100644 --- a/libs/annotations/src/main/res/values-fr/strings.xml +++ b/libs/annotations/src/main/res/values-fr/strings.xml @@ -38,5 +38,6 @@ Couleur de la note Supprimé %1$s par %2$s + diff --git a/libs/annotations/src/main/res/values-ht/strings.xml b/libs/annotations/src/main/res/values-ht/strings.xml index d60711fd1b..4b5c8ff167 100644 --- a/libs/annotations/src/main/res/values-ht/strings.xml +++ b/libs/annotations/src/main/res/values-ht/strings.xml @@ -38,5 +38,6 @@ Note Koulè Elimine %1$s pa %2$s + diff --git a/libs/annotations/src/main/res/values-is/strings.xml b/libs/annotations/src/main/res/values-is/strings.xml index b5531e10de..cbd9350234 100644 --- a/libs/annotations/src/main/res/values-is/strings.xml +++ b/libs/annotations/src/main/res/values-is/strings.xml @@ -37,5 +37,6 @@ Sendi Litur glósu Fjarlægt %1$s af %2$s + diff --git a/libs/annotations/src/main/res/values-it/strings.xml b/libs/annotations/src/main/res/values-it/strings.xml index 6a1c5f216c..5f2fa6cab4 100644 --- a/libs/annotations/src/main/res/values-it/strings.xml +++ b/libs/annotations/src/main/res/values-it/strings.xml @@ -36,5 +36,6 @@ Invio in corso Colore nota Rimosso il %1$s da %2$s + diff --git a/libs/annotations/src/main/res/values-ja/strings.xml b/libs/annotations/src/main/res/values-ja/strings.xml index ce4c759081..f3731a2b6f 100644 --- a/libs/annotations/src/main/res/values-ja/strings.xml +++ b/libs/annotations/src/main/res/values-ja/strings.xml @@ -38,5 +38,6 @@ ノートの色 %1$sにより除去済%2$s + diff --git a/libs/annotations/src/main/res/values-mi/strings.xml b/libs/annotations/src/main/res/values-mi/strings.xml index 1567a45db4..05f1b504ef 100644 --- a/libs/annotations/src/main/res/values-mi/strings.xml +++ b/libs/annotations/src/main/res/values-mi/strings.xml @@ -37,5 +37,6 @@ E tuku ana Tuhi Tae Kua tangohia %1$s e %2$s + diff --git a/libs/annotations/src/main/res/values-nb/strings.xml b/libs/annotations/src/main/res/values-nb/strings.xml index 75023d68ee..a7a39aced7 100644 --- a/libs/annotations/src/main/res/values-nb/strings.xml +++ b/libs/annotations/src/main/res/values-nb/strings.xml @@ -37,5 +37,6 @@ Sender Notatfarge Fjernet %1$s av %2$s + diff --git a/libs/annotations/src/main/res/values-nl/strings.xml b/libs/annotations/src/main/res/values-nl/strings.xml index 5ed72b4e5d..6554f4e836 100644 --- a/libs/annotations/src/main/res/values-nl/strings.xml +++ b/libs/annotations/src/main/res/values-nl/strings.xml @@ -38,5 +38,6 @@ Kleur van opmerking %1$s verwijderd door %2$s + diff --git a/libs/annotations/src/main/res/values-pl/strings.xml b/libs/annotations/src/main/res/values-pl/strings.xml index b2bbd855d6..bc857a8d29 100644 --- a/libs/annotations/src/main/res/values-pl/strings.xml +++ b/libs/annotations/src/main/res/values-pl/strings.xml @@ -38,5 +38,6 @@ Kolor notatki Usunięte %1$s przez %2$s + diff --git a/libs/annotations/src/main/res/values-pt-rBR/strings.xml b/libs/annotations/src/main/res/values-pt-rBR/strings.xml index a277feb6a6..a1b7319c8c 100644 --- a/libs/annotations/src/main/res/values-pt-rBR/strings.xml +++ b/libs/annotations/src/main/res/values-pt-rBR/strings.xml @@ -36,5 +36,6 @@ Enviando Cor da observação Removido %1$s por %2$s + diff --git a/libs/annotations/src/main/res/values-pt-rPT/strings.xml b/libs/annotations/src/main/res/values-pt-rPT/strings.xml index 03fbe87b99..6a5f47db1f 100644 --- a/libs/annotations/src/main/res/values-pt-rPT/strings.xml +++ b/libs/annotations/src/main/res/values-pt-rPT/strings.xml @@ -38,5 +38,6 @@ Observar cor Removido %1$s por %2$s + diff --git a/libs/annotations/src/main/res/values-ru/strings.xml b/libs/annotations/src/main/res/values-ru/strings.xml index 98ead02248..9fc6aac87e 100644 --- a/libs/annotations/src/main/res/values-ru/strings.xml +++ b/libs/annotations/src/main/res/values-ru/strings.xml @@ -38,5 +38,6 @@ Отметить цвет %1$s удален %2$s + diff --git a/libs/annotations/src/main/res/values-sl/strings.xml b/libs/annotations/src/main/res/values-sl/strings.xml index 46b285d354..68f4f28674 100644 --- a/libs/annotations/src/main/res/values-sl/strings.xml +++ b/libs/annotations/src/main/res/values-sl/strings.xml @@ -37,5 +37,6 @@ Pošiljanje Barva opombe Odstranjeno %1$s, odstranil %2$s + diff --git a/libs/annotations/src/main/res/values-sv/strings.xml b/libs/annotations/src/main/res/values-sv/strings.xml index d104c77450..43adaf6db0 100644 --- a/libs/annotations/src/main/res/values-sv/strings.xml +++ b/libs/annotations/src/main/res/values-sv/strings.xml @@ -36,5 +36,6 @@ Skickar Anteckningsfärg Borttagen %1$s av %2$s + diff --git a/libs/annotations/src/main/res/values-th/strings.xml b/libs/annotations/src/main/res/values-th/strings.xml index d26603b218..816c1d248d 100644 --- a/libs/annotations/src/main/res/values-th/strings.xml +++ b/libs/annotations/src/main/res/values-th/strings.xml @@ -37,5 +37,6 @@ กำลังส่ง สีหมายเหตุ ลบ %1$s โดย %2$s + diff --git a/libs/annotations/src/main/res/values-vi/strings.xml b/libs/annotations/src/main/res/values-vi/strings.xml index 4daa087ba8..b9f3a5cd43 100644 --- a/libs/annotations/src/main/res/values-vi/strings.xml +++ b/libs/annotations/src/main/res/values-vi/strings.xml @@ -37,5 +37,6 @@ Đang Gửi Màu Ghi Chú Đã bị gỡ %1$s bởi %2$s + diff --git a/libs/annotations/src/main/res/values-zh/strings.xml b/libs/annotations/src/main/res/values-zh/strings.xml index 0e33334040..a10a031b4c 100644 --- a/libs/annotations/src/main/res/values-zh/strings.xml +++ b/libs/annotations/src/main/res/values-zh/strings.xml @@ -38,5 +38,6 @@ 注释颜色 移除%1$s者 %2$s + diff --git a/libs/annotations/src/main/res/values/strings.xml b/libs/annotations/src/main/res/values/strings.xml index 44e3188fec..9fc2b43536 100644 --- a/libs/annotations/src/main/res/values/strings.xml +++ b/libs/annotations/src/main/res/values/strings.xml @@ -36,5 +36,6 @@ Sending Note Color Removed %1$s by %2$s + diff --git a/libs/canvas-api-2/build.gradle b/libs/canvas-api-2/build.gradle index 04ea5f0af8..a3715e4744 100644 --- a/libs/canvas-api-2/build.gradle +++ b/libs/canvas-api-2/build.gradle @@ -135,7 +135,9 @@ dependencies { api Libs.PAPERDB // Firebase - implementation platform(Libs.FIREBASE_BOM) + implementation platform(Libs.FIREBASE_BOM) { + exclude group: 'com.google.firebase', module: 'firebase-analytics' + } implementation Libs.FIREBASE_CORE implementation Libs.FIREBASE_CONFIG diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/AppManager.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/AppManager.kt index 94f760ee81..dd167bb8ea 100644 --- a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/AppManager.kt +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/AppManager.kt @@ -50,8 +50,6 @@ abstract class AppManager : Application() { super.onCreate() AndroidThreeTen.init(this) Paper.init(this) - // Permissions missing lint suppressed - we have the permissions in the app manifests - Analytics.firebase = FirebaseAnalytics.getInstance(this) EventBus.getDefault().register(this) logTokenAnalytics() } diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/CourseAPI.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/CourseAPI.kt index 848cf32d3a..a04a32a708 100644 --- a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/CourseAPI.kt +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/CourseAPI.kt @@ -38,7 +38,7 @@ object CourseAPI { @get:GET("dashboard/dashboard_cards") val dashboardCourses: Call> - @get:GET("courses?include[]=term&include[]=total_scores&include[]=license&include[]=is_public&include[]=needs_grading_count&include[]=permissions&include[]=favorites&include[]=current_grading_period_scores&include[]=course_image&include[]=sections&state[]=completed&state[]=available") + @get:GET("courses?include[]=term&include[]=total_scores&include[]=license&include[]=is_public&include[]=needs_grading_count&include[]=permissions&include[]=favorites&include[]=current_grading_period_scores&include[]=course_image&include[]=banner_image&include[]=sections&state[]=completed&state[]=available") val firstPageCourses: Call> @get:GET("courses?include[]=term&include[]=total_scores&include[]=license&include[]=is_public&include[]=needs_grading_count&include[]=permissions&include[]=favorites&include[]=current_grading_period_scores&include[]=course_image&include[]=sections&state[]=current_and_concluded") diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/DiscussionAPI.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/DiscussionAPI.kt index 20f669f202..23875fb693 100644 --- a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/DiscussionAPI.kt +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/DiscussionAPI.kt @@ -27,7 +27,6 @@ import com.instructure.canvasapi2.models.DiscussionTopicHeader import com.instructure.canvasapi2.models.postmodels.DiscussionEntryPostBody import com.instructure.canvasapi2.models.postmodels.DiscussionTopicPostBody import com.instructure.canvasapi2.utils.APIHelper -import okhttp3.MediaType import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.MultipartBody import okhttp3.RequestBody @@ -88,6 +87,9 @@ object DiscussionAPI { @POST("{contextType}/{contextId}/discussion_topics/{topicId}/entries/{entryId}/rating") fun rateDiscussionEntry(@Path("contextType") contextType: String, @Path("contextId") contextId: Long, @Path("topicId") topicId: Long, @Path("entryId") entryId: Long, @Query("rating") rating: Int): Call + @PUT("{contextType}/{contextId}/discussion_topics/{topicId}/read") + fun markDiscussionTopicRead(@Path("contextType") contextType: String, @Path("contextId") contextId: Long, @Path("topicId") topicId: Long): Call + @PUT("{contextType}/{contextId}/discussion_topics/{topicId}/entries/{entryId}/read") fun markDiscussionTopicEntryRead(@Path("contextType") contextType: String, @Path("contextId") contextId: Long, @Path("topicId") topicId: Long, @Path("entryId") entryId: Long): Call @@ -270,6 +272,11 @@ object DiscussionAPI { callback.addCall(adapter.build(DiscussionInterface::class.java, params).rateDiscussionEntry(contextType, canvasContext.id, topicId, entryId, rating)).enqueue(callback) } + fun markDiscussionTopicRead(adapter: RestBuilder, canvasContext: CanvasContext, topicId: Long, callback: StatusCallback, params: RestParams) { + val contextType = CanvasContext.getApiContext(canvasContext) + callback.addCall(adapter.build(DiscussionInterface::class.java, params).markDiscussionTopicRead(contextType, canvasContext.id, topicId)).enqueue(callback) + } + fun markDiscussionTopicEntryRead(adapter: RestBuilder, canvasContext: CanvasContext, topicId: Long, entryId: Long, callback: StatusCallback, params: RestParams) { val contextType = CanvasContext.getApiContext(canvasContext) callback.addCall(adapter.build(DiscussionInterface::class.java, params).markDiscussionTopicEntryRead(contextType, canvasContext.id, topicId, entryId)).enqueue(callback) diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/InboxApi.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/InboxApi.kt index 34eab7952f..1603303c17 100644 --- a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/InboxApi.kt +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/InboxApi.kt @@ -66,7 +66,7 @@ object InboxApi { @Field("bulk_message") isBulk: Int): Call> @GET("conversations/{conversationId}?include[]=participant_avatars") - fun getConversation(@Path("conversationId") conversationId: Long): Call + fun getConversation(@Path("conversationId") conversationId: Long, @Query("auto_mark_as_read") markAsRead: Boolean): Call @PUT("conversations/{conversationId}") fun updateConversation(@Path("conversationId") conversationId: Long, @Query("conversation[workflow_state]") workflowState: String, @Query("conversation[starred]") isStarred: Boolean?): Call @@ -90,8 +90,8 @@ object InboxApi { fun markConversationAsUnread(@Query("conversation_ids[]") conversationId: Long, @Query("event") conversationEvent: String): Call } - fun getConversation(adapter: RestBuilder, callback: StatusCallback, params: RestParams, conversationId: Long) { - callback.addCall(adapter.build(InboxInterface::class.java, params).getConversation(conversationId)).enqueue(callback) + fun getConversation(adapter: RestBuilder, callback: StatusCallback, params: RestParams, conversationId: Long, markAsRead: Boolean) { + callback.addCall(adapter.build(InboxInterface::class.java, params).getConversation(conversationId, markAsRead)).enqueue(callback) } fun getConversations(scope: Scope, adapter: RestBuilder, callback: StatusCallback>, params: RestParams) { @@ -145,7 +145,7 @@ object InboxApi { } @Throws(IOException::class) - fun getConversation(adapter: RestBuilder, params: RestParams, conversationId: Long): Response { - return adapter.build(InboxInterface::class.java, params).getConversation(conversationId).execute() + fun getConversation(adapter: RestBuilder, params: RestParams, conversationId: Long, markAsRead: Boolean): Response { + return adapter.build(InboxInterface::class.java, params).getConversation(conversationId, markAsRead).execute() } } diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/NotificationPreferencesAPI.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/NotificationPreferencesAPI.kt index b76bbc3be2..1033346199 100644 --- a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/NotificationPreferencesAPI.kt +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/NotificationPreferencesAPI.kt @@ -22,7 +22,7 @@ import com.instructure.canvasapi2.models.NotificationPreferenceResponse import retrofit2.Call import retrofit2.http.* -internal object NotificationPreferencesAPI { +object NotificationPreferencesAPI { interface NotificationPreferencesInterface { @GET("users/{userId}/communication_channels/{communicationChannelId}/notification_preferences") diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/SubmissionAPI.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/SubmissionAPI.kt index 6a682599db..fbc5594eec 100644 --- a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/SubmissionAPI.kt +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/SubmissionAPI.kt @@ -131,6 +131,12 @@ object SubmissionAPI { @GET("courses/{courseId}/assignments/{assignmentId}/submission_summary") fun getSubmissionSummary(@Path("courseId") courseId: Long, @Path("assignmentId") assignmentId: Long): Call + + @PUT("courses/{courseId}/assignments/{assignmentId}/submissions/self/read") + fun markSubmissionAsRead( + @Path("courseId") courseId: Long, + @Path("assignmentId") assignmentId: Long + ): Call } fun getSingleSubmission(courseId: Long, assignmentId: Long, studentId: Long, adapter: RestBuilder, callback: StatusCallback, params: RestParams) { @@ -212,6 +218,16 @@ object SubmissionAPI { ).enqueue(callback) } + fun markSubmissionAsRead( + adapter: RestBuilder, + params: RestParams, + courseId: Long, + assignmentId: Long, + callback: StatusCallback + ) { + callback.addCall(adapter.build(SubmissionInterface::class.java, params).markSubmissionAsRead(courseId, assignmentId)).enqueue(callback) + } + private fun generateRubricAssessmentQueryMap(rubricAssessment: Map): Map { val map = mutableMapOf() for ((criterionIdKey, ratingValue) in rubricAssessment) { diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/UnreadCountAPI.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/UnreadCountAPI.kt index 0976f05353..c7b54c8bff 100644 --- a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/UnreadCountAPI.kt +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/UnreadCountAPI.kt @@ -16,7 +16,7 @@ internal object UnreadCountAPI { @GET("conversations/unread_count") fun getUnreadConversationCount(): Call - @GET("users/self/activity_stream/summary") + @GET("users/self/activity_stream/summary?only_active_courses=true") fun getNotificationsCount(): Call> @GET("users/self/observer_alerts/unread_count") diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/di/ApiModule.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/di/ApiModule.kt index 43bb352b46..1c69c391ab 100644 --- a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/di/ApiModule.kt +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/di/ApiModule.kt @@ -1,6 +1,7 @@ package com.instructure.canvasapi2.di import com.instructure.canvasapi2.apis.HelpLinksAPI +import com.instructure.canvasapi2.apis.NotificationPreferencesAPI import com.instructure.canvasapi2.apis.PlannerAPI import com.instructure.canvasapi2.managers.* import com.instructure.canvasapi2.utils.ApiPrefs @@ -130,6 +131,6 @@ class ApiModule { @Provides fun provideNotificationPreferencesManager(): NotificationPreferencesManager { - return NotificationPreferencesManager + return NotificationPreferencesManager(NotificationPreferencesAPI) } } \ No newline at end of file diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/AssignmentManager.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/AssignmentManager.kt index e19b167cb9..55b5a42674 100644 --- a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/AssignmentManager.kt +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/AssignmentManager.kt @@ -219,4 +219,6 @@ object AssignmentManager { AssignmentAPI.getFirstPageAssignments(courseId, forceNetwork, adapter, depaginatedCallback) } + fun getAllAssignmentsAsync(courseId: Long, forceNetwork: Boolean) = apiAsync> { getAllAssignments(courseId, forceNetwork, it) } + } diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/DiscussionManager.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/DiscussionManager.kt index 65810e4509..24aee6a99c 100644 --- a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/DiscussionManager.kt +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/DiscussionManager.kt @@ -134,6 +134,16 @@ object DiscussionManager { DiscussionAPI.rateDiscussionEntry(adapter, canvasContext, topicId, entryId, rating, callback, params) } + fun markDiscussionTopicRead( + canvasContext: CanvasContext, + topicId: Long, + callback: StatusCallback + ) { + val adapter = RestBuilder(callback) + val params = RestParams() + DiscussionAPI.markDiscussionTopicRead(adapter, canvasContext, topicId, callback, params) + } + fun markDiscussionTopicEntryRead( canvasContext: CanvasContext, topicId: Long, diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/FileUploadManager.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/FileUploadManager.kt index 931b6f82cd..fde7083d30 100644 --- a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/FileUploadManager.kt +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/FileUploadManager.kt @@ -103,6 +103,13 @@ data class FileUploadConfig( assignmentId: Long ) = fromSubmitObject(fso, "courses/$courseId/assignments/$assignmentId/submissions/self/comments") + fun forSubmissionCommentFromTeacher( + fso: FileSubmitObject, + courseId: Long, + assignmentId: Long, + assigneeId: Long + ) = fromSubmitObject(fso, "courses/$courseId/assignments/$assignmentId/submissions/$assigneeId/comments") + fun forCourse( fso: FileSubmitObject, courseId: Long, diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/GroupManager.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/GroupManager.kt index 65c6167236..873eda4144 100644 --- a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/GroupManager.kt +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/GroupManager.kt @@ -119,6 +119,8 @@ object GroupManager { GroupAPI.getGroupsForCourse(adapter, depaginatedCallback, params, courseId) } + fun getAllGroupsForCourseAsync(courseId: Long, forceNetwork: Boolean) = apiAsync> { getAllGroupsForCourse(courseId, it, forceNetwork) } + @Throws(IOException::class) fun getFavoriteGroupsSynchronous(forceNetwork: Boolean): List { val adapter = RestBuilder() diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/InboxManager.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/InboxManager.kt index f48379ffc5..e8c4d2ce7d 100644 --- a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/InboxManager.kt +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/InboxManager.kt @@ -66,10 +66,10 @@ object InboxManager { InboxApi.getConversationsFiltered(scope, canvasContext, adapter, callback, params) } - fun getConversation(conversationId: Long, forceNetwork: Boolean, callback: StatusCallback) { + fun getConversation(conversationId: Long, forceNetwork: Boolean, callback: StatusCallback, markAsRead: Boolean = true) { val adapter = RestBuilder(callback) val params = RestParams(isForceReadFromNetwork = forceNetwork) - InboxApi.getConversation(adapter, callback, params, conversationId) + InboxApi.getConversation(adapter, callback, params, conversationId, markAsRead) } fun starConversation( @@ -142,7 +142,7 @@ object InboxManager { val adapter = RestBuilder() val params = RestParams(isForceReadFromNetwork = forceNetwork) return try { - val response = InboxApi.getConversation(adapter, params, conversationId) + val response = InboxApi.getConversation(adapter, params, conversationId, markAsRead = true) if (response.isSuccessful) response.body() else null } catch (e: IOException) { null diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/NotificationPreferencesManager.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/NotificationPreferencesManager.kt index dd4ef63ff1..21ed0ca56a 100644 --- a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/NotificationPreferencesManager.kt +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/NotificationPreferencesManager.kt @@ -15,6 +15,8 @@ */ package com.instructure.canvasapi2.managers +import androidx.annotation.StringRes +import com.instructure.canvasapi2.R import com.instructure.canvasapi2.StatusCallback import com.instructure.canvasapi2.apis.NotificationPreferencesAPI import com.instructure.canvasapi2.builders.RestBuilder @@ -22,10 +24,7 @@ import com.instructure.canvasapi2.builders.RestParams import com.instructure.canvasapi2.models.NotificationPreferenceResponse import com.instructure.canvasapi2.utils.weave.apiAsync -object NotificationPreferencesManager { - - const val IMMEDIATELY = "immediately" - const val NEVER = "never" +class NotificationPreferencesManager(private val notificationPreferencesApi: NotificationPreferencesAPI) { fun getNotificationPreferences( userId: Long, @@ -35,7 +34,7 @@ object NotificationPreferencesManager { ) { val adapter = RestBuilder(callback) val params = RestParams(isForceReadFromNetwork = forceNetwork) - NotificationPreferencesAPI.getNotificationPreferences( + notificationPreferencesApi.getNotificationPreferences( userId, commChannelId, adapter, @@ -58,7 +57,7 @@ object NotificationPreferencesManager { ) { val adapter = RestBuilder(callback) val params = RestParams() - NotificationPreferencesAPI.updatePreferenceCategory( + notificationPreferencesApi.updatePreferenceCategory( categoryName, channelId, frequency, @@ -73,5 +72,15 @@ object NotificationPreferencesManager { channelId: Long, frequency: String ) = apiAsync { updatePreferenceCategory(categoryName, channelId, frequency, it) } +} +enum class NotificationPreferencesFrequency(val apiString: String, @StringRes val stringRes: Int) { + IMMEDIATELY("immediately", R.string.emailNotificationsImmediately), + DAILY("daily", R.string.emailNotificationsDaily), + WEEKLY("weekly", R.string.emailNotificationsWeekly), + NEVER("never", R.string.emailNotificationsNever); + + companion object { + fun fromApiString(apiString: String) = values().find { apiString == it.apiString } ?: IMMEDIATELY + } } diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/PlannerManager.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/PlannerManager.kt index 8cd21e8663..5179f9f738 100644 --- a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/PlannerManager.kt +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/PlannerManager.kt @@ -38,7 +38,7 @@ class PlannerManager(private val plannerApi: PlannerAPI) { endDate: String? = null ) { val adapter = RestBuilder(callback) - val params = RestParams(isForceReadFromNetwork = forceNetwork) + val params = RestParams(isForceReadFromNetwork = forceNetwork, usePerPageQueryParam = true) plannerApi.getPlannerItems(adapter, callback, params, startDate, endDate) } diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/SubmissionManager.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/SubmissionManager.kt index f47d5144e8..6a6f115e5b 100644 --- a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/SubmissionManager.kt +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/SubmissionManager.kt @@ -274,6 +274,16 @@ object SubmissionManager { postStudentAnnotationSubmission(canvasContext, assignmentId, annotatableAttachmentId, it) } + fun markSubmissionAsRead( + courseId: Long, + assignmentId: Long, + callback: StatusCallback + ) { + val adapter = RestBuilder(callback) + val params = RestParams(isForceReadFromNetwork = true) + SubmissionAPI.markSubmissionAsRead(adapter, params, courseId, assignmentId, callback) + } + private fun postStudentAnnotationSubmission( canvasContext: CanvasContext, assignmentId: Long, diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/UnreadCountManager.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/UnreadCountManager.kt index 47ac3a2371..2f7f31a2ab 100644 --- a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/UnreadCountManager.kt +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/UnreadCountManager.kt @@ -22,6 +22,7 @@ import com.instructure.canvasapi2.builders.RestBuilder import com.instructure.canvasapi2.builders.RestParams import com.instructure.canvasapi2.models.UnreadConversationCount import com.instructure.canvasapi2.models.UnreadCount +import com.instructure.canvasapi2.models.UnreadNotificationCount object UnreadCountManager { @@ -37,4 +38,9 @@ object UnreadCountManager { UnreadCountAPI.getUnreadAlertCount(adapter, params, studentId, callback) } + fun getUnreadNotificationCount(callback: StatusCallback>, forceNetwork: Boolean) { + val adapter = RestBuilder(callback) + val params = RestParams(isForceReadFromNetwork = forceNetwork) + UnreadCountAPI.getUnreadNotificationsCount(adapter, params, callback) + } } diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/models/Assignment.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/models/Assignment.kt index 3ff045d18f..b60c2fb00d 100644 --- a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/models/Assignment.kt +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/models/Assignment.kt @@ -123,7 +123,13 @@ data class Assignment( * an empty submission with a null value for "submittedAt". For very old assignments, canvas might not * return a submission at all. */ - val isSubmitted: Boolean get() = submission?.submittedAt != null + val isSubmitted: Boolean get() { + return if (turnInType == TurnInType.NONE || turnInType == TurnInType.ON_PAPER) { + !(submission?.missing ?: false) + } else { + submission?.submittedAt != null + } + } val isAllowedToSubmit: Boolean get() { diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/models/Course.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/models/Course.kt index 5a5ed923d9..0ba758e41a 100644 --- a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/models/Course.kt +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/models/Course.kt @@ -60,6 +60,8 @@ data class Course( val accessRestrictedByDate: Boolean = false, @SerializedName("image_download_url") val imageUrl: String? = null, + @SerializedName("banner_image_download_url") + val bannerImageUrl: String? = null, @SerializedName("has_weighted_grading_periods") val isWeightedGradingPeriods: Boolean = false, @SerializedName("has_grading_periods") diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/models/Page.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/models/Page.kt index 63560b0e35..b2bf026ad7 100644 --- a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/models/Page.kt +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/models/Page.kt @@ -43,7 +43,9 @@ data class Page( @SerializedName("published") var published: Boolean = false, @SerializedName("editing_roles") - var editingRoles: String? = null + var editingRoles: String? = null, + @SerializedName("html_url") + var htmlUrl: String? = null ) : CanvasModel(), Parcelable { override val comparisonDate get() = updatedAt override val comparisonString get() = title diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/models/User.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/models/User.kt index a1480a6eff..ae4939858c 100644 --- a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/models/User.kt +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/models/User.kt @@ -47,7 +47,9 @@ data class User( val effective_locale: String? = null, val pronouns: String? = null, @SerializedName("k5_user") - val k5User: Boolean = false + val k5User: Boolean = false, + @SerializedName("root_account") + val rootAccount: String? = null ) : CanvasContext() { override val comparisonString get() = name override val type get() = CanvasContext.Type.USER diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/models/postmodels/PendingSubmissionComment.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/models/postmodels/PendingSubmissionComment.kt index d0ee6fdcc9..a0143ded4a 100644 --- a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/models/postmodels/PendingSubmissionComment.kt +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/models/postmodels/PendingSubmissionComment.kt @@ -29,8 +29,17 @@ data class PendingSubmissionComment( var status = CommentSendStatus.DRAFT var progress = 0f var filePath = "" + var workerId: UUID? = null + var workerInputData: FileUploadWorkerData? = null } +data class FileUploadWorkerData( + val filePaths: List, + val courseId: Long, + val assignmentId: Long, + val userId: Long +) + enum class CommentSendStatus { DRAFT, SENDING, ERROR } diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/utils/Analytics.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/utils/Analytics.kt index 08a931fdc8..461f4aa57b 100644 --- a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/utils/Analytics.kt +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/utils/Analytics.kt @@ -18,7 +18,6 @@ package com.instructure.canvasapi2.utils import android.os.Bundle -import com.google.firebase.analytics.FirebaseAnalytics import com.instructure.canvasapi2.utils.AnalyticsParamConstants.ASSIGNMENT_ID import com.instructure.canvasapi2.utils.AnalyticsParamConstants.CANVAS_CONTEXT_ID import com.instructure.canvasapi2.utils.AnalyticsParamConstants.DOMAIN_PARAM @@ -27,18 +26,16 @@ import com.instructure.canvasapi2.utils.AnalyticsParamConstants.USER_CONTEXT_ID object Analytics { - lateinit var firebase: FirebaseAnalytics - fun logEvent(eventName: String, bundle: Bundle? = null) { - firebase.logEvent(eventName, bundle) + } fun logEvent(eventName: String) { - firebase.logEvent(eventName, null) + } fun setUserProperty(propertyName: String, propertyValue: String) { - firebase.setUserProperty(propertyName, propertyValue) + } fun createOriginBundle(origin: String): Bundle { @@ -103,6 +100,8 @@ object AnalyticsEventConstants { const val WHAT_IF_GRADES = "what_if_grades_used" + const val CHANGED_C4E_MODE = "c4e_changed" + /* QR Code Login */ const val QR_CODE_LOGIN_CLICKED = "qr_code_login_clicked" const val QR_CODE_LOGIN_SUCCESS = "qr_code_login_success" @@ -114,23 +113,15 @@ object AnalyticsEventConstants { } /** - * PARAMS - * Due to the limits on custom params, we will mostly be using a mapping of the pre-defined params, - * mappings will be recorded below. Make sure we are only using params where the data is relevant. - * - * [DOMAIN_PARAM] -> AFFILIATION - * [USER_CONTEXT_ID] -> CHARACTER - * [CANVAS_CONTEXT_ID] -> GROUP_ID - * [ASSIGNMENT_ID]/DISCUSSION/ETC ID -> ITEM_ID - * There is also ITEM_CATEGORY if the event is vague regarding the type of item - * [SCREEN_OF_ORIGIN] -> ORIGIN - * Used when events can originate from multiple locations - * + * If other analytics platforms support custom params we can use these with any name, if not we can have a similiar soultion as before. */ object AnalyticsParamConstants { - const val DOMAIN_PARAM = FirebaseAnalytics.Param.AFFILIATION - const val USER_CONTEXT_ID = FirebaseAnalytics.Param.CHARACTER - const val CANVAS_CONTEXT_ID = FirebaseAnalytics.Param.GROUP_ID - const val ASSIGNMENT_ID = FirebaseAnalytics.Param.ITEM_ID - const val SCREEN_OF_ORIGIN = FirebaseAnalytics.Param.ORIGIN + const val DOMAIN_PARAM = "" + const val USER_CONTEXT_ID = "" + const val CANVAS_CONTEXT_ID = "" + const val ASSIGNMENT_ID = "" + const val SCREEN_OF_ORIGIN = "" + + //custom + const val MANUAL_C4E_STATE = "manual_c4e_state" } diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/utils/LocaleUtils.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/utils/LocaleUtils.kt index 101e775ed0..eeb4fc4619 100644 --- a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/utils/LocaleUtils.kt +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/utils/LocaleUtils.kt @@ -69,7 +69,7 @@ object LocaleUtils { Handler().postDelayed({ val intent = Intent(context, startingClass) intent.putExtra(LANGUAGES_PENDING_INTENT_KEY, LANGUAGES_PENDING_INTENT_ID) - val mPendingIntent = PendingIntent.getActivity(context, LANGUAGES_PENDING_INTENT_ID, intent, PendingIntent.FLAG_CANCEL_CURRENT) + val mPendingIntent = PendingIntent.getActivity(context, LANGUAGES_PENDING_INTENT_ID, intent, PendingIntent.FLAG_CANCEL_CURRENT or PendingIntent.FLAG_IMMUTABLE) val mgr = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager mgr.set(AlarmManager.RTC, System.currentTimeMillis() + 100, mPendingIntent) exitProcess(0) diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/utils/MasqueradeHelper.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/utils/MasqueradeHelper.kt index bf3d896bca..676f16ceb3 100644 --- a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/utils/MasqueradeHelper.kt +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/utils/MasqueradeHelper.kt @@ -97,6 +97,9 @@ object MasqueradeHelper { cleanupMasquerading(ContextKeeper.appContext) ApiPrefs.user = response.body() ApiPrefs.masqueradeId = response.body()!!.id + response.body()?.rootAccount?.let { + ApiPrefs.domain = it + } restartApplication(startingClass) } } diff --git a/libs/canvas-api-2/src/main/res/values-zh-rHK/strings.xml b/libs/canvas-api-2/src/main/res/values-b+zh+HK/strings.xml similarity index 100% rename from libs/canvas-api-2/src/main/res/values-zh-rHK/strings.xml rename to libs/canvas-api-2/src/main/res/values-b+zh+HK/strings.xml diff --git a/libs/canvas-api-2/src/test/java/com/instructure/canvasapi2/pact/canvas/apis/CoursesApiPactTests.kt b/libs/canvas-api-2/src/test/java/com/instructure/canvasapi2/pact/canvas/apis/CoursesApiPactTests.kt index fcc8b5f791..bf2b1f3842 100644 --- a/libs/canvas-api-2/src/test/java/com/instructure/canvasapi2/pact/canvas/apis/CoursesApiPactTests.kt +++ b/libs/canvas-api-2/src/test/java/com/instructure/canvasapi2/pact/canvas/apis/CoursesApiPactTests.kt @@ -102,7 +102,7 @@ class CoursesApiPactTests : ApiPactTestBase() { //region Test grabbing all courses // - val allCoursesQuery = "include[]=term&include[]=total_scores&include[]=license&include[]=is_public&include[]=needs_grading_count&include[]=permissions&include[]=favorites&include[]=current_grading_period_scores&include[]=course_image&include[]=sections&state[]=completed&state[]=available" + val allCoursesQuery = "include[]=term&include[]=total_scores&include[]=license&include[]=is_public&include[]=needs_grading_count&include[]=permissions&include[]=favorites&include[]=current_grading_period_scores&include[]=course_image&include[]=banner_image&include[]=sections&state[]=completed&state[]=available" val allCoursesPath = "/api/v1/courses" val allCoursesFieldInfo = listOf( // Evidently, permissions info is *not* returned from this call, even though include[]=permissions is specified diff --git a/libs/canvas-api-2/src/test/java/com/instructure/canvasapi2/pact/canvas/apis/InboxApiPactTests.kt b/libs/canvas-api-2/src/test/java/com/instructure/canvasapi2/pact/canvas/apis/InboxApiPactTests.kt index 0301ef4187..8be259fbe6 100644 --- a/libs/canvas-api-2/src/test/java/com/instructure/canvasapi2/pact/canvas/apis/InboxApiPactTests.kt +++ b/libs/canvas-api-2/src/test/java/com/instructure/canvasapi2/pact/canvas/apis/InboxApiPactTests.kt @@ -221,7 +221,7 @@ class InboxApiPactTests : ApiPactTestBase() { // populated. Make sure that the provider_state is set up accordingly. // - val getOneConversationQuery = "include[]=participant_avatars" + val getOneConversationQuery = "include[]=participant_avatars&auto_mark_as_read=true" val getOneConversationPath = "/api/v1/conversations/1" val getOneConversationFieldConfig = PactConversationFieldConfig( includeMessages = true, @@ -255,7 +255,7 @@ class InboxApiPactTests : ApiPactTestBase() { fun `grab a specific conversation`() { val service = createService() - val getConversationCall = service.getConversation(1) + val getConversationCall = service.getConversation(1, true) val getConversationResult = getConversationCall.execute() assertQueryParamsAndPath(getConversationCall, getOneConversationQuery, getOneConversationPath) diff --git a/libs/flutter_student_embed/lib/l10n/res/intl_en.arb b/libs/flutter_student_embed/lib/l10n/res/intl_en.arb index a0ec0ea1d5..c0d0de8f91 100644 --- a/libs/flutter_student_embed/lib/l10n/res/intl_en.arb +++ b/libs/flutter_student_embed/lib/l10n/res/intl_en.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2022-01-28T12:37:53.041723", + "@@last_modified": "2022-10-06T10:11:51.373212", "coursesLabel": "Courses", "@coursesLabel": { "description": "The label for the Courses tab", diff --git a/libs/flutter_student_embed/lib/l10n/res/intl_messages.arb b/libs/flutter_student_embed/lib/l10n/res/intl_messages.arb index a0ec0ea1d5..c0d0de8f91 100644 --- a/libs/flutter_student_embed/lib/l10n/res/intl_messages.arb +++ b/libs/flutter_student_embed/lib/l10n/res/intl_messages.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2022-01-28T12:37:53.041723", + "@@last_modified": "2022-10-06T10:11:51.373212", "coursesLabel": "Courses", "@coursesLabel": { "description": "The label for the Courses tab", diff --git a/libs/flutter_student_embed/lib/network/utils/dio_config.dart b/libs/flutter_student_embed/lib/network/utils/dio_config.dart index c17ed9fc79..83f164f3d7 100644 --- a/libs/flutter_student_embed/lib/network/utils/dio_config.dart +++ b/libs/flutter_student_embed/lib/network/utils/dio_config.dart @@ -167,7 +167,7 @@ class DioConfig { if (path == null) { return DioCacheManager(CacheConfig(baseUrl: baseUrl)).clearAll(); } else { - return DioCacheManager(CacheConfig(baseUrl: baseUrl)).deleteByPrimaryKey(path); + return DioCacheManager(CacheConfig(baseUrl: baseUrl)).deleteByPrimaryKey(path, requestMethod: "GET"); } } } diff --git a/libs/flutter_student_embed/lib/screens/calendar/calendar_widget/calendar_month.dart b/libs/flutter_student_embed/lib/screens/calendar/calendar_widget/calendar_month.dart index f15f98bbeb..25fb032cd7 100644 --- a/libs/flutter_student_embed/lib/screens/calendar/calendar_widget/calendar_month.dart +++ b/libs/flutter_student_embed/lib/screens/calendar/calendar_widget/calendar_month.dart @@ -72,6 +72,7 @@ class _CalendarMonthState extends State { @override Widget build(BuildContext context) { + weekStarts = CalendarMonth.generateWeekStarts(widget.year, widget.month); final weekWidgets = weekStarts.mapIndexed((index, weekStart) { final weekWidget = CalendarWeek( firstDay: weekStart, diff --git a/libs/flutter_student_embed/lib/utils/crash_utils.dart b/libs/flutter_student_embed/lib/utils/crash_utils.dart index ab53f79fe6..05e98bfee6 100644 --- a/libs/flutter_student_embed/lib/utils/crash_utils.dart +++ b/libs/flutter_student_embed/lib/utils/crash_utils.dart @@ -25,8 +25,8 @@ class CrashUtils { // Set up error handling 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/libs/login-api-2/src/main/java/com/instructure/loginapi/login/activities/BaseLoginFindSchoolActivity.kt b/libs/login-api-2/src/main/java/com/instructure/loginapi/login/activities/BaseLoginFindSchoolActivity.kt index 1de4fa365f..595f50a5bb 100644 --- a/libs/login-api-2/src/main/java/com/instructure/loginapi/login/activities/BaseLoginFindSchoolActivity.kt +++ b/libs/login-api-2/src/main/java/com/instructure/loginapi/login/activities/BaseLoginFindSchoolActivity.kt @@ -150,6 +150,7 @@ abstract class BaseLoginFindSchoolActivity : AppCompatActivity(), ErrorReportDia mNextActionButton!!.isEnabled = false mNextActionButton!!.setTextColor(ContextCompat.getColor(this@BaseLoginFindSchoolActivity, R.color.backgroundMedium)) + domainInput.requestFocus() domainInput.setOnEditorActionListener { _, _, _ -> validateDomain(AccountDomain(domainInput!!.text.toString())) true diff --git a/libs/login-api-2/src/main/java/com/instructure/loginapi/login/activities/BaseLoginLandingPageActivity.kt b/libs/login-api-2/src/main/java/com/instructure/loginapi/login/activities/BaseLoginLandingPageActivity.kt index 7f444b601e..c766311cd7 100644 --- a/libs/login-api-2/src/main/java/com/instructure/loginapi/login/activities/BaseLoginLandingPageActivity.kt +++ b/libs/login-api-2/src/main/java/com/instructure/loginapi/login/activities/BaseLoginLandingPageActivity.kt @@ -39,6 +39,7 @@ import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import com.google.gson.Gson import com.instructure.canvasapi2.apis.ErrorReportAPI +import com.instructure.canvasapi2.models.AccountDomain import com.instructure.canvasapi2.models.ErrorReportPreFill import com.instructure.canvasapi2.utils.APIHelper import com.instructure.canvasapi2.utils.Analytics @@ -57,7 +58,9 @@ import com.instructure.loginapi.login.util.Const.MOBILE_VERIFY_FLOW import com.instructure.loginapi.login.util.Const.NORMAL_FLOW import com.instructure.loginapi.login.util.Const.SNICKER_DOODLES import com.instructure.loginapi.login.util.Const.URL_CANVAS_NETWORK +import com.instructure.loginapi.login.util.LoginPrefs import com.instructure.loginapi.login.util.PreviousUsersUtils +import com.instructure.loginapi.login.util.SavedLoginInfo import com.instructure.loginapi.login.viewmodel.LoginViewModel import com.instructure.pandautils.mvvm.Event import com.instructure.pandautils.utils.* @@ -74,7 +77,7 @@ abstract class BaseLoginLandingPageActivity : AppCompatActivity(), ErrorReportDi protected abstract fun beginFindSchoolFlow(): Intent - protected abstract fun signInActivityIntent(snickerDoodle: SnickerDoodle): Intent + protected abstract fun signInActivityIntent(accountDomain: AccountDomain): Intent protected abstract fun beginCanvasNetworkFlow(url: String): Intent @@ -102,22 +105,13 @@ abstract class BaseLoginLandingPageActivity : AppCompatActivity(), ErrorReportDi loadPreviousUsers() setupGesture() setupSnickerDoodles() + setupButtons() } private fun bindViews() { // Only show the what's new text if the app supports it changesLayout.visibility = if (appChangesLink() != null) View.VISIBLE else View.GONE - findMySchool.onClick { - if (APIHelper.hasNetworkConnection()) { - val intent = beginFindSchoolFlow() - intent.putExtra(Const.CANVAS_LOGIN, canvasLogin) - startActivity(intent) - } else { - NoInternetConnectionDialog.show(supportFragmentManager) - } - } - canvasNetwork.onClick { if (APIHelper.hasNetworkConnection()) { val intent = beginCanvasNetworkFlow(URL_CANVAS_NETWORK) @@ -358,7 +352,7 @@ abstract class BaseLoginLandingPageActivity : AppCompatActivity(), ErrorReportDi drawerRecyclerView.layoutManager = LinearLayoutManager(this, RecyclerView.VERTICAL, true) drawerRecyclerView.adapter = SnickerDoodleAdapter(snickerDoodles) { snickerDoodle -> drawerLayout.closeDrawers() - val intent = signInActivityIntent(snickerDoodle) + val intent = signInActivityIntent(AccountDomain(snickerDoodle.domain)) intent.putExtra(SNICKER_DOODLES, snickerDoodle) startActivity(intent) finish() @@ -369,6 +363,50 @@ abstract class BaseLoginLandingPageActivity : AppCompatActivity(), ErrorReportDi } } + private fun setupButtons() { + val lastSavedLogin = LoginPrefs.lastSavedLogin + if (lastSavedLogin != null) { + openRecentSchool?.visibility = View.VISIBLE + findAnotherSchool?.visibility = View.VISIBLE + findMySchool?.visibility = View.GONE + + openRecentSchool?.text = if (lastSavedLogin.accountDomain.name.isNullOrEmpty()) { + lastSavedLogin.accountDomain.domain + } else { + lastSavedLogin.accountDomain.name + } + openRecentSchool?.onClick { openRecentSchool(lastSavedLogin) } + + findAnotherSchool?.onClick { findSchool() } + } else { + openRecentSchool?.visibility = View.GONE + findAnotherSchool?.visibility = View.GONE + findMySchool?.visibility = View.VISIBLE + + findMySchool?.onClick { findSchool() } + } + } + + private fun openRecentSchool(lastSavedLogin: SavedLoginInfo) { + if (APIHelper.hasNetworkConnection()) { + val intent = signInActivityIntent(lastSavedLogin.accountDomain) + intent.putExtra(Const.CANVAS_LOGIN, lastSavedLogin.canvasLogin) + startActivity(intent) + } else { + NoInternetConnectionDialog.show(supportFragmentManager) + } + } + + private fun findSchool() { + if (APIHelper.hasNetworkConnection()) { + val intent = beginFindSchoolFlow() + intent.putExtra(Const.CANVAS_LOGIN, canvasLogin) + startActivity(intent) + } else { + NoInternetConnectionDialog.show(supportFragmentManager) + } + } + override fun onTicketPost() { toast(R.string.errorReportThankyou) } diff --git a/libs/login-api-2/src/main/java/com/instructure/loginapi/login/activities/BaseLoginSignInActivity.kt b/libs/login-api-2/src/main/java/com/instructure/loginapi/login/activities/BaseLoginSignInActivity.kt index 848211bd80..8839e4e587 100644 --- a/libs/login-api-2/src/main/java/com/instructure/loginapi/login/activities/BaseLoginSignInActivity.kt +++ b/libs/login-api-2/src/main/java/com/instructure/loginapi/login/activities/BaseLoginSignInActivity.kt @@ -66,7 +66,9 @@ import com.instructure.loginapi.login.util.Const.CANVAS_LOGIN_FLOW import com.instructure.loginapi.login.util.Const.MASQUERADE_FLOW import com.instructure.loginapi.login.util.Const.MOBILE_VERIFY_FLOW import com.instructure.loginapi.login.util.Const.SNICKER_DOODLES +import com.instructure.loginapi.login.util.LoginPrefs import com.instructure.loginapi.login.util.PreviousUsersUtils.add +import com.instructure.loginapi.login.util.SavedLoginInfo import com.instructure.loginapi.login.viewmodel.LoginViewModel import com.instructure.pandautils.mvvm.Event import com.instructure.pandautils.utils.* @@ -452,6 +454,7 @@ abstract class BaseLoginSignInActivity : AppCompatActivity(), OnAuthenticationSe ) add(this@BaseLoginSignInActivity, user) refreshWidgets() + LoginPrefs.lastSavedLogin = SavedLoginInfo(accountDomain, canvasLogin) handleLaunchApplicationMainActivityIntent() } } diff --git a/libs/login-api-2/src/main/java/com/instructure/loginapi/login/adapter/PreviousUsersAdapter.kt b/libs/login-api-2/src/main/java/com/instructure/loginapi/login/adapter/PreviousUsersAdapter.kt index 0827a48915..4d96bba9f0 100644 --- a/libs/login-api-2/src/main/java/com/instructure/loginapi/login/adapter/PreviousUsersAdapter.kt +++ b/libs/login-api-2/src/main/java/com/instructure/loginapi/login/adapter/PreviousUsersAdapter.kt @@ -23,9 +23,8 @@ import androidx.recyclerview.widget.RecyclerView import com.instructure.canvasapi2.utils.Pronouns.span import com.instructure.loginapi.login.R import com.instructure.loginapi.login.model.SignedInUser -import com.instructure.pandautils.utils.ProfileUtils.loadAvatarForUser +import com.instructure.pandautils.utils.ProfileUtils import kotlinx.android.synthetic.main.adapter_previous_users.view.* -import java.util.* class PreviousUsersAdapter( private val previousUsers: ArrayList, @@ -63,7 +62,7 @@ class PreviousUsersAdapter( class PreviousUserHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { fun bind(user: SignedInUser, onUserClick: () -> Unit, onUserRemove: () -> Unit) = with(itemView) { - loadAvatarForUser(usersAvatar, user.user.name, user.user.avatarUrl) + ProfileUtils.loadAvatarForUser(usersAvatar, user.user.name, user.user.avatarUrl, 0) userName.text = span(user.user.name, user.user.pronouns) schoolDomain.text = user.domain setOnClickListener { onUserClick() } diff --git a/libs/login-api-2/src/main/java/com/instructure/loginapi/login/util/LoginPrefs.kt b/libs/login-api-2/src/main/java/com/instructure/loginapi/login/util/LoginPrefs.kt new file mode 100644 index 0000000000..2a11152c7f --- /dev/null +++ b/libs/login-api-2/src/main/java/com/instructure/loginapi/login/util/LoginPrefs.kt @@ -0,0 +1,15 @@ +package com.instructure.loginapi.login.util + +import com.instructure.canvasapi2.models.AccountDomain +import com.instructure.canvasapi2.utils.BooleanPref +import com.instructure.canvasapi2.utils.GsonPref +import com.instructure.canvasapi2.utils.IntPref +import com.instructure.canvasapi2.utils.LongPref +import com.instructure.canvasapi2.utils.PrefManager +import com.instructure.pandautils.dialogs.RatingDialog + +data class SavedLoginInfo(val accountDomain: AccountDomain, val canvasLogin: Int) + +object LoginPrefs : PrefManager("loginPrefs") { + var lastSavedLogin by GsonPref(SavedLoginInfo::class.java, null) +} \ No newline at end of file diff --git a/libs/login-api-2/src/main/res/drawable/bg_button_rounded_outline.xml b/libs/login-api-2/src/main/res/drawable/bg_button_rounded_outline.xml index 0071462ec5..bafbe7dbd1 100644 --- a/libs/login-api-2/src/main/res/drawable/bg_button_rounded_outline.xml +++ b/libs/login-api-2/src/main/res/drawable/bg_button_rounded_outline.xml @@ -19,8 +19,7 @@ - - + diff --git a/libs/login-api-2/src/main/res/layout-land/activity_login_landing_page.xml b/libs/login-api-2/src/main/res/layout-land/activity_login_landing_page.xml index d1cc7aba67..e71c00f174 100644 --- a/libs/login-api-2/src/main/res/layout-land/activity_login_landing_page.xml +++ b/libs/login-api-2/src/main/res/layout-land/activity_login_landing_page.xml @@ -50,13 +50,34 @@ android:layout_width="64dp" android:layout_height="64dp" android:importantForAccessibility="no" + android:layout_marginBottom="20dp" app:srcCompat="@drawable/ic_canvas_logo"/> +