diff --git a/PULL_REQUEST_TEMPLATE b/PULL_REQUEST_TEMPLATE
new file mode 100644
index 0000000000..b56e60264e
--- /dev/null
+++ b/PULL_REQUEST_TEMPLATE
@@ -0,0 +1,16 @@
+--- edit or delete this section ---
+## Screenshots
+
+
+Before | After |
+
+ |
+ |
+
+
+
+## Checklist
+
+- [ ] Follow-up e2e test ticket created or not needed
+- [ ] A11y checked
+- [ ] Approve from product or not needed
diff --git a/apps/flutter_parent/android/app/build.gradle b/apps/flutter_parent/android/app/build.gradle
index ea3bc8685a..532b872ab9 100644
--- a/apps/flutter_parent/android/app/build.gradle
+++ b/apps/flutter_parent/android/app/build.gradle
@@ -78,6 +78,19 @@ android {
shrinkResources false // Must be false, otherwise resources we need are erroneously stripped out
proguardFiles 'proguard-rules.pro'
}
+ applicationVariants.all{
+ variant ->
+ variant.outputs.each{
+ output->
+ project.ext { appName = 'parent' }
+ def dateTimeStamp = new Date().format('yyyy-MM-dd-HH-mm-ss')
+ def newName = output.outputFile.name
+ newName = newName.replace("app-", "$project.ext.appName-")
+ newName = newName.replace("-debug", "-dev-debug-" + dateTimeStamp)
+ newName = newName.replace("-release", "-prod-release")
+ output.outputFileName = newName
+ }
+ }
}
}
diff --git a/apps/flutter_parent/android/app/src/main/AndroidManifest.xml b/apps/flutter_parent/android/app/src/main/AndroidManifest.xml
index eb3ec7352a..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 = ""
)
- 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"/>
+
+
+
+
+ android:textSize="14sp"
+ android:layout_marginBottom="36dp"/>
+
+
+
+
+ style="@style/LoginButtonStyle.Filled"
+ android:layout_marginHorizontal="48dp"
+ android:layout_marginBottom="36dp"
+ android:text="@string/findMySchool" />
-
+ android:layout_centerVertical="true"
+ android:background="@drawable/avatar_circular_border_thin"
+ android:backgroundTint="@color/backgroundMedium"
+ android:padding="@dimen/avatar_border_width_thin" />
You will stop acting as %s and return to your account.
We\'ve made a few changes.
See what\'s new
+ Find another school
diff --git a/libs/login-api-2/src/main/res/values/styles.xml b/libs/login-api-2/src/main/res/values/styles.xml
index e1fa4bba1c..47d0d83972 100644
--- a/libs/login-api-2/src/main/res/values/styles.xml
+++ b/libs/login-api-2/src/main/res/values/styles.xml
@@ -97,4 +97,27 @@
- @font/lato_font_family
+
+
+
+
+
+
+
diff --git a/libs/pandares/src/main/assets/confetti.json b/libs/pandares/src/main/assets/confetti.json
new file mode 100644
index 0000000000..f84de5d6e7
--- /dev/null
+++ b/libs/pandares/src/main/assets/confetti.json
@@ -0,0 +1 @@
+{"v":"5.5.6","fr":60,"ip":0,"op":300,"w":609,"h":812,"nm":"lottie (mobile)","ddd":0,"assets":[{"id":"comp_0","layers":[{"ddd":0,"ind":1,"ty":0,"nm":"_small-side","refId":"comp_1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[218,320,0],"ix":2},"a":{"a":0,"k":[400,400,0],"ix":1},"s":{"a":0,"k":[-100,100,100],"ix":6}},"ao":0,"w":800,"h":800,"ip":15,"op":234,"st":15,"bm":0}]},{"id":"comp_1","layers":[{"ddd":0,"ind":1,"ty":4,"nm":"streamer b","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":166,"ix":10},"p":{"a":0,"k":[554,664,0],"ix":2},"a":{"a":0,"k":[-157,-245,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[-1.685,-13.314],[0,-14.907],[0,-15.206],[0,-14.907],[0,-14.907],[0,-15.206],[1.754,-14.206],[-3.934,-9.465]],"o":[[-3.895,8.562],[1.872,14.789],[0,15.206],[0,14.907],[0,14.907],[0,15.206],[0,14.314],[-1.803,14.605],[0,0]],"v":[[-156.5,-406],[-166.5,-367],[-146.5,-327],[-166.5,-286],[-146.5,-246],[-166.5,-206],[-146.5,-165],[-166.5,-127],[-156.5,-84]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.929411768913,0.745098054409,0.196078434587,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":9,"s":[4]},{"t":57,"s":[0.5]}],"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":14,"s":[0]},{"t":57,"s":[100]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":9,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":14,"s":[35]},{"t":57,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":9,"op":58,"st":9,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"streamer a","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":167,"ix":10},"p":{"a":0,"k":[532,582,0],"ix":2},"a":{"a":0,"k":[-157,-245,0],"ix":1},"s":{"a":0,"k":[-100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[-1.685,-13.314],[0,-14.907],[0,-15.206],[0,-14.907],[0,-14.907],[0,-15.206],[1.754,-14.206],[-3.934,-9.465]],"o":[[-3.895,8.562],[1.872,14.789],[0,15.206],[0,14.907],[0,14.907],[0,15.206],[0,14.314],[-1.803,14.605],[0,0]],"v":[[-156.5,-406],[-166.5,-367],[-146.5,-327],[-166.5,-286],[-146.5,-246],[-166.5,-206],[-146.5,-165],[-166.5,-127],[-156.5,-84]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.196078434587,0.380392163992,0.929411768913,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":5,"s":[4]},{"t":48,"s":[0.5]}],"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":10,"s":[0]},{"t":48,"s":[100]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":5,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":10,"s":[35]},{"t":48,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":5,"op":49,"st":5,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"circle a","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":1,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.333],"y":[0]},"t":29,"s":[321.019]},{"t":158,"s":[1800]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.167,"y":0.167},"t":1,"s":[599.5,838,0],"to":[-30,-106.667,0],"ti":[46.667,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.333,"y":0},"t":29,"s":[419.5,198,0],"to":[-46.667,0,0],"ti":[0,0,0]},{"t":158,"s":[319.5,838,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0,0]},"t":1,"s":[50,100,100]},{"i":{"x":[0.833,0.833,0.833],"y":[1,0.833,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":29,"s":[100,100,100]},{"t":128,"s":[100,50,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[16,16],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.960784316063,0.572549045086,0.180392161012,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":1,"op":159,"st":1,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"circle b","sr":1,"ks":{"o":{"a":0,"k":50,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":3,"s":[0]},{"t":128,"s":[1440]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.167,"y":0.167},"t":3,"s":[599.5,838,0],"to":[-26.667,-93.333,0],"ti":[66.667,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.333,"y":0},"t":21,"s":[439.5,278,0],"to":[-66.667,0,0],"ti":[0,0,0]},{"t":128,"s":[199.5,838,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0,0]},"t":3,"s":[50,100,100]},{"i":{"x":[0.833,0.833,0.833],"y":[1,0.833,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":21,"s":[100,100,100]},{"t":98,"s":[100,50,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[16,16],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.196078434587,0.588235318661,0.929411768913,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":3,"op":129,"st":3,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":"star a","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":1,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.333],"y":[0]},"t":31,"s":[343.949]},{"t":158,"s":[1800]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.167,"y":0.167},"t":1,"s":[596.087,836.292,0],"to":[-36.098,-100,0],"ti":[52.765,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.333,"y":0},"t":31,"s":[379.5,236.292,0],"to":[-52.765,0,0],"ti":[0,0,0]},{"t":158,"s":[279.5,836.292,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0,0]},"t":1,"s":[50,100,100]},{"i":{"x":[0.833,0.833,0.833],"y":[1,0.833,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":31,"s":[100,100,100]},{"t":128,"s":[100,50,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"sr","sy":1,"d":1,"pt":{"a":0,"k":5,"ix":3},"p":{"a":0,"k":[0,0],"ix":4},"r":{"a":0,"k":0,"ix":5},"ir":{"a":0,"k":5,"ix":6},"is":{"a":0,"k":0,"ix":8},"or":{"a":0,"k":12,"ix":7},"os":{"a":0,"k":0,"ix":9},"ix":1,"nm":"Polystar Path 1","mn":"ADBE Vector Shape - Star","hd":false},{"ty":"fl","c":{"a":0,"k":[0.929411768913,0.745098054409,0.196078434587,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Polystar 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":1,"op":159,"st":1,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":"star b","sr":1,"ks":{"o":{"a":0,"k":50,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":3,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.333],"y":[0]},"t":23,"s":[288]},{"t":128,"s":[1800]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.167,"y":0.167},"t":3,"s":[596.087,836.292,0],"to":[-39.431,-113.333,0],"ti":[66.098,1.667,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.333,"y":0},"t":23,"s":[359.5,156.292,0],"to":[-66.098,-1.667,0],"ti":[0,0,0]},{"t":128,"s":[199.5,826.292,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0,0]},"t":3,"s":[50,100,100]},{"i":{"x":[0.833,0.833,0.833],"y":[1,0.833,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":23,"s":[100,100,100]},{"t":98,"s":[100,50,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"sr","sy":1,"d":1,"pt":{"a":0,"k":5,"ix":3},"p":{"a":0,"k":[0,0],"ix":4},"r":{"a":0,"k":0,"ix":5},"ir":{"a":0,"k":5,"ix":6},"is":{"a":0,"k":0,"ix":8},"or":{"a":0,"k":12,"ix":7},"os":{"a":0,"k":0,"ix":9},"ix":1,"nm":"Polystar Path 1","mn":"ADBE Vector Shape - Star","hd":false},{"ty":"fl","c":{"a":0,"k":[0.537254929543,0.196078434587,0.929411768913,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Polystar 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":3,"op":129,"st":3,"bm":0},{"ddd":0,"ind":7,"ty":4,"nm":"rec a","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.333],"y":[0]},"t":33,"s":[381.468]},{"t":218,"s":[2520]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.167,"y":0.167},"t":0,"s":[599.5,842,0],"to":[-23.333,-100,0],"ti":[41.333,1.333,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.333,"y":0},"t":33,"s":[459.5,242,0],"to":[-41.333,-1.333,0],"ti":[0,0,0]},{"t":218,"s":[351.5,834,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0,0]},"t":0,"s":[50,100,100]},{"i":{"x":[0.833,0.833,0.833],"y":[1,0.833,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":33,"s":[100,100,100]},{"t":188,"s":[100,50,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[16,8],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":0,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"fl","c":{"a":0,"k":[0.305882364511,0.831372559071,0.411764711142,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":219,"st":0,"bm":0},{"ddd":0,"ind":8,"ty":4,"nm":"rec b","sr":1,"ks":{"o":{"a":0,"k":50,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":2,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.333],"y":[0]},"t":25,"s":[262.857]},{"t":191,"s":[2160]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.167,"y":0.167},"t":2,"s":[599.5,842,0],"to":[-23.333,-113.333,0],"ti":[74,5,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.333,"y":0},"t":25,"s":[459.5,162,0],"to":[-74,-5,0],"ti":[0,0,0]},{"t":191,"s":[155.5,812,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0,0]},"t":2,"s":[50,100,100]},{"i":{"x":[0.833,0.833,0.833],"y":[1,0.833,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":25,"s":[100,100,100]},{"t":161,"s":[100,50,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[16,8],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":0,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"fl","c":{"a":0,"k":[0.929411768913,0.196078434587,0.784313738346,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":2,"op":192,"st":2,"bm":0},{"ddd":0,"ind":9,"ty":4,"nm":"square a","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.333],"y":[0]},"t":35,"s":[462.385]},{"t":218,"s":[2880]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.167,"y":0.167},"t":0,"s":[599.5,838,0],"to":[-43.333,-123.333,0],"ti":[60,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.333,"y":0},"t":35,"s":[339.5,98,0],"to":[-60,0,0],"ti":[0,0,0]},{"t":218,"s":[239.5,838,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0,0]},"t":0,"s":[50,100,100]},{"i":{"x":[0.833,0.833,0.833],"y":[1,0.833,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":35,"s":[100,100,100]},{"t":188,"s":[100,50,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[16,16],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":0,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"fl","c":{"a":0,"k":[0.588235318661,0.831372559071,0.305882364511,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":219,"st":0,"bm":0},{"ddd":0,"ind":10,"ty":4,"nm":"square b","sr":1,"ks":{"o":{"a":0,"k":50,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":2,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.333],"y":[0]},"t":27,"s":[285.714]},{"t":191,"s":[2160]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.167,"y":0.167},"t":2,"s":[599.5,838,0],"to":[-30,-120,0],"ti":[75,3.333,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.333,"y":0},"t":27,"s":[419.5,118,0],"to":[-75,-3.333,0],"ti":[0,0,0]},{"t":191,"s":[149.5,818,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0,0]},"t":2,"s":[50,100,100]},{"i":{"x":[0.833,0.833,0.833],"y":[1,0.833,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":27,"s":[100,100,100]},{"t":161,"s":[100,50,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[16,16],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":0,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"fl","c":{"a":0,"k":[0.196078434587,0.380392163992,0.929411768913,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":2,"op":192,"st":2,"bm":0},{"ddd":0,"ind":11,"ty":4,"nm":"streamer b 2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":171,"ix":10},"p":{"a":0,"k":[543,427,0],"ix":2},"a":{"a":0,"k":[-157,-245,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[-1.685,-13.314],[0,-14.907],[0,-15.206],[0,-14.907],[0,-14.907],[0,-15.206],[1.754,-14.206],[-3.934,-9.465]],"o":[[-3.895,8.562],[1.872,14.789],[0,15.206],[0,14.907],[0,14.907],[0,15.206],[0,14.314],[-1.803,14.605],[0,0]],"v":[[-156.5,-406],[-166.5,-367],[-146.5,-327],[-166.5,-286],[-146.5,-246],[-166.5,-206],[-146.5,-165],[-166.5,-127],[-156.5,-84]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.929411768913,0.196078434587,0.317647069693,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":13,"s":[4]},{"t":61,"s":[0.5]}],"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":18,"s":[0]},{"t":61,"s":[100]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":13,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":18,"s":[35]},{"t":61,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":13,"op":62,"st":13,"bm":0},{"ddd":0,"ind":12,"ty":4,"nm":"streamer a 2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":151,"ix":10},"p":{"a":0,"k":[454,444,0],"ix":2},"a":{"a":0,"k":[-157,-245,0],"ix":1},"s":{"a":0,"k":[-100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[-1.685,-13.314],[0,-14.907],[0,-15.206],[0,-14.907],[0,-14.907],[0,-15.206],[1.754,-14.206],[-3.934,-9.465]],"o":[[-3.895,8.562],[1.872,14.789],[0,15.206],[0,14.907],[0,14.907],[0,15.206],[0,14.314],[-1.803,14.605],[0,0]],"v":[[-156.5,-406],[-166.5,-367],[-146.5,-327],[-166.5,-286],[-146.5,-246],[-166.5,-206],[-146.5,-165],[-166.5,-127],[-156.5,-84]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.305882364511,0.831372559071,0.803921580315,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":10,"s":[4]},{"t":53,"s":[0.5]}],"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":15,"s":[0]},{"t":53,"s":[100]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":10,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":15,"s":[35]},{"t":53,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":10,"op":54,"st":10,"bm":0},{"ddd":0,"ind":13,"ty":4,"nm":"circle a 2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":1,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.333],"y":[0]},"t":29,"s":[-371.368]},{"t":191,"s":[-2520]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.167,"y":0.167},"t":1,"s":[599.5,838,0],"to":[-53.333,-113.333,0],"ti":[56.667,-3.333,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.333,"y":0},"t":29,"s":[279.5,158,0],"to":[-56.667,3.333,0],"ti":[0,0,0]},{"t":191,"s":[259.5,858,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0,0.167,0]},"t":1,"s":[100,50,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":29,"s":[100,100,100]},{"t":161,"s":[50,100,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[16,16],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.305882364511,0.831372559071,0.411764711142,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":1,"op":192,"st":1,"bm":0},{"ddd":0,"ind":14,"ty":4,"nm":"circle b 2","sr":1,"ks":{"o":{"a":0,"k":50,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":3,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.333],"y":[0]},"t":21,"s":[-250.839]},{"t":158,"s":[-2160]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.167,"y":0.167},"t":3,"s":[599.5,838,0],"to":[-53.333,-100,0],"ti":[63,-7,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.333,"y":0},"t":21,"s":[279.5,238,0],"to":[-63,7,0],"ti":[0,0,0]},{"t":158,"s":[221.5,880,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0,0.167,0]},"t":3,"s":[100,50,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":21,"s":[100,100,100]},{"t":128,"s":[50,100,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[16,16],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.537254929543,0.196078434587,0.929411768913,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":3,"op":159,"st":3,"bm":0},{"ddd":0,"ind":15,"ty":4,"nm":"star a 2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":1,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.333],"y":[0]},"t":31,"s":[-397.895]},{"t":191,"s":[-2520]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.167,"y":0.167},"t":1,"s":[596.087,836.292,0],"to":[-9.431,-113.333,0],"ti":[42.765,-3.333,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.333,"y":0},"t":31,"s":[539.5,156.292,0],"to":[-42.765,3.333,0],"ti":[0,0,0]},{"t":191,"s":[339.5,856.292,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0,0.167,0]},"t":1,"s":[100,50,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":31,"s":[100,100,100]},{"t":161,"s":[50,100,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"sr","sy":1,"d":1,"pt":{"a":0,"k":5,"ix":3},"p":{"a":0,"k":[0,0],"ix":4},"r":{"a":0,"k":0,"ix":5},"ir":{"a":0,"k":5,"ix":6},"is":{"a":0,"k":0,"ix":8},"or":{"a":0,"k":12,"ix":7},"os":{"a":0,"k":0,"ix":9},"ix":1,"nm":"Polystar Path 1","mn":"ADBE Vector Shape - Star","hd":false},{"ty":"fl","c":{"a":0,"k":[0.305882364511,0.831372559071,0.803921580315,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Polystar 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":1,"op":192,"st":1,"bm":0},{"ddd":0,"ind":16,"ty":4,"nm":"star b 2","sr":1,"ks":{"o":{"a":0,"k":50,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":3,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.333],"y":[0]},"t":23,"s":[-278.71]},{"t":158,"s":[-2160]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.167,"y":0.167},"t":3,"s":[596.087,836.292,0],"to":[-12.765,-96.667,0],"ti":[52.765,-3.333,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.333,"y":0},"t":23,"s":[519.5,256.292,0],"to":[-52.765,3.333,0],"ti":[0,0,0]},{"t":158,"s":[279.5,856.292,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0,0.167,0]},"t":3,"s":[100,50,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":23,"s":[100,100,100]},{"t":128,"s":[50,100,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"sr","sy":1,"d":1,"pt":{"a":0,"k":5,"ix":3},"p":{"a":0,"k":[0,0],"ix":4},"r":{"a":0,"k":0,"ix":5},"ir":{"a":0,"k":5,"ix":6},"is":{"a":0,"k":0,"ix":8},"or":{"a":0,"k":12,"ix":7},"os":{"a":0,"k":0,"ix":9},"ix":1,"nm":"Polystar Path 1","mn":"ADBE Vector Shape - Star","hd":false},{"ty":"fl","c":{"a":0,"k":[0.929411768913,0.196078434587,0.784313738346,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Polystar 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":3,"op":159,"st":3,"bm":0},{"ddd":0,"ind":17,"ty":4,"nm":"rec a 2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.333],"y":[0]},"t":33,"s":[-556.875]},{"t":128,"s":[-2160]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.167,"y":0.167},"t":0,"s":[599.5,842,0],"to":[-16.667,-120,0],"ti":[66.667,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.333,"y":0},"t":33,"s":[499.5,122,0],"to":[-66.667,0,0],"ti":[0,0,0]},{"t":128,"s":[199.5,842,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0,0.167,0]},"t":0,"s":[100,50,100]},{"i":{"x":[0.833,0.833,0.833],"y":[1,0.833,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":33,"s":[100,100,100]},{"t":98,"s":[100,50,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[16,8],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":0,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"fl","c":{"a":0,"k":[0.588235318661,0.831372559071,0.305882364511,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":129,"st":0,"bm":0},{"ddd":0,"ind":18,"ty":4,"nm":"rec b 2","sr":1,"ks":{"o":{"a":0,"k":50,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":2,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.333],"y":[0]},"t":25,"s":[-306.667]},{"t":218,"s":[-2880]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.167,"y":0.167},"t":2,"s":[599.5,842,0],"to":[-22.62,-109.87,0],"ti":[2.877,-2.055,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.333,"y":0},"t":25,"s":[339.5,262,0],"to":[-51.333,36.667,0],"ti":[0,0,0]},{"t":218,"s":[219.5,862,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0,0.167,0]},"t":2,"s":[100,50,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":25,"s":[100,100,100]},{"t":188,"s":[50,100,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[16,8],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":0,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"fl","c":{"a":0,"k":[0.929411768913,0.196078434587,0.317647069693,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":2,"op":219,"st":2,"bm":0},{"ddd":0,"ind":19,"ty":4,"nm":"square a 2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.333],"y":[0]},"t":35,"s":[-590.625]},{"t":128,"s":[-2160]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.167,"y":0.167},"t":0,"s":[599.5,838,0],"to":[-20,-90,0],"ti":[36.667,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.333,"y":0},"t":35,"s":[479.5,298,0],"to":[-36.667,0,0],"ti":[0,0,0]},{"t":128,"s":[379.5,838,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0,0.167,0]},"t":0,"s":[100,50,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":35,"s":[100,100,100]},{"t":98,"s":[50,100,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[16,16],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":0,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"fl","c":{"a":0,"k":[0.960784316063,0.572549045086,0.180392161012,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":129,"st":0,"bm":0},{"ddd":0,"ind":20,"ty":4,"nm":"square b 2","sr":1,"ks":{"o":{"a":0,"k":50,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":2,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.333],"y":[0]},"t":27,"s":[-333.333]},{"t":218,"s":[-2880]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.167,"y":0.167},"t":2,"s":[599.5,838,0],"to":[-10,-103.333,0],"ti":[50,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.333,"y":0},"t":27,"s":[539.5,218,0],"to":[-50,0,0],"ti":[0,0,0]},{"t":218,"s":[299.5,838,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0,0.167,0]},"t":2,"s":[100,50,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":27,"s":[100,100,100]},{"t":188,"s":[50,100,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[16,16],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":0,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"fl","c":{"a":0,"k":[0.196078434587,0.588235318661,0.929411768913,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":2,"op":219,"st":2,"bm":0}]},{"id":"comp_2","layers":[{"ddd":0,"ind":1,"ty":0,"nm":"_small-side","refId":"comp_1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[260,320,0],"ix":2},"a":{"a":0,"k":[400,400,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"w":800,"h":800,"ip":0,"op":219,"st":0,"bm":0}]},{"id":"comp_3","layers":[{"ddd":0,"ind":1,"ty":0,"nm":"left","refId":"comp_4","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[400,400,0],"ix":2},"a":{"a":0,"k":[400,400,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"w":800,"h":800,"ip":13,"op":313,"st":13,"bm":0},{"ddd":0,"ind":2,"ty":0,"nm":"right","refId":"comp_4","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[400,400,0],"ix":2},"a":{"a":0,"k":[400,400,0],"ix":1},"s":{"a":0,"k":[-100,100,100],"ix":6}},"ao":0,"w":800,"h":800,"ip":30,"op":330,"st":30,"bm":0}]},{"id":"comp_4","layers":[{"ddd":0,"ind":1,"ty":4,"nm":"streamer a 4","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":14,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":13,"s":[219.178,-190.096,0],"to":[-95.333,426.667,0],"ti":[167.333,-560.667,0]},{"t":173,"s":[179.178,989.904,0]}],"ix":2},"a":{"a":0,"k":[-157,-245,0],"ix":1},"s":{"a":0,"k":[-100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[-1.685,-13.314],[0,-14.907],[0,-15.206],[0,-14.907],[0,-14.907],[0,-15.206],[1.754,-14.206],[-3.934,-9.465]],"o":[[-3.895,8.562],[1.872,14.789],[0,15.206],[0,14.907],[0,14.907],[0,15.206],[0,14.314],[-1.803,14.605],[0,0]],"v":[[-156.5,-406],[-166.5,-367],[-146.5,-327],[-166.5,-286],[-146.5,-246],[-166.5,-206],[-146.5,-165],[-166.5,-127],[-156.5,-84]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.196078434587,0.380392163992,0.929411768913,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":13,"s":[4]},{"t":176,"s":[0.5]}],"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":18,"s":[0]},{"t":176,"s":[100]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":13,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":18,"s":[40]},{"t":176,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":13,"op":174,"st":13,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"streamer b 4","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":-1.458,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":5,"s":[269.863,-175.455,0],"to":[-110,415.333,0],"ti":[216,-599.333,0]},{"t":173,"s":[69.863,984.545,0]}],"ix":2},"a":{"a":0,"k":[-157,-245,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[-1.685,-13.314],[0,-14.907],[0,-15.206],[0,-14.907],[0,-14.907],[0,-15.206],[1.754,-14.206],[-3.934,-9.465]],"o":[[-3.895,8.562],[1.872,14.789],[0,15.206],[0,14.907],[0,14.907],[0,15.206],[0,14.314],[-1.803,14.605],[0,0]],"v":[[-156.5,-406],[-166.5,-367],[-146.5,-327],[-166.5,-286],[-146.5,-246],[-166.5,-206],[-146.5,-165],[-166.5,-127],[-156.5,-84]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.929411768913,0.745098054409,0.196078434587,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":5,"s":[4]},{"t":173,"s":[0.5]}],"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":10,"s":[0]},{"t":173,"s":[100]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":5,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":10,"s":[40]},{"t":173,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":5,"op":174,"st":5,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"circle a 4","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":8,"s":[0]},{"t":155,"s":[1800]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":8,"s":[325.643,-26.292,0],"to":[-101.333,75.667,0],"ti":[15.333,-507.667,0]},{"t":155,"s":[125.643,835.708,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":8,"s":[50,100,100]},{"t":155,"s":[100,50,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[16,16],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.960784316063,0.572549045086,0.180392161012,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":8,"op":156,"st":-7,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"circle b 4","sr":1,"ks":{"o":{"a":0,"k":50,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":2,"s":[0]},{"t":215,"s":[2520]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":2,"s":[619.5,-26.292,0],"to":[-138,77.667,0],"ti":[-2,-497.667,0]},{"t":215,"s":[259.5,835.708,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":2,"s":[50,100,100]},{"t":215,"s":[100,50,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[16,16],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.196078434587,0.588235318661,0.929411768913,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":2,"op":216,"st":-13,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":"star a 4","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":6,"s":[0]},{"t":245,"s":[2880]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":6,"s":[376.929,-28,0],"to":[-213.333,157.667,0],"ti":[173.333,-127.667,0]},{"t":245,"s":[116.929,834,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":6,"s":[50,100,100]},{"t":245,"s":[100,50,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"sr","sy":1,"d":1,"pt":{"a":0,"k":5,"ix":3},"p":{"a":0,"k":[0,0],"ix":4},"r":{"a":0,"k":0,"ix":5},"ir":{"a":0,"k":5,"ix":6},"is":{"a":0,"k":0,"ix":8},"or":{"a":0,"k":12,"ix":7},"os":{"a":0,"k":0,"ix":9},"ix":1,"nm":"Polystar Path 1","mn":"ADBE Vector Shape - Star","hd":false},{"ty":"fl","c":{"a":0,"k":[0.929411768913,0.745098054409,0.196078434587,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Polystar 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":6,"op":246,"st":-9,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":"star b 4","sr":1,"ks":{"o":{"a":0,"k":50,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":2,"s":[0]},{"t":125,"s":[1800]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":2,"s":[171.786,-28,0],"to":[0,0,0],"ti":[-161.333,-275.667,0]},{"t":125,"s":[251.786,834,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":2,"s":[50,100,100]},{"t":125,"s":[100,50,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"sr","sy":1,"d":1,"pt":{"a":0,"k":5,"ix":3},"p":{"a":0,"k":[0,0],"ix":4},"r":{"a":0,"k":0,"ix":5},"ir":{"a":0,"k":5,"ix":6},"is":{"a":0,"k":0,"ix":8},"or":{"a":0,"k":12,"ix":7},"os":{"a":0,"k":0,"ix":9},"ix":1,"nm":"Polystar Path 1","mn":"ADBE Vector Shape - Star","hd":false},{"ty":"fl","c":{"a":0,"k":[0.537254929543,0.196078434587,0.929411768913,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Polystar 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":2,"op":126,"st":-13,"bm":0},{"ddd":0,"ind":7,"ty":4,"nm":"rec a 4","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":4,"s":[0]},{"t":185,"s":[2520]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":4,"s":[428.214,-22.292,0],"to":[-167.333,119.667,0],"ti":[-130.667,-315.667,0]},{"t":185,"s":[228.214,839.708,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":4,"s":[50,100,100]},{"t":185,"s":[100,50,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[16,8],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":0,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"fl","c":{"a":0,"k":[0.305882364511,0.831372559071,0.411764711142,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":4,"op":186,"st":-11,"bm":0},{"ddd":0,"ind":8,"ty":4,"nm":"rec b 4","sr":1,"ks":{"o":{"a":0,"k":50,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":6,"s":[0]},{"t":245,"s":[2880]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":6,"s":[223.071,-22.292,0],"to":[0,0,0],"ti":[-92.571,-383.708,0]},{"t":245,"s":[223.071,839.708,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":6,"s":[50,100,100]},{"t":245,"s":[100,50,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[16,8],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":0,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"fl","c":{"a":0,"k":[0.929411768913,0.196078434587,0.784313738346,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":6,"op":246,"st":-9,"bm":0},{"ddd":0,"ind":9,"ty":4,"nm":"square a 4","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":4,"s":[0]},{"t":217,"s":[2520]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":4,"s":[120.5,-26.292,0],"to":[13,430.305,0],"ti":[52.221,-418.892,0]},{"t":217,"s":[198.5,833.708,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":4,"s":[50,100,100]},{"t":217,"s":[100,50,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[16,16],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":0,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"fl","c":{"a":0,"k":[0.588235318661,0.831372559071,0.305882364511,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":4,"op":218,"st":-11,"bm":0},{"ddd":0,"ind":10,"ty":4,"nm":"square b 4","sr":1,"ks":{"o":{"a":0,"k":50,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":8,"s":[0]},{"t":215,"s":[2520]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":8,"s":[274.357,-26.292,0],"to":[157.333,415.667,0],"ti":[22.667,-253.667,0]},{"t":215,"s":[114.357,835.708,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":8,"s":[50,100,100]},{"t":215,"s":[100,50,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[16,16],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":0,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"fl","c":{"a":0,"k":[0.588235318661,0.831372559071,0.305882364511,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":8,"op":216,"st":-7,"bm":0},{"ddd":0,"ind":11,"ty":4,"nm":"streamer a 3","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":3,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[275.178,-173.096,0],"to":[-87.333,413.333,0],"ti":[177.333,-643.333,0]},{"t":185,"s":[219.178,974.904,0]}],"ix":2},"a":{"a":0,"k":[-157,-245,0],"ix":1},"s":{"a":0,"k":[-100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[-1.685,-13.314],[0,-14.907],[0,-15.206],[0,-14.907],[0,-14.907],[0,-15.206],[1.754,-14.206],[-3.934,-9.465]],"o":[[-3.895,8.562],[1.872,14.789],[0,15.206],[0,14.907],[0,14.907],[0,15.206],[0,14.314],[-1.803,14.605],[0,0]],"v":[[-156.5,-406],[-166.5,-367],[-146.5,-327],[-166.5,-286],[-146.5,-246],[-166.5,-206],[-146.5,-165],[-166.5,-127],[-156.5,-84]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.305882364511,0.831372559071,0.803921580315,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[4]},{"t":177,"s":[0.5]}],"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":5,"s":[0]},{"t":177,"s":[100]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":5,"s":[40]},{"t":177,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":186,"st":0,"bm":0},{"ddd":0,"ind":12,"ty":4,"nm":"streamer b 3","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":9,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":11,"s":[199.863,74.545,0],"to":[-13.363,405.455,0],"ti":[179.333,-430.667,0]},{"t":217,"s":[139.863,834.545,0]}],"ix":2},"a":{"a":0,"k":[-157,-245,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[-1.685,-13.314],[0,-14.907],[0,-15.206],[0,-14.907],[0,-14.907],[0,-15.206],[1.754,-14.206],[-3.934,-9.465]],"o":[[-3.895,8.562],[1.872,14.789],[0,15.206],[0,14.907],[0,14.907],[0,15.206],[0,14.314],[-1.803,14.605],[0,0]],"v":[[-156.5,-406],[-166.5,-367],[-146.5,-327],[-166.5,-286],[-146.5,-246],[-166.5,-206],[-146.5,-165],[-166.5,-127],[-156.5,-84]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.929411768913,0.196078434587,0.317647069693,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":11,"s":[4]},{"t":199,"s":[0.5]}],"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":16,"s":[0]},{"t":199,"s":[100]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":11,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":16,"s":[40]},{"t":199,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":11,"op":218,"st":11,"bm":0},{"ddd":0,"ind":13,"ty":4,"nm":"circle a 3","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":2,"s":[0]},{"t":125,"s":[-1800]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":2,"s":[365.643,-26.292,0],"to":[-101.333,75.667,0],"ti":[15.333,-507.667,0]},{"t":125,"s":[165.643,835.708,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":2,"s":[100,50,100]},{"t":125,"s":[50,100,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[16,16],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.305882364511,0.831372559071,0.411764711142,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":2,"op":126,"st":-13,"bm":0},{"ddd":0,"ind":14,"ty":4,"nm":"circle b 3","sr":1,"ks":{"o":{"a":0,"k":50,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":8,"s":[0]},{"t":245,"s":[-2880]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":8,"s":[519.5,-26.292,0],"to":[-138,77.667,0],"ti":[-2,-497.667,0]},{"t":245,"s":[159.5,835.708,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":8,"s":[100,50,100]},{"t":245,"s":[50,100,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[16,16],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.537254929543,0.196078434587,0.929411768913,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":8,"op":246,"st":-7,"bm":0},{"ddd":0,"ind":15,"ty":4,"nm":"star a 3","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":6,"s":[0]},{"t":245,"s":[-2880]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":6,"s":[416.929,-28,0],"to":[-213.333,157.667,0],"ti":[173.333,-127.667,0]},{"t":245,"s":[156.929,834,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":6,"s":[100,50,100]},{"t":245,"s":[50,100,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"sr","sy":1,"d":1,"pt":{"a":0,"k":5,"ix":3},"p":{"a":0,"k":[0,0],"ix":4},"r":{"a":0,"k":0,"ix":5},"ir":{"a":0,"k":5,"ix":6},"is":{"a":0,"k":0,"ix":8},"or":{"a":0,"k":12,"ix":7},"os":{"a":0,"k":0,"ix":9},"ix":1,"nm":"Polystar Path 1","mn":"ADBE Vector Shape - Star","hd":false},{"ty":"fl","c":{"a":0,"k":[0.305882364511,0.831372559071,0.803921580315,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Polystar 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":6,"op":246,"st":-9,"bm":0},{"ddd":0,"ind":16,"ty":4,"nm":"star b 3","sr":1,"ks":{"o":{"a":0,"k":50,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":8,"s":[0]},{"t":155,"s":[-2160]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":8,"s":[211.786,-28,0],"to":[0,0,0],"ti":[-161.333,-275.667,0]},{"t":155,"s":[291.786,834,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":8,"s":[100,50,100]},{"t":155,"s":[50,100,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"sr","sy":1,"d":1,"pt":{"a":0,"k":5,"ix":3},"p":{"a":0,"k":[0,0],"ix":4},"r":{"a":0,"k":0,"ix":5},"ir":{"a":0,"k":5,"ix":6},"is":{"a":0,"k":0,"ix":8},"or":{"a":0,"k":12,"ix":7},"os":{"a":0,"k":0,"ix":9},"ix":1,"nm":"Polystar Path 1","mn":"ADBE Vector Shape - Star","hd":false},{"ty":"fl","c":{"a":0,"k":[0.929411768913,0.196078434587,0.784313738346,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Polystar 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":8,"op":156,"st":-7,"bm":0},{"ddd":0,"ind":17,"ty":4,"nm":"rec a 3","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":4,"s":[0]},{"t":185,"s":[-2160]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":4,"s":[468.214,-22.292,0],"to":[-167.333,119.667,0],"ti":[-130.667,-315.667,0]},{"t":185,"s":[268.214,839.708,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":4,"s":[100,50,100]},{"t":185,"s":[50,100,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[16,8],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":0,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"fl","c":{"a":0,"k":[0.588235318661,0.831372559071,0.305882364511,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":4,"op":186,"st":-11,"bm":0},{"ddd":0,"ind":18,"ty":4,"nm":"rec b 3","sr":1,"ks":{"o":{"a":0,"k":50,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":6,"s":[0]},{"t":245,"s":[-2880]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":6,"s":[263.071,-22.292,0],"to":[0,143.667,0],"ti":[-114.571,-267.708,0]},{"t":245,"s":[263.071,839.708,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":6,"s":[100,50,100]},{"t":245,"s":[50,100,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[16,8],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":0,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"fl","c":{"a":0,"k":[0.929411768913,0.196078434587,0.317647069693,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":6,"op":246,"st":-9,"bm":0},{"ddd":0,"ind":19,"ty":4,"nm":"square a 3","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":2,"s":[0]},{"t":217,"s":[-2520]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":2,"s":[160.5,-26.292,0],"to":[13,430.305,0],"ti":[52.221,-418.892,0]},{"t":217,"s":[238.5,833.708,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":2,"s":[100,50,100]},{"t":217,"s":[50,100,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[16,16],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":0,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"fl","c":{"a":0,"k":[0.960784316063,0.572549045086,0.180392161012,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":2,"op":218,"st":-13,"bm":0},{"ddd":0,"ind":20,"ty":4,"nm":"square b 3","sr":1,"ks":{"o":{"a":0,"k":50,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":4,"s":[0]},{"t":185,"s":[-2160]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":4,"s":[314.357,-26.292,0],"to":[157.333,415.667,0],"ti":[22.667,-253.667,0]},{"t":185,"s":[154.357,835.708,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":4,"s":[100,50,100]},{"t":185,"s":[50,100,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[16,16],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":0,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"fl","c":{"a":0,"k":[0.196078434587,0.588235318661,0.929411768913,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":4,"op":186,"st":-11,"bm":0}]}],"layers":[{"ddd":0,"ind":1,"ty":0,"nm":"cannon (small - left)","refId":"comp_0","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[76,452,0],"ix":2},"a":{"a":0,"k":[0,360,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"w":480,"h":720,"ip":0,"op":300,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":0,"nm":"cannon (small - right)","refId":"comp_2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[533,452,0],"ix":2},"a":{"a":0,"k":[479.994,360,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"w":480,"h":720,"ip":0,"op":300,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":0,"nm":"cannon (small - top)","refId":"comp_3","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[304,408,0],"ix":2},"a":{"a":0,"k":[400,400,0],"ix":1},"s":{"a":0,"k":[102,102,100],"ix":6}},"ao":0,"w":800,"h":800,"ip":0,"op":300,"st":0,"bm":0}],"markers":[]}
\ No newline at end of file
diff --git a/apps/student/src/main/res/anim/ease_in_bottom.xml b/libs/pandares/src/main/res/anim/ease_in_bottom.xml
similarity index 100%
rename from apps/student/src/main/res/anim/ease_in_bottom.xml
rename to libs/pandares/src/main/res/anim/ease_in_bottom.xml
diff --git a/apps/student/src/main/res/anim/ease_in_shrink.xml b/libs/pandares/src/main/res/anim/ease_in_shrink.xml
similarity index 100%
rename from apps/student/src/main/res/anim/ease_in_shrink.xml
rename to libs/pandares/src/main/res/anim/ease_in_shrink.xml
diff --git a/apps/student/src/main/res/anim/expand_from_middle.xml b/libs/pandares/src/main/res/anim/expand_from_middle.xml
similarity index 100%
rename from apps/student/src/main/res/anim/expand_from_middle.xml
rename to libs/pandares/src/main/res/anim/expand_from_middle.xml
diff --git a/apps/student/src/main/res/anim/fab_hide.xml b/libs/pandares/src/main/res/anim/fab_hide.xml
similarity index 100%
rename from apps/student/src/main/res/anim/fab_hide.xml
rename to libs/pandares/src/main/res/anim/fab_hide.xml
diff --git a/apps/student/src/main/res/anim/fab_reveal.xml b/libs/pandares/src/main/res/anim/fab_reveal.xml
similarity index 100%
rename from apps/student/src/main/res/anim/fab_reveal.xml
rename to libs/pandares/src/main/res/anim/fab_reveal.xml
diff --git a/apps/student/src/main/res/anim/fab_rotate_backward.xml b/libs/pandares/src/main/res/anim/fab_rotate_backward.xml
similarity index 100%
rename from apps/student/src/main/res/anim/fab_rotate_backward.xml
rename to libs/pandares/src/main/res/anim/fab_rotate_backward.xml
diff --git a/apps/student/src/main/res/anim/fab_rotate_forward.xml b/libs/pandares/src/main/res/anim/fab_rotate_forward.xml
similarity index 100%
rename from apps/student/src/main/res/anim/fab_rotate_forward.xml
rename to libs/pandares/src/main/res/anim/fab_rotate_forward.xml
diff --git a/apps/student/src/main/res/anim/fade_in_quick.xml b/libs/pandares/src/main/res/anim/fade_in_quick.xml
similarity index 100%
rename from apps/student/src/main/res/anim/fade_in_quick.xml
rename to libs/pandares/src/main/res/anim/fade_in_quick.xml
diff --git a/apps/student/src/main/res/anim/fade_out.xml b/libs/pandares/src/main/res/anim/fade_out.xml
similarity index 100%
rename from apps/student/src/main/res/anim/fade_out.xml
rename to libs/pandares/src/main/res/anim/fade_out.xml
diff --git a/apps/student/src/main/res/anim/fade_out_quick.xml b/libs/pandares/src/main/res/anim/fade_out_quick.xml
similarity index 100%
rename from apps/student/src/main/res/anim/fade_out_quick.xml
rename to libs/pandares/src/main/res/anim/fade_out_quick.xml
diff --git a/apps/student/src/main/res/anim/hs_slide_out_left.xml b/libs/pandares/src/main/res/anim/hs_slide_out_left.xml
similarity index 100%
rename from apps/student/src/main/res/anim/hs_slide_out_left.xml
rename to libs/pandares/src/main/res/anim/hs_slide_out_left.xml
diff --git a/apps/student/src/main/res/anim/none.xml b/libs/pandares/src/main/res/anim/none.xml
similarity index 100%
rename from apps/student/src/main/res/anim/none.xml
rename to libs/pandares/src/main/res/anim/none.xml
diff --git a/apps/student/src/main/res/anim/rotate.xml b/libs/pandares/src/main/res/anim/rotate.xml
similarity index 100%
rename from apps/student/src/main/res/anim/rotate.xml
rename to libs/pandares/src/main/res/anim/rotate.xml
diff --git a/apps/student/src/main/res/anim/rotate_back.xml b/libs/pandares/src/main/res/anim/rotate_back.xml
similarity index 100%
rename from apps/student/src/main/res/anim/rotate_back.xml
rename to libs/pandares/src/main/res/anim/rotate_back.xml
diff --git a/apps/student/src/main/res/anim/scale_slide_in_bottom.xml b/libs/pandares/src/main/res/anim/scale_slide_in_bottom.xml
similarity index 100%
rename from apps/student/src/main/res/anim/scale_slide_in_bottom.xml
rename to libs/pandares/src/main/res/anim/scale_slide_in_bottom.xml
diff --git a/apps/student/src/main/res/anim/scale_slide_in_bottom_slow.xml b/libs/pandares/src/main/res/anim/scale_slide_in_bottom_slow.xml
similarity index 100%
rename from apps/student/src/main/res/anim/scale_slide_in_bottom_slow.xml
rename to libs/pandares/src/main/res/anim/scale_slide_in_bottom_slow.xml
diff --git a/apps/student/src/main/res/anim/shrink_to_middle.xml b/libs/pandares/src/main/res/anim/shrink_to_middle.xml
similarity index 100%
rename from apps/student/src/main/res/anim/shrink_to_middle.xml
rename to libs/pandares/src/main/res/anim/shrink_to_middle.xml
diff --git a/apps/student/src/main/res/anim/slide_in_from_bottom.xml b/libs/pandares/src/main/res/anim/slide_in_from_bottom.xml
similarity index 100%
rename from apps/student/src/main/res/anim/slide_in_from_bottom.xml
rename to libs/pandares/src/main/res/anim/slide_in_from_bottom.xml
diff --git a/apps/student/src/main/res/anim/slide_in_right.xml b/libs/pandares/src/main/res/anim/slide_in_right.xml
similarity index 100%
rename from apps/student/src/main/res/anim/slide_in_right.xml
rename to libs/pandares/src/main/res/anim/slide_in_right.xml
diff --git a/apps/student/src/main/res/anim/slide_out_to_bottom.xml b/libs/pandares/src/main/res/anim/slide_out_to_bottom.xml
similarity index 100%
rename from apps/student/src/main/res/anim/slide_out_to_bottom.xml
rename to libs/pandares/src/main/res/anim/slide_out_to_bottom.xml
diff --git a/apps/student/src/main/res/anim/slow_push_left_in.xml b/libs/pandares/src/main/res/anim/slow_push_left_in.xml
similarity index 100%
rename from apps/student/src/main/res/anim/slow_push_left_in.xml
rename to libs/pandares/src/main/res/anim/slow_push_left_in.xml
diff --git a/apps/student/src/main/res/anim/slow_push_left_out.xml b/libs/pandares/src/main/res/anim/slow_push_left_out.xml
similarity index 100%
rename from apps/student/src/main/res/anim/slow_push_left_out.xml
rename to libs/pandares/src/main/res/anim/slow_push_left_out.xml
diff --git a/apps/student/src/main/res/anim/slow_push_right_in.xml b/libs/pandares/src/main/res/anim/slow_push_right_in.xml
similarity index 100%
rename from apps/student/src/main/res/anim/slow_push_right_in.xml
rename to libs/pandares/src/main/res/anim/slow_push_right_in.xml
diff --git a/apps/student/src/main/res/anim/slow_push_right_out.xml b/libs/pandares/src/main/res/anim/slow_push_right_out.xml
similarity index 100%
rename from apps/student/src/main/res/anim/slow_push_right_out.xml
rename to libs/pandares/src/main/res/anim/slow_push_right_out.xml
diff --git a/apps/student/src/main/res/anim/up_from_bottom.xml b/libs/pandares/src/main/res/anim/up_from_bottom.xml
similarity index 100%
rename from apps/student/src/main/res/anim/up_from_bottom.xml
rename to libs/pandares/src/main/res/anim/up_from_bottom.xml
diff --git a/libs/pandares/src/main/res/drawable/avatar_circular_border_thick.xml b/libs/pandares/src/main/res/drawable/avatar_circular_border_thick.xml
new file mode 100644
index 0000000000..f05c70e9b2
--- /dev/null
+++ b/libs/pandares/src/main/res/drawable/avatar_circular_border_thick.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/libs/pandares/src/main/res/drawable/avatar_circular_border_thin.xml b/libs/pandares/src/main/res/drawable/avatar_circular_border_thin.xml
new file mode 100644
index 0000000000..a8e278dbf1
--- /dev/null
+++ b/libs/pandares/src/main/res/drawable/avatar_circular_border_thin.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/libs/pandares/src/main/res/drawable/ic_upload.xml b/libs/pandares/src/main/res/drawable/ic_upload.xml
new file mode 100644
index 0000000000..eea4fa9ffd
--- /dev/null
+++ b/libs/pandares/src/main/res/drawable/ic_upload.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/apps/student/src/main/res/drawable/upload_file_bg.xml b/libs/pandares/src/main/res/drawable/upload_file_bg.xml
similarity index 100%
rename from apps/student/src/main/res/drawable/upload_file_bg.xml
rename to libs/pandares/src/main/res/drawable/upload_file_bg.xml
diff --git a/libs/pandares/src/main/res/values-ar/strings.xml b/libs/pandares/src/main/res/values-ar/strings.xml
index 0eba1ea79a..45ca8106b6 100644
--- a/libs/pandares/src/main/res/values-ar/strings.xml
+++ b/libs/pandares/src/main/res/values-ar/strings.xml
@@ -993,7 +993,6 @@
عقوبة التأخير (-%s)
الدرجة النهائية
- أكثر من 99
أكثر من 99
الفتح في عرض ويب
@@ -1316,6 +1315,30 @@
نسق التطبيق
فاتح
داكن
- النظام الافتراضي
-
+ مثل الجهاز
+ Canvas متاح الآن في النسق الداكن
+ اختر نسق التطبيق
+ النسق الفاتح
+ النسق الداكن
+ مثل نسق الجهاز
+ حفظ
+ يمكنك تغييرها لاحقًا في إعدادات التطبيق
+ تحديد
+ تحميل ملف
+
+
+ - %s من الرسائل غير المقروءة
+ - %s رسالة غير مقروءة
+ - %s من الرسائل غير المقروءة
+ - %s من الرسائل غير المقروءة
+ - %s من الرسائل غير المقروءة
+ - %s من الرسائل غير المقروءة
+
+ لم يتم تحديد تعليق توضيحي
+ إعلامات البريد الإلكتروني
+ فورًا
+ يوميًا
+ أسبوعيًا
+ أبدًا
+ حدد التكرار
diff --git a/libs/pandares/src/main/res/values-b+da+instk12/strings.xml b/libs/pandares/src/main/res/values-b+da+instk12/strings.xml
index bcb582c646..bc5755d4a8 100644
--- a/libs/pandares/src/main/res/values-b+da+instk12/strings.xml
+++ b/libs/pandares/src/main/res/values-b+da+instk12/strings.xml
@@ -958,7 +958,6 @@
Straf for sen aflevering (-%s)
Endelig vurdering
- 99+
99+
Åbner i webvisning
@@ -1261,6 +1260,26 @@
App-tema
Lys
Mørk
- Systemstandard
-
+ Samme som enhed
+ Canvas er nu tilgængelig i mørkt tema
+ Vælg app-tema
+ Lyst tema
+ Mørkt tema
+ Samme tema som på enhed
+ Gem
+ Du kan ændre det senere i app-indstillinger
+ Tag fat
+ Filoverførsel
+
+
+ - %s ulæst besked
+ - %s ulæste beskeder
+
+ Der er ikke valgt nogen anmærkning
+ E-mail-meddelelser
+ Straks
+ Dagligt
+ Ugentlig
+ Aldrig
+ Vælg frekvens
diff --git a/libs/pandares/src/main/res/values-b+en+AU+unimelb/strings.xml b/libs/pandares/src/main/res/values-b+en+AU+unimelb/strings.xml
index b494aa11a8..e41ad96893 100644
--- a/libs/pandares/src/main/res/values-b+en+AU+unimelb/strings.xml
+++ b/libs/pandares/src/main/res/values-b+en+AU+unimelb/strings.xml
@@ -958,7 +958,6 @@
Late penalty (-%s)
Final Grade
- 99+
99+
Opens in webview
@@ -1261,6 +1260,26 @@
App Theme
Light
Dark
- System default
-
+ Same as device
+ Canvas is now available in dark theme
+ Choose app theme
+ Light theme
+ Dark theme
+ Same as device theme
+ Save
+ You can change it later in app settings
+ Grab
+ File Upload
+
+
+ - %s unread message
+ - %s unread messages
+
+ No annotation selected
+ Email Notifications
+ Immediately
+ Daily
+ Weekly
+ Never
+ Select frequency
diff --git a/libs/pandares/src/main/res/values-b+en+GB+instukhe/strings.xml b/libs/pandares/src/main/res/values-b+en+GB+instukhe/strings.xml
index e8c43256d2..d1847bc686 100644
--- a/libs/pandares/src/main/res/values-b+en+GB+instukhe/strings.xml
+++ b/libs/pandares/src/main/res/values-b+en+GB+instukhe/strings.xml
@@ -958,7 +958,6 @@
Late penalty (-%s)
Final grade
- 99+
99+
Opens in webview
@@ -1261,6 +1260,26 @@
App Theme
Light
Dark
- System default
-
+ Same as device
+ Canvas is now available in dark theme
+ Choose app theme
+ Light theme
+ Dark theme
+ Same as device theme
+ Save
+ You can change it later in app settings
+ Grab
+ File upload
+
+
+ - %s unread message
+ - %s unread messages
+
+ No annotation selected
+ Email Notifications
+ Immediately
+ Daily
+ Weekly
+ Never
+ Select frequency
diff --git a/libs/pandares/src/main/res/values-b+nb+instk12/strings.xml b/libs/pandares/src/main/res/values-b+nb+instk12/strings.xml
index fd918c305e..d0a4cfc8e7 100644
--- a/libs/pandares/src/main/res/values-b+nb+instk12/strings.xml
+++ b/libs/pandares/src/main/res/values-b+nb+instk12/strings.xml
@@ -958,7 +958,6 @@
Forsinkelsesstraff (-%s)
Sluttvurdering
- 99+
99+
Åpne i nettvisning
@@ -1262,6 +1261,26 @@
App-tema
Lys
Mørk
- Systemstandard
-
+ Samme som enhet
+ Canvas er nå tilgjengelig med mørkt tema
+ Velg app-tema
+ Lyst tema
+ Mørkt tema
+ Samme som enhetstema
+ Lagre
+ Du kan endre det senere i appinnstillinger
+ Grip
+ Filopplasting
+
+
+ - %s uleste melding
+ - %s uleste meldinger
+
+ Ingen merknad valgt
+ E-post varslinger
+ Umiddelbart
+ Daglig
+ Ukentlig
+ Aldri
+ Velg frekvens
diff --git a/libs/pandares/src/main/res/values-b+sv+instk12/strings.xml b/libs/pandares/src/main/res/values-b+sv+instk12/strings.xml
index 5ad94b0667..2df3d9187b 100644
--- a/libs/pandares/src/main/res/values-b+sv+instk12/strings.xml
+++ b/libs/pandares/src/main/res/values-b+sv+instk12/strings.xml
@@ -958,7 +958,6 @@
Förseningsbestraffning (-%s)
Totalt omdöme
- 99+
99+
Öppnas i webbvy
@@ -1261,6 +1260,26 @@
Apptema
Ljus
Mörk
- Systemstandard
-
+ Samma som enhet
+ Canvas finns inte tillgängligt med ett mörkt tema
+ Välj apptema
+ Ljust tema
+ Mörkt tema
+ Samma tema som enheten
+ Spara
+ Du kan ändra det sedan i appinställningarna
+ Ta tag i
+ Filuppladdning
+
+
+ - %s oläst meddelande
+ - %s olästa meddelanden
+
+ Inga noteringar har valts
+ E-postaviseringar
+ Omedelbart
+ Varje dag
+ Veckovis
+ Aldrig
+ Välj frekvens
diff --git a/libs/pandares/src/main/res/values-zh-rHK/strings.xml b/libs/pandares/src/main/res/values-b+zh+HK/strings.xml
similarity index 98%
rename from libs/pandares/src/main/res/values-zh-rHK/strings.xml
rename to libs/pandares/src/main/res/values-b+zh+HK/strings.xml
index 49c9019e06..159b81df15 100644
--- a/libs/pandares/src/main/res/values-zh-rHK/strings.xml
+++ b/libs/pandares/src/main/res/values-b+zh+HK/strings.xml
@@ -949,7 +949,6 @@
逾期懲罰 (-%s)
最終評分
- 99+
99+
在網頁中打開
@@ -1247,6 +1246,25 @@
應用程式主題
亮色
暗色
- 系統預設
-
+ 和裝置相同
+ Canvas 現在可在暗色主題中使用
+ 選擇應用程式主題
+ 淡色主題
+ 暗色主題
+ 和裝置主題相同
+ 儲存
+ 您可以稍後在應用桯式設定中變更
+ 擷取
+ 檔案上傳
+
+
+ - %s 條未讀訊息
+
+ 未選擇任何註釋
+ 電子郵件通知
+ 立即
+ 每天
+ 每週
+ 永不
+ 選擇頻率
diff --git a/libs/pandares/src/main/res/values-b+zh+Hans/strings.xml b/libs/pandares/src/main/res/values-b+zh+Hans/strings.xml
index 30e9cbeba0..e171c021d5 100644
--- a/libs/pandares/src/main/res/values-b+zh+Hans/strings.xml
+++ b/libs/pandares/src/main/res/values-b+zh+Hans/strings.xml
@@ -949,7 +949,6 @@
迟交罚分 (-%s)
最终评分
- 99+
99+
在网页视图中打开
@@ -1247,6 +1246,25 @@
应用主题
浅色的、轻的
深色的、黑的
- 系统默认
-
+ 与设备相同
+ Canvas 已推出深色主题
+ 选择应用程序主题
+ 浅色主题
+ 深色主题
+ 与设备主题相同
+ 保存
+ 您可以后在应用设置中更改
+ 抓取图像
+ 文件上传
+
+
+ - %s条未读消息
+
+ 未选择任何批注
+ 电子邮件通知
+ 立即
+ 每天
+ 每周
+ 从不
+ 选择频率
diff --git a/libs/pandares/src/main/res/values-b+zh+Hant/strings.xml b/libs/pandares/src/main/res/values-b+zh+Hant/strings.xml
index 49c9019e06..159b81df15 100644
--- a/libs/pandares/src/main/res/values-b+zh+Hant/strings.xml
+++ b/libs/pandares/src/main/res/values-b+zh+Hant/strings.xml
@@ -949,7 +949,6 @@
逾期懲罰 (-%s)
最終評分
- 99+
99+
在網頁中打開
@@ -1247,6 +1246,25 @@
應用程式主題
亮色
暗色
- 系統預設
-
+ 和裝置相同
+ Canvas 現在可在暗色主題中使用
+ 選擇應用程式主題
+ 淡色主題
+ 暗色主題
+ 和裝置主題相同
+ 儲存
+ 您可以稍後在應用桯式設定中變更
+ 擷取
+ 檔案上傳
+
+
+ - %s 條未讀訊息
+
+ 未選擇任何註釋
+ 電子郵件通知
+ 立即
+ 每天
+ 每週
+ 永不
+ 選擇頻率
diff --git a/libs/pandares/src/main/res/values-ca/strings.xml b/libs/pandares/src/main/res/values-ca/strings.xml
index 8509046ba7..cfef1939cb 100644
--- a/libs/pandares/src/main/res/values-ca/strings.xml
+++ b/libs/pandares/src/main/res/values-ca/strings.xml
@@ -958,7 +958,6 @@
Sanció per endarreriment (-%s)
Nota final
- 99+
99+
S\'obre en vista web
@@ -1262,6 +1261,26 @@
Tema de l’aplicació
Clar
Fosc
- Valor predeterminat del sistema
-
+ El mateix que el dispositiu
+ Ara, el Canvas està disponible en tema fosc
+ Trieu el tema de l’aplicació
+ Tema clar
+ Tema fosc
+ El mateix tema que el del dispositiu
+ Desa
+ Podeu canviar-lo més endavant a la configuració de l’aplicació
+ Agafa
+ Penjada de fitxer
+
+
+ - %s missatge sense llegir
+ - %s missatges sense llegir
+
+ No hi ha cap anotació seleccionada
+ Notificacions per correu electrònic
+ Immediatament
+ Diari
+ Setmanal
+ Mai
+ Seleccioneu la freqüència
diff --git a/libs/pandares/src/main/res/values-cy/strings.xml b/libs/pandares/src/main/res/values-cy/strings.xml
index ef9bafd0a4..0fb957bed7 100644
--- a/libs/pandares/src/main/res/values-cy/strings.xml
+++ b/libs/pandares/src/main/res/values-cy/strings.xml
@@ -958,7 +958,6 @@
Cosb am fod yn hwyr (-%s)
Gradd Derfynol
- 99+
99+
Agor yn webview
@@ -1114,8 +1113,8 @@
Dyddiad Anhysbys
%s. minws
%s.
- %s %s
- %s %s, %s
+ %1$s %2$s
+ %1$s %2$s, %3$s
- %s Munud
@@ -1127,8 +1126,8 @@
Does dim cynadleddau i’w dangos eto
Does dim disgrifiad ar gyfer y gynhadledd hon
Gwall wrth lwytho’ch cynadleddau
- Wedi dirwyn i ben %s am %s
- Wedi dechrau %s am %s
+ Wedi dirwyn i ben %1$s am %2$s
+ Wedi dechrau %1$s am %2$s
Heb ei ddechrau
Ar y Gweill
Ymuno
@@ -1139,7 +1138,7 @@
Cynhadledd ar waith
Sgoriau maen prawf %s
- %s, %s
+ %1$s, %2$s
rhagor o wybodaeth
Botymau Creu Ffeil a Creu Ffolder yn weladwy
Botymau Creu Ffeil a Creu Ffolder yn gudd
@@ -1261,6 +1260,26 @@
Thema Ap
Golau
Tywyll
- Rhagosodiad system
-
+ Yn un fath a’r ddyfais
+ Mae Canvas bellach ar gael mewn thema dywyll
+ Dewis thema ap
+ Thema golau
+ Thema dywyll
+ Yr un fath a thema’r ddyfais
+ Cadw
+ Gallwch chi ei newid yn nes ymlaen yng ngosodiadau’r ap
+ Gafael
+ Llwytho Ffeil i Fyny
+
+
+ - %s neges heb ei darllen
+ - %s neges heb ei darllen
+
+ Dim anodiad wedi’i ddewis
+ Hysbysiadau E-bost
+ Yn syth
+ Pob dydd
+ Pob wythnos
+ Byth
+ Dewis amlder
diff --git a/libs/pandares/src/main/res/values-da/strings.xml b/libs/pandares/src/main/res/values-da/strings.xml
index 47a16ce4c7..23cddf6150 100644
--- a/libs/pandares/src/main/res/values-da/strings.xml
+++ b/libs/pandares/src/main/res/values-da/strings.xml
@@ -958,7 +958,6 @@
Straf for sen aflevering (-%s)
Endelig karakter
- 99+
99+
Åbner i webvisning
@@ -1261,6 +1260,26 @@
App-tema
Lys
Mørk
- Systemstandard
-
+ Samme som enhed
+ Canvas er nu tilgængelig i mørkt tema
+ Vælg app-tema
+ Lyst tema
+ Mørkt tema
+ Samme tema som på enhed
+ Gem
+ Du kan ændre det senere i app-indstillinger
+ Tag fat
+ Filoverførsel
+
+
+ - %s ulæst besked
+ - %s ulæste beskeder
+
+ Der er ikke valgt nogen anmærkning
+ E-mail-meddelelser
+ Straks
+ Dagligt
+ Ugentlig
+ Aldrig
+ Vælg frekvens
diff --git a/libs/pandares/src/main/res/values-de/strings.xml b/libs/pandares/src/main/res/values-de/strings.xml
index 88ad68c03b..447e9656d8 100644
--- a/libs/pandares/src/main/res/values-de/strings.xml
+++ b/libs/pandares/src/main/res/values-de/strings.xml
@@ -113,7 +113,7 @@
%1$s von %2$s Punkten
Hypothetische Punktzahl eingeben
Gering: %s
- Mittel: %s
+ Mittelwert: %s
Hoch: %s
@@ -484,7 +484,7 @@
Entschuldigung. Sie sind nicht zu Ankündigungen in diesem Kurs berechtigt.
Entschuldigung. Sie sind nicht zu Diskussionen in diesem Kurs berechtigt.
- Announcements
+ Ankündigungen
Der Titel darf nicht leer sein.
@@ -733,7 +733,7 @@
Neues Ereignis erstellen
Panda ausstellen
- Announcements
+ Ankündigungen
Konto hinzufügen
Benutzer wechseln
@@ -797,7 +797,7 @@
Benachrichtigung erhalten, wenn es eine neue Ankündigung in Ihrem Kurs gibt.
Benachrichtigung erhalten, wenn jemand auf eine Ankündigung von Ihnen antwortet.
Benachrichtigung erhalten, wenn eine Aufgabe/Abgabe benotet/geändert wurde und wenn eine Notengewichtung geändert wurde.
- Benachrichtigung erhalten bei Einladungen zu Webkonferenzen, Gruppen, Kooperationen, Bewertung durch Mitstudenten und Erinnerungen.
+ Benachrichtigung erhalten bei Einladungen zu Webkonferenzen, Gruppen, Kooperationen, Peer Review und Erinnerungen.
Nur für Kursleiter und Administratoren. Benachrichtigung erhalten, wenn eine Aufgabe abgegeben oder erneut abgegeben wird.
Nur für Kursleiter und Administratoren. Benachrichtigung erhalten, wenn eine Aufgabe zu spät abgegeben wird.
Benachrichtigung erhalten, wenn zu Ihrer Einreichung ein Kommentar abgegeben wird.
@@ -958,7 +958,6 @@
Strafe für Verspätung (-%s)
Gesamtnote
- über 99
über 99
In Webview öffnen
@@ -999,7 +998,7 @@
Konferenzen werden auf Mobilgeräten noch nicht unterstützt.
Dateivorschaubild
Fehler beim Versuch diese PDF-Datei zu laden
- Tut uns sehr leid! Diese Funktion steht für die Studentenansicht nicht erlaubt.
+ Tut uns sehr leid! Diese Funktion steht für die Studierendenansicht nicht erlaubt.
Hier gibt es nichts zu sehen
Nicht unterstützte Funktion
@@ -1042,7 +1041,7 @@
Note außer Kraft setzen
Aktuelle Note
Öffnet in Canvas Student
- Studentenansicht
+ Studierendenansicht
@@ -1233,7 +1232,7 @@
Tippen Sie hier, um fortzufahren.
Entwurf verfügbar
Fehler bei, Laden der Abgabe
- Tippen Sie hier, um den vollständigen Content anzuzeigen
+ Tippen Sie hier, um den vollständigen Inhalt anzuzeigen
Wichtige Termine
Keine wichtigen Termine
Wichtige Termine
@@ -1261,6 +1260,26 @@
App-Design
Hell
Dunkel
- Systemstandard
-
+ Wie Gerät
+ Canvas ist jetzt in dunklem Design verfügbar
+ App-Design auswählen
+ Helles Design
+ Dunkles Design
+ Entspricht dem Gerätedesign
+ Speichern
+ Sie können es später in den App-Einstellungen ändern.
+ Greifen
+ Datei hochladen
+
+
+ - %s ungelesene Nachricht
+ - %s ungelesene Nachrichten
+
+ Keine Anmerkung ausgewählt
+ E-Mail-Benachrichtigungen
+ Sofort
+ Täglich
+ Wöchentlich
+ Nie
+ Frequenz auswählen
diff --git a/libs/pandares/src/main/res/values-en-rAU/strings.xml b/libs/pandares/src/main/res/values-en-rAU/strings.xml
index 5e317cf5e8..5482cbdcca 100644
--- a/libs/pandares/src/main/res/values-en-rAU/strings.xml
+++ b/libs/pandares/src/main/res/values-en-rAU/strings.xml
@@ -958,7 +958,6 @@
Late penalty (-%s)
Final Grade
- 99+
99+
Opens in webview
@@ -1261,6 +1260,26 @@
App Theme
Light
Dark
- System default
-
+ Same as device
+ Canvas is now available in dark theme
+ Choose app theme
+ Light theme
+ Dark theme
+ Same as device theme
+ Save
+ You can change it later in app settings
+ Grab
+ File Upload
+
+
+ - %s unread message
+ - %s unread messages
+
+ No annotation selected
+ Email Notifications
+ Immediately
+ Daily
+ Weekly
+ Never
+ Select frequency
diff --git a/libs/pandares/src/main/res/values-en-rCA/strings.xml b/libs/pandares/src/main/res/values-en-rCA/strings.xml
index 8d0d17f9a4..a013dfa5e7 100644
--- a/libs/pandares/src/main/res/values-en-rCA/strings.xml
+++ b/libs/pandares/src/main/res/values-en-rCA/strings.xml
@@ -1,4 +1,4 @@
-
diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/activities/BaseViewMediaActivity.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/activities/BaseViewMediaActivity.kt
index 07daa4f4c6..eb3d13f41e 100644
--- a/libs/pandautils/src/main/java/com/instructure/pandautils/activities/BaseViewMediaActivity.kt
+++ b/libs/pandautils/src/main/java/com/instructure/pandautils/activities/BaseViewMediaActivity.kt
@@ -192,7 +192,7 @@ abstract class BaseViewMediaActivity : AppCompatActivity() {
private fun prepare() = mExoAgent.prepare(mediaPlayerView)
override fun onPause() {
- if (!mDestroyOnExit) mExoAgent.flagForResume()
+ mExoAgent.flagForResume()
super.onPause()
}
diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/binding/BindableSpinnerAdapter.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/binding/BindableSpinnerAdapter.kt
new file mode 100644
index 0000000000..e467c0ee66
--- /dev/null
+++ b/libs/pandautils/src/main/java/com/instructure/pandautils/binding/BindableSpinnerAdapter.kt
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2022 - present Instructure, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.instructure.pandautils.binding
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.ArrayAdapter
+import androidx.annotation.IntegerRes
+import androidx.annotation.LayoutRes
+import androidx.databinding.DataBindingUtil
+import androidx.databinding.ViewDataBinding
+import com.instructure.canvasapi2.models.Course
+import com.instructure.pandautils.BR
+import com.instructure.pandautils.mvvm.ItemViewModel
+
+class BindableSpinnerAdapter(
+ context: Context,
+ @LayoutRes private val viewResource: Int,
+ private var itemViewModels: List
+) : ArrayAdapter(context, viewResource, itemViewModels) {
+
+ fun setCourses(itemViewModels: List) {
+ this.itemViewModels = itemViewModels
+ notifyDataSetChanged()
+ }
+
+ override fun getCount(): Int = itemViewModels.size
+
+ @SuppressLint("InflateParams")
+ override fun getDropDownView(position: Int, convertView: View?, parent: ViewGroup): View {
+ val binding: ViewDataBinding = DataBindingUtil.inflate(
+ LayoutInflater.from(parent.context), viewResource, parent, false
+ )
+ binding.setVariable(BR.itemViewModel, itemViewModels[position])
+ return binding.root
+ }
+
+ @SuppressLint("InflateParams")
+ override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
+ val binding: ViewDataBinding = DataBindingUtil.inflate(
+ LayoutInflater.from(parent.context), viewResource, parent, false
+ )
+ binding.setVariable(BR.itemViewModel, itemViewModels[position])
+ return binding.root
+ }
+}
\ No newline at end of file
diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/binding/BindingAdapters.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/binding/BindingAdapters.kt
index 8d01564e71..cc68d61c78 100644
--- a/libs/pandautils/src/main/java/com/instructure/pandautils/binding/BindingAdapters.kt
+++ b/libs/pandautils/src/main/java/com/instructure/pandautils/binding/BindingAdapters.kt
@@ -18,13 +18,19 @@ package com.instructure.pandautils.binding
import android.graphics.Bitmap
import android.graphics.drawable.GradientDrawable
+import android.graphics.drawable.ShapeDrawable
+import android.graphics.drawable.shapes.OvalShape
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.accessibility.AccessibilityNodeInfo
import android.webkit.JavascriptInterface
+import android.widget.AdapterView
import android.widget.ImageView
+import android.widget.Spinner
+import androidx.annotation.ColorInt
import androidx.annotation.DrawableRes
+import androidx.annotation.LayoutRes
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.content.ContextCompat
import androidx.databinding.BindingAdapter
@@ -130,8 +136,8 @@ fun bindImageWithOverlay(imageView: ImageView, imageUrl: String?, overlayColor:
}
} else {
Glide.with(imageView)
- .load(imageUrl)
- .into(imageView)
+ .load(imageUrl)
+ .into(imageView)
}
}
@@ -146,6 +152,7 @@ fun addBorderToContainer(view: View, borderColor: Int?, borderWidth: Int?, backg
border.cornerRadius = borderCornerRadius?.toPx?.toFloat() ?: 4.toPx.toFloat()
view.background = border
}
+
@BindingAdapter("layout_constraintWidth_percent")
fun bindConstraintWidthPercentage(view: View, percentage: Float) {
val params = view.layoutParams as ConstraintLayout.LayoutParams
@@ -155,7 +162,9 @@ fun bindConstraintWidthPercentage(view: View, percentage: Float) {
@BindingAdapter("imageRes")
fun bindImageResource(imageView: ImageView, @DrawableRes imageRes: Int) {
- imageView.setImageDrawable(ContextCompat.getDrawable(imageView.context, imageRes))
+ if (imageRes != 0) {
+ imageView.setImageDrawable(ContextCompat.getDrawable(imageView.context, imageRes))
+ }
}
@BindingAdapter("bitmap")
@@ -182,7 +191,7 @@ fun bindAccesibilityDelegate(view: View, clickDescription: String) {
fun setBottomMargin(view: View, bottomMargin: Int) {
val layoutParams: ViewGroup.MarginLayoutParams = view.layoutParams as ViewGroup.MarginLayoutParams
layoutParams.setMargins(layoutParams.leftMargin, layoutParams.topMargin,
- layoutParams.rightMargin, bottomMargin)
+ layoutParams.rightMargin, bottomMargin)
view.layoutParams = layoutParams
}
@@ -204,3 +213,19 @@ fun bindUrl(canvasWebView: CanvasWebView, url: String?) {
canvasWebView.loadUrl(it)
}
}
+
+@BindingAdapter(value = ["itemViewModels", "layoutRes"], requireAll = false)
+fun bindSpinner(spinner: Spinner, itemViewModels: List?, @LayoutRes layoutRes: Int) {
+ itemViewModels?.let {
+ spinner.adapter = BindableSpinnerAdapter(
+ spinner.context,
+ layoutRes,
+ itemViewModels
+ )
+ }
+}
+
+@BindingAdapter("ovalColor")
+fun bindOvalColorBackground(imageView: ImageView, @ColorInt ovalColor: Int) {
+ imageView.background = ShapeDrawable(OvalShape()).apply { paint.color = ovalColor }
+}
diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/di/ApplicationModule.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/di/ApplicationModule.kt
index 693a6693e2..265e30db49 100644
--- a/libs/pandautils/src/main/java/com/instructure/pandautils/di/ApplicationModule.kt
+++ b/libs/pandautils/src/main/java/com/instructure/pandautils/di/ApplicationModule.kt
@@ -17,8 +17,10 @@
package com.instructure.pandautils.di
import android.app.NotificationManager
+import android.content.ContentResolver
import android.content.Context
import android.content.res.Resources
+import androidx.work.WorkManager
import com.google.firebase.crashlytics.FirebaseCrashlytics
import com.instructure.canvasapi2.managers.OAuthManager
import com.instructure.pandautils.typeface.TypefaceBehavior
@@ -70,4 +72,16 @@ class ApplicationModule {
fun provideNotificationManager(@ApplicationContext context: Context): NotificationManager {
return context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
}
+
+ @Provides
+ @Singleton
+ fun provideContentResolver(@ApplicationContext context: Context): ContentResolver {
+ return context.contentResolver
+ }
+
+ @Provides
+ @Singleton
+ fun provideWorkManager(@ApplicationContext context: Context): WorkManager {
+ return WorkManager.getInstance(context)
+ }
}
\ No newline at end of file
diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/di/FileUploadModule.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/di/FileUploadModule.kt
new file mode 100644
index 0000000000..1d337f32d9
--- /dev/null
+++ b/libs/pandautils/src/main/java/com/instructure/pandautils/di/FileUploadModule.kt
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2022 - present Instructure, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.instructure.pandautils.di
+
+import android.content.ContentResolver
+import android.content.Context
+import com.instructure.pandautils.features.file.upload.FileUploadUtilsHelper
+import com.instructure.pandautils.features.file.upload.preferences.FileUploadPreferences
+import com.instructure.pandautils.features.file.upload.worker.FileUploadBundleCreator
+import com.instructure.pandautils.utils.FileUploadUtils
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.android.qualifiers.ApplicationContext
+import dagger.hilt.components.SingletonComponent
+
+@Module
+@InstallIn(SingletonComponent::class)
+class FileUploadModule {
+
+ @Provides
+ fun provideFileUploadUtils(): FileUploadUtils {
+ return FileUploadUtils
+ }
+
+ @Provides
+ fun provideFileUploadUtilsHelper(@ApplicationContext context: Context, contentResolver: ContentResolver, fileUploadUtils: FileUploadUtils): FileUploadUtilsHelper {
+ return FileUploadUtilsHelper(fileUploadUtils, context, contentResolver)
+ }
+
+ @Provides
+ fun provideFileUploadBundleCreator(): FileUploadBundleCreator {
+ return FileUploadBundleCreator()
+ }
+
+ @Provides
+ fun provideFileUploadPreferences(): FileUploadPreferences {
+ return FileUploadPreferences
+ }
+}
\ No newline at end of file
diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/dialogs/UploadFilesDialog.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/dialogs/UploadFilesDialog.kt
deleted file mode 100644
index 0ff2650843..0000000000
--- a/libs/pandautils/src/main/java/com/instructure/pandautils/dialogs/UploadFilesDialog.kt
+++ /dev/null
@@ -1,779 +0,0 @@
-/*
- * Copyright (C) 2018 - 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.pandautils.dialogs
-
-import android.app.Activity
-import android.app.Dialog
-import android.content.Context
-import android.content.DialogInterface
-import android.content.Intent
-import android.content.pm.PackageManager
-import android.content.res.Configuration
-import android.net.Uri
-import android.os.Bundle
-import android.provider.MediaStore
-import android.util.TypedValue
-import android.view.*
-import android.widget.TextView
-import android.widget.Toast
-import androidx.appcompat.app.AlertDialog
-import androidx.appcompat.app.AppCompatDialogFragment
-import androidx.core.content.FileProvider
-import androidx.fragment.app.FragmentManager
-import androidx.recyclerview.widget.RecyclerView
-import com.instructure.canvasapi2.models.*
-import com.instructure.canvasapi2.models.postmodels.FileSubmitObject
-import com.instructure.canvasapi2.utils.ApiPrefs
-import com.instructure.canvasapi2.utils.Logger
-import com.instructure.canvasapi2.utils.weave.catch
-import com.instructure.canvasapi2.utils.weave.tryWeave
-import com.instructure.pandautils.R
-import com.instructure.pandautils.analytics.SCREEN_VIEW_UPLOAD_FILES
-import com.instructure.pandautils.analytics.ScreenView
-import com.instructure.pandautils.services.FileUploadService
-import com.instructure.pandautils.utils.*
-import com.instructure.pandautils.views.AttachmentView
-import kotlinx.android.synthetic.main.adapter_file_uploads.view.*
-import kotlinx.android.synthetic.main.dialog_files_upload.*
-import kotlinx.android.synthetic.main.dialog_files_upload.view.*
-import kotlinx.coroutines.Job
-import org.greenrobot.eventbus.EventBus
-import org.greenrobot.eventbus.Subscribe
-import java.io.File
-import java.util.*
-
-@ScreenView(SCREEN_VIEW_UPLOAD_FILES)
-class UploadFilesDialog : AppCompatDialogFragment() {
-
- init {
- retainInstance = true
- }
-
- enum class FileUploadType {
- ASSIGNMENT, COURSE, USER, MESSAGE, DISCUSSION, QUIZ, SUBMISSION_COMMENT, GROUP
- }
-
- private var getUriContentsJob: Job? = null
-
- private var dialogCallback: ((Int) -> Unit)? = null //(Int) -> Unit by Delegates.notNull()
- private var dialogAttachmentCallback: ((Int, FileSubmitObject?) -> Unit)? = null //-> Unit by Delegates.notNull()
-
- private var uploadType: FileUploadType by SerializableArg(FileUploadType.ASSIGNMENT)
- private var canvasContext: CanvasContext by ParcelableArg(ApiPrefs.user)
- private var isOneFileOnly: Boolean by BooleanArg()
- private var position: Int by IntArg()
-
- private var fileListAdapter: FileRecyclerViewAdapter? = null
- private val fileList: ArrayList by ParcelableArrayListArg(ArrayList())
- private var fileSubmitUri: Uri? = null
- private var cameraImageUri: Uri? = null
-
- private var assignment: Assignment? by NullableParcelableArg()
- private var parentFolderId: Long by LongArg()
- private var quizQuestionId: Long by LongArg()
- private var quizId: Long by LongArg()
- private var courseId: Long by LongArg()
-
- private var dialogRootView: View? = null
-
- //region Lifecycle
-
- override fun onStart() {
- EventBus.getDefault().register(this)
- super.onStart()
- // Don't dim the background when the dialog is created.
- dialog?.window?.let {
- val windowParams = it.attributes
- windowParams.dimAmount = 0f
- windowParams.flags = windowParams.flags or WindowManager.LayoutParams.FLAG_DIM_BEHIND
- it.attributes = windowParams
-
- setDialogMargins(it)
- }
- }
-
- override fun onStop() {
- EventBus.getDefault().unregister(this)
- super.onStop()
- }
-
- override fun onConfigurationChanged(newConfig: Configuration) {
- super.onConfigurationChanged(newConfig)
- dialog?.window?.let { setDialogMargins(it) }
- }
-
- override fun onDestroyView() {
- // Fix for rotation bug
- dialog?.let { if (retainInstance) it.setDismissMessage(null) }
- super.onDestroyView()
- }
-
- override fun onActivityCreated(savedInstanceState: Bundle?) {
- super.onActivityCreated(savedInstanceState)
-
- dialog?.let {
- it.window?.attributes?.windowAnimations = R.style.FileUploadDialogAnimation
- it.window?.setWindowAnimations(R.style.FileUploadDialogAnimation)
- }
-
- handleUriContents()
- }
-
- override fun onDestroy() {
- super.onDestroy()
- getUriContentsJob?.cancel()
- }
-
- override fun onCancel(dialog: DialogInterface) {
- super.onCancel(dialog)
- dialogCallback?.invoke(EVENT_DIALOG_CANCELED)
- dialogAttachmentCallback?.invoke(EVENT_DIALOG_CANCELED, null)
- }
-
- @Suppress("unused", "UNUSED_PARAMETER")
- @Subscribe(sticky = true)
- fun onActivityResults(event: OnActivityResults) {
- event.get {
- event.remove()//Remove the event so it doesn't show up again somewhere else.
-
- if(it.resultCode == Activity.RESULT_OK) {
- if (it.requestCode == CAMERA_PIC_REQUEST) {
- // Attempt to restore URI in case were were booted from memory
- if (cameraImageUri == null) cameraImageUri = Uri.parse(FilePrefs.tempCaptureUri)
-
- //if it's still null, tell the user there is an error and return.
- if (cameraImageUri == null) {
- Toast.makeText(activity, R.string.utils_errorGettingPhoto, Toast.LENGTH_SHORT).show()
- return@get
- }
-
- getUriContents(cameraImageUri!!)
- } else if(it.data != null && it.data.data != null) {
- getUriContents(it.data.data!!)
- }
- }
- }
- }
-
- //endregion
-
- //region View Setup
-
- override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
-
- val title: String
- val positiveText: String
-
- // Get dialog headers
- when (uploadType) {
- FileUploadType.ASSIGNMENT -> {
- title = getString(R.string.assignmentHeader) + " " + assignment?.name
- positiveText = getString(R.string.turnIn)
- }
- FileUploadType.COURSE -> {
- title = getString(R.string.utils_uploadTo) + " " + getString(R.string.utils_uploadCourseFiles)
- positiveText = getString(R.string.upload)
- }
- FileUploadType.GROUP -> {
- title = getString(R.string.utils_uploadTo) + " " + getString(R.string.utils_uploadGroupFiles)
- positiveText = getString(R.string.upload)
- }
- FileUploadType.MESSAGE -> {
- title = getString(R.string.utils_attachFile)
- positiveText = getString(R.string.utils_okay)
- }
- FileUploadType.DISCUSSION -> {
- isOneFileOnly = true
- title= getString(R.string.utils_attachFile)
- positiveText = getString(R.string.utils_okay)
- }
- FileUploadType.QUIZ -> {
- isOneFileOnly = true
- title = getString(R.string.utils_uploadTo) + " " + getString(R.string.utils_uploadMyFiles)
- positiveText = getString(R.string.utils_upload)
- }
- FileUploadType.SUBMISSION_COMMENT -> {
- title = getString(R.string.utils_uploadToSubmissionComment)
- positiveText = getString(R.string.utils_upload)
- }
- else -> {
- title = getString(R.string.utils_uploadTo) + " " + getString(R.string.utils_uploadMyFiles)
- positiveText = getString(R.string.utils_upload)
- }
- }
-
- dialogRootView = View.inflate(activity, R.layout.dialog_files_upload, null)
- setupViews(dialogRootView!!)
-
- val dialog = AlertDialog.Builder(requireContext())
- .setTitle(title)
- .setView(dialogRootView)
- .setPositiveButton(positiveText, null)
- .setNegativeButton(R.string.utils_cancel, null)
- .create()
-
- dialog.setCanceledOnTouchOutside(true)
- dialog.setCancelable(true)
- dialog.setOnShowListener {
- val positive = dialog.getButton(DialogInterface.BUTTON_POSITIVE)
- positive.setTextColor(ThemePrefs.buttonColor)
- positive.setOnClickListener { dialogPositiveButtonClick() }
- val negative = dialog.getButton(DialogInterface.BUTTON_NEGATIVE)
- negative.setTextColor(ThemePrefs.buttonColor)
- negative.setOnClickListener {
- onCancel(dialog)
- dismiss()
- }
- }
-
- return dialog
- }
-
- private fun setupViews(view: View) {
- //setup list view
-
- fileListAdapter = FileRecyclerViewAdapter { _, position ->
- fileList.removeAt(position)
- fileListAdapter!!.notifyItemRemoved(position)
- refreshFileButtonsVisibility(view)
- }.apply {
- view.fileRecyclerView.adapter = this
- }
-
- setOnClickListeners(view)
- setupAllowedExtensions(view)
- }
-
- private fun setupAllowedExtensions(view: View) {
- val allowedExtensions = view.findViewById(R.id.allowedExtensions)
- //if there are only certain file types that are allowed, let the user know
- if (uploadType != UploadFilesDialog.FileUploadType.SUBMISSION_COMMENT && assignment != null && assignment?.allowedExtensions != null && (assignment?.allowedExtensions?.size ?: 0) > 0) {
- assignment!!.let {
- allowedExtensions.visibility = View.VISIBLE
- var extensions = getString(R.string.allowedExtensions)
- for (i in 0 until it.allowedExtensions.size) {
- extensions += it.allowedExtensions[i]
- if (it.allowedExtensions.size > 1 && i < it.allowedExtensions.size - 1) {
- extensions += ","
- }
- }
- allowedExtensions.text = extensions
- }
- } else {
- allowedExtensions.visibility = View.GONE
- }
- }
-
- private fun setDialogMargins(window: Window) {
- val displayMetrics = requireActivity().resources.displayMetrics
- val screenWidth = displayMetrics.widthPixels.toFloat()
- val width = Math.round(screenWidth - convertDipsToPixels(2f, requireContext()) * 2) // * 2 for margin on each side
- window.setLayout(width, ViewGroup.LayoutParams.WRAP_CONTENT)
- }
-
- private fun convertDipsToPixels(dp: Float, context: Context): Float {
- val resources = context.resources
- return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, resources.displayMetrics)
- }
-
- private fun refreshFileButtonsVisibility(view: View) {
- if (isOneFileOnly && fileList.size > 0) {
- view.addButtonsContainer.setGone()
- } else {
- view.addButtonsContainer.setVisible()
- }
- }
-
- //endregion
-
- //region Click Listeners
-
- private fun setOnClickListeners(view: View) {
- view.fromCamera.onClick { onFromCameraClick() }
- view.fromGallery.onClick { onFromGalleryClick() }
- view.fromDevice.onClick { onFromDeviceClick() }
- }
-
- private fun dialogPositiveButtonClick() {
- when (uploadType) {
- FileUploadType.MESSAGE -> {
- /* Uploads for inbox messages handled as multi-part in the API POST */
- uploadFiles()
- }
- FileUploadType.DISCUSSION -> {
- /* Uploads for discussions handled as multi-part in the API POST */
- FilesSelected(fileList).post()
- if(isOneFileOnly && fileList.isNotEmpty()) {
- dialogAttachmentCallback?.invoke(EVENT_ON_FILE_SELECTED, fileList.firstOrNull())
- }
- dismiss()
- }
- FileUploadType.QUIZ -> {
- QuizFileUploadStarted(Pair(quizQuestionId, position)).post()
- uploadFiles()
- }
- else -> uploadFiles()
- }
- }
-
- private fun onFromCameraClick() {
- if (!PermissionUtils.hasPermissions(requireActivity(), PermissionUtils.CAMERA)) {
- requestPermissions(PermissionUtils.makeArray(PermissionUtils.CAMERA), PermissionUtils.PERMISSION_REQUEST_CODE)
- return
- }
-
- val fileName = "pic_" + System.currentTimeMillis().toString() + ".jpg"
- val file = File(FileUploadUtils.getExternalCacheDir(requireContext()), fileName)
-
- cameraImageUri = FileProvider.getUriForFile(requireContext(), requireContext().packageName + Const.FILE_PROVIDER_AUTHORITY, file)
-
- if (cameraImageUri != null) {
- //save the intent information in case we get booted from memory.
- FilePrefs.tempCaptureUri = cameraImageUri.toString()
- }
-
- val intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
- intent.putExtra(MediaStore.EXTRA_OUTPUT, cameraImageUri)
-
- if (isIntentAvailable(requireActivity(), intent.action)) {
- activity?.startActivityForResult(intent, CAMERA_PIC_REQUEST)
- }
- }
-
- private fun onFromGalleryClick() {
- val intent = Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI)
- intent.flags = Intent.FLAG_GRANT_READ_URI_PERMISSION
- val file = File(requireContext().filesDir, "/image/*")
- intent.setDataAndType(FileProvider.getUriForFile(requireContext(),
- requireContext().packageName + Const.FILE_PROVIDER_AUTHORITY, file), "image/*")
- activity?.startActivityForResult(intent, PICK_IMAGE_GALLERY)
- }
-
- private fun onFromDeviceClick() {
- val intent = Intent(Intent.ACTION_GET_CONTENT)
- intent.flags = Intent.FLAG_GRANT_READ_URI_PERMISSION
- intent.type = "*/*"
- intent.addCategory(Intent.CATEGORY_OPENABLE)
- activity?.startActivityForResult(intent, PICK_FILE_FROM_DEVICE)
- }
-
- //endregion
-
- //region Permissions
-
- override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) {
- if (requestCode == PermissionUtils.PERMISSION_REQUEST_CODE) {
- if (PermissionUtils.allPermissionsGrantedResultSummary(grantResults)) {
- onFromCameraClick()
- } else {
- Toast.makeText(activity, R.string.permissionDenied, Toast.LENGTH_LONG).show()
- }
- }
- }
-
- //endregion
-
- private fun uploadFiles() {
- if (fileList.size == 0) {
- if (isOneFileOnly) {
- addButtonsContainer.setVisible()
- dialogAttachmentCallback?.invoke(EVENT_ON_FILE_SELECTED, null)
- dismiss()
- } else Toast.makeText(activity, R.string.noFilesUploaded, Toast.LENGTH_SHORT).show()
- } else {
- if (uploadType == FileUploadType.ASSIGNMENT) {
-
- var uploadNotSupported = false
-
- if (!checkIfFileSubmissionAllowed()) { //see if we can actually submit files to this assignment
- Toast.makeText(activity, R.string.fileUploadNotSupported, Toast.LENGTH_SHORT).show()
- uploadNotSupported = true
- return
- }
-
- //make sure that what we've uploaded can still be uploaded (allowed extensions)
- run {
- fileList.forEach {
- if (!isExtensionAllowed(it.fullPath)) {
- //didn't match any of the extensions, don't upload
- Toast.makeText(activity, R.string.oneOrMoreExtensionNotAllowed, Toast.LENGTH_SHORT).show()
- uploadNotSupported = true
- return@run
- }
- }
- }
-
- if(uploadNotSupported) return
- }
-
- // Start the upload service
- var bundle: Bundle? = null
- val intent = Intent(activity, FileUploadService::class.java)
-
- val parentFolderIdentifier = if(parentFolderId == INVALID_ID) null else parentFolderId
-
- when (uploadType) {
- FileUploadType.USER -> {
- bundle = FileUploadService.getUserFilesBundle(fileList, parentFolderIdentifier)
- intent.action = FileUploadService.ACTION_USER_FILE
- }
- FileUploadType.COURSE -> {
- bundle = FileUploadService.getCourseFilesBundle(fileList, canvasContext.id, parentFolderIdentifier)
- intent.action = FileUploadService.ACTION_COURSE_FILE
- }
- FileUploadType.GROUP -> {
- bundle = FileUploadService.getCourseFilesBundle(fileList, canvasContext.id, parentFolderIdentifier)
- intent.action = FileUploadService.ACTION_GROUP_FILE
- }
- FileUploadType.MESSAGE -> {
- bundle = FileUploadService.getUserFilesBundle(fileList, null)
- intent.action = FileUploadService.ACTION_MESSAGE_ATTACHMENTS
- }
- FileUploadType.DISCUSSION -> {
- bundle = FileUploadService.getUserFilesBundle(fileList, null)
- intent.action = FileUploadService.ACTION_DISCUSSION_ATTACHMENT
- }
- FileUploadType.QUIZ -> {
- bundle = FileUploadService.getQuizFileBundle(fileList, parentFolderIdentifier, quizQuestionId, position, courseId, quizId)
- intent.action = FileUploadService.ACTION_QUIZ_FILE
- }
- FileUploadType.SUBMISSION_COMMENT -> {
- bundle = FileUploadService.getSubmissionCommentBundle(fileList, canvasContext.id, assignment!!)
- intent.action = FileUploadService.ACTION_SUBMISSION_COMMENT
- }
- else -> {
- if(assignment != null) {
- bundle = FileUploadService.getAssignmentSubmissionBundle(fileList, canvasContext.id, assignment!!)
- intent.action = FileUploadService.ACTION_ASSIGNMENT_SUBMISSION
- }
- }
- }
-
- if(bundle != null) {
- dialogCallback?.invoke(EVENT_ON_UPLOAD_BEGIN)
- dialogAttachmentCallback?.invoke(EVENT_ON_UPLOAD_BEGIN, null)
- intent.putExtras(bundle)
- activity?.startService(intent)
- dismiss()
- }
- }
- }
-
- private fun handleUriContents() {
- //we only want to open the dialog in the beginning if we're not coming from an external source (sharing)
- if (uploadType == FileUploadType.MESSAGE || uploadType == FileUploadType.DISCUSSION) {
- return // Do nothing
- } else if (fileSubmitUri != null) {
- getUriContents(fileSubmitUri!!)
- }
- }
-
- private fun getUriContents(fileUri: Uri) {
- getUriContentsJob = tryWeave {
- dialog?.fileLoadingContainer?.setVisible()
-
- val submitObject = inBackground {
- val cr = requireActivity().contentResolver
- val mimeType = FileUploadUtils.getFileMimeType(cr, fileUri)
- val fileName = FileUploadUtils.getFileNameWithDefault(cr, fileUri)
- FileUploadUtils.getFileSubmitObjectFromInputStream(requireContext(), fileUri, fileName, mimeType)
- }
-
- submitObject?.let {
- if (it.errorMessage.isNullOrBlank()) {
- addIfExtensionAllowed(it)
- } else {
- Toast.makeText(activity, it.errorMessage, Toast.LENGTH_SHORT).show()
- }
- }
-
- dialog?.fileLoadingContainer?.setGone()
- } catch {
- Logger.e("Error with UploadFilesDialog.getUriContents: " + it.message)
- if(isAdded) dialog?.fileLoadingContainer?.setGone()
- }
- }
-
- private fun addIfExtensionAllowed(fileSubmitObject: FileSubmitObject): Boolean {
- if (assignment != null && (assignment?.allowedExtensions == null || assignment?.allowedExtensions?.size == 0)) {
- addToFileSubmitObjects(fileSubmitObject)
- return true
- }
-
- //get the extension and compare it to the list of allowed extensions
- val index = fileSubmitObject.fullPath.lastIndexOf(".")
- if (assignment != null && index != -1) {
- val ext = fileSubmitObject.fullPath.substring(index + 1)
- for (i in 0 until (assignment?.allowedExtensions?.size ?: 0)) {
- if (assignment!!.allowedExtensions[i].trim { it <= ' ' }.equals(ext, ignoreCase = true)) {
- addToFileSubmitObjects(fileSubmitObject)
- return true
- }
- }
- //didn't match any of the extensions, don't upload
- Toast.makeText(activity, R.string.extensionNotAllowed, Toast.LENGTH_SHORT).show()
- return false
- }
-
- //if we're sharing it from an external source we won't know which assignment they're trying to
- //submit to, so we won't know if there are any extension limits
- //also, the assignment and/or course could be null due to memory pressures
- if (assignment == null || canvasContext.id != 0L) {
- addToFileSubmitObjects(fileSubmitObject)
- return true
- }
- //don't want to try to upload it since it's not allowed.
- Toast.makeText(activity, R.string.extensionNotAllowed, Toast.LENGTH_SHORT).show()
- return false
- }
-
- private fun addToFileSubmitObjects(fileSubmitObject: FileSubmitObject) {
- fileList.add(fileSubmitObject)
- fileListAdapter?.notifyDataSetChanged()
- if(dialogRootView != null) refreshFileButtonsVisibility(dialogRootView!!)
- }
-
- // Used when the user hits the submit button after sharing files, we want to make sure they are allowed
- private fun isExtensionAllowed(filePath: String): Boolean {
- if (assignment != null && (assignment!!.allowedExtensions == null || assignment!!.allowedExtensions.size == 0)) {
- // There is an assignment, but no extension restriction...
- return true
- }
- // Get the extension and compare it to the list of allowed extensions
- val index = filePath.lastIndexOf(".")
- if (assignment != null && index != -1) {
- val ext = filePath.substring(index + 1)
- assignment!!.allowedExtensions.forEachIndexed { i, _ ->
- if(assignment!!.allowedExtensions[i].trim { it <= ' ' }.equals(ext, ignoreCase = true)) return true
- }
- return false
- }
- return false
- }
-
- private fun checkIfFileSubmissionAllowed(): Boolean {
- return if (assignment != null) {
- assignment!!.submissionTypesRaw.contains(Assignment.SubmissionType.ONLINE_UPLOAD.apiString)
- } else false
- }
-
- private fun isIntentAvailable(context: Context, action: String?): Boolean {
- return context.packageManager.queryIntentActivities(Intent(action), PackageManager.MATCH_DEFAULT_ONLY).size > 0
- }
-
- companion object {
- // Request codes, handled in Activity.onActivityResults(), activity required to implement EventBus in onActivityResults()
- const val CAMERA_PIC_REQUEST = RequestCodes.CAMERA_PIC_REQUEST
- const val PICK_IMAGE_GALLERY = RequestCodes.PICK_IMAGE_GALLERY
- const val PICK_FILE_FROM_DEVICE = 7000
-
- const val EVENT_DIALOG_CANCELED = 1
- const val EVENT_ON_UPLOAD_BEGIN = 2
- const val EVENT_ON_FILE_SELECTED = 3
-
- private const val INVALID_ID = -1L
- private const val INVALID_ID_INT = -1
-
- private fun getInstance(args: Bundle) : UploadFilesDialog {
- return UploadFilesDialog().apply {
- arguments = args
-
- fileSubmitUri = args.getParcelable(Const.URI)
- uploadType = args.getSerializable(Const.UPLOAD_TYPE) as FileUploadType
- parentFolderId = args.getLong(Const.PARENT_FOLDER_ID, INVALID_ID)
- quizQuestionId = args.getLong(Const.QUIZ_ANSWER_ID, INVALID_ID)
- quizId = args.getLong(Const.QUIZ, INVALID_ID)
- courseId = args.getLong(Const.COURSE_ID, INVALID_ID)
- position = args.getInt(Const.POSITION, INVALID_ID_INT)
- }
- }
-
- fun getInstance(args: Bundle, callback: (Int) -> Unit): UploadFilesDialog {
- return getInstance(args).apply { dialogCallback = callback }
- }
-
- fun getInstance(args: Bundle, callback: (Int, FileSubmitObject?) -> Unit): UploadFilesDialog {
- return getInstance(args).apply { dialogAttachmentCallback = callback }
- }
-
- /**
- * Typically what is used in Canvas Student and Teacher. Returns a status of the dialog fragment.
- * When file(s are selected the file(s) will trigger the [FileUploadService] to begin the upload.
- */
- fun show(fragmentManager: FragmentManager?, args: Bundle, callback: (Int) -> Unit) {
- if (fragmentManager != null) // Manager can be null if the user hits the back button too quickly
- UploadFilesDialog.getInstance(args, callback).show(fragmentManager, UploadFilesDialog::class.java.simpleName)
- }
-
- /**
- * Only used to pick a file and get an attachment object back. No file uploads are triggered when calling this function.
- */
- fun show(fragmentManager: FragmentManager?, args: Bundle, callback: (Int, FileSubmitObject?) -> Unit) {
- fragmentManager ?: return
- UploadFilesDialog.getInstance(args, callback).show(fragmentManager, UploadFilesDialog::class.java.simpleName)
- }
-
- fun createBundle(submitURI: Uri?, type: FileUploadType, parentFolderId: Long?): Bundle {
- val bundle = Bundle()
- if(submitURI != null) bundle.putParcelable(Const.URI, submitURI)
- if (parentFolderId != null) bundle.putLong(Const.PARENT_FOLDER_ID, parentFolderId)
- bundle.putSerializable(Const.UPLOAD_TYPE, type)
- return bundle
- }
-
- fun createMessageAttachmentsBundle(defaultFileList: ArrayList): Bundle {
- val bundle = createBundle(null, FileUploadType.MESSAGE, null)
- bundle.putParcelableArrayList(Const.FILES, defaultFileList)
- return bundle
- }
-
- fun createDiscussionsBundle(defaultFileList: ArrayList): Bundle {
- val bundle = createBundle(null, FileUploadType.DISCUSSION, null)
- bundle.putParcelableArrayList(Const.FILES, defaultFileList)
- return bundle
- }
-
- fun createFilesBundle(submitURI: Uri?, parentFolderId: Long?): Bundle {
- return createBundle(submitURI, FileUploadType.USER, parentFolderId)
- }
-
- fun createContextBundle(submitURI: Uri?, context: CanvasContext, parentFolderId: Long?): Bundle {
- return when {
- context.isCourse -> createCourseBundle(submitURI, context as Course, parentFolderId)
- context.isGroup -> createGroupBundle(submitURI, context as Group, parentFolderId)
- else -> createUserBundle(submitURI, context as User, parentFolderId)
- }
- }
-
- private fun createCourseBundle(submitURI: Uri?, course: Course, parentFolderId: Long?): Bundle {
- val bundle = createBundle(submitURI, FileUploadType.COURSE, parentFolderId)
- bundle.putParcelable(Const.CANVAS_CONTEXT, course)
- return bundle
- }
-
- private fun createGroupBundle(submitURI: Uri?, group: Group, parentFolderId: Long?): Bundle {
- val bundle = createBundle(submitURI, FileUploadType.GROUP, parentFolderId)
- bundle.putParcelable(Const.CANVAS_CONTEXT, group)
- return bundle
- }
-
- private fun createUserBundle(submitURI: Uri?, user: User, parentFolderId: Long?): Bundle {
- val bundle = createBundle(submitURI, FileUploadType.USER, parentFolderId)
- bundle.putParcelable(Const.CANVAS_CONTEXT, user)
- return bundle
- }
-
- fun createAssignmentBundle(submitURI: Uri?, course: Course, assignment: Assignment): Bundle {
- val bundle = createBundle(submitURI, FileUploadType.ASSIGNMENT, null)
- bundle.putParcelable(Const.CANVAS_CONTEXT, course)
- bundle.putParcelable(Const.ASSIGNMENT, assignment)
- return bundle
- }
-
- fun createQuizFileBundle(quizQuestionId: Long, courseId: Long, quizId: Long, position: Int): Bundle {
- val bundle = createBundle(null, FileUploadType.QUIZ, null)
- bundle.putLong(Const.QUIZ_ANSWER_ID, quizQuestionId)
- bundle.putLong(Const.QUIZ, quizId)
- bundle.putLong(Const.COURSE_ID, courseId)
- bundle.putInt(Const.POSITION, position)
- return bundle
- }
-
- fun createSubmissionCommentBundle(course: Course, assignment: Assignment, defaultFileList: java.util.ArrayList): Bundle {
- val bundle = createBundle(null, FileUploadType.SUBMISSION_COMMENT, null)
- bundle.putParcelable(Const.CANVAS_CONTEXT, course)
- bundle.putParcelable(Const.ASSIGNMENT, assignment)
- bundle.putParcelableArrayList(Const.FILES, defaultFileList)
- return bundle
- }
-
- fun createAttachmentsBundle(defaultFileList: ArrayList = ArrayList()): Bundle {
- val bundle = createBundle(null, FileUploadType.MESSAGE, null)
- bundle.putParcelableArrayList(Const.FILES, defaultFileList)
- return bundle
- }
- }
-
- inner class FileRecyclerViewAdapter(private val onRemovedFileCallback: (FileSubmitObject, Int) -> Unit) : RecyclerView.Adapter() {
-
- override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FileViewHolder =
- FileViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.adapter_file_uploads, parent, false))
-
- override fun onBindViewHolder(holder: FileViewHolder, position: Int) {
- holder.bind(fileList[position]) {
- // The position passed to onBindViewHolder could be stale at this point if the user has recently
- // added/removed items, so we need to use the current adapter position instead.
- val currentPosition = holder.adapterPosition
- onRemovedFileCallback(fileList[currentPosition], currentPosition)
- }
- }
-
- override fun getItemCount(): Int {
- return fileList.size
- }
-
- fun clear() {
- fileList.clear()
- notifyDataSetChanged()
- }
- }
-}
-
-class FileViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
-
- fun bind(item: FileSubmitObject, onRemovedFileCallback: (FileSubmitObject) -> Unit): Unit = with(itemView) {
-
- AttachmentView.setColorAndIcon(context, item.contentType, item.name, null, fileIcon)
-
- ColorUtils.colorIt(ThemePrefs.brandColor, fileIcon)
-
- when {
- item.currentState == FileSubmitObject.STATE.UPLOADING -> {
- progressBar.isIndeterminate = true
- progressBar.visibility = View.VISIBLE
- fileIcon.setGone()
- removeFile.setGone()
- }
- item.currentState == FileSubmitObject.STATE.COMPLETE -> {
- fileIcon.setImageResource(R.drawable.ic_checkmark)
- removeFile.setGone()
- progressBar.isIndeterminate = false
- progressBar.visibility = View.GONE
- fileIcon.setVisible()
- }
- item.currentState == FileSubmitObject.STATE.NORMAL -> {
- removeFile.setImageResource(R.drawable.ic_close)
- removeFile.contentDescription = context.getString(R.string.utils_removeAttachment)
- removeFile.setOnClickListener {
- onRemovedFileCallback(item)
- }
- }
- }
-
- fileName.text = item.name
- fileSize.text = humanReadableByteCount(item.size)
- }
-
- private fun humanReadableByteCount(bytes: Long): String {
- val unit = 1024
- if (bytes < unit) return bytes.toString() + " B"
- val exp = (Math.log(bytes.toDouble()) / Math.log(unit.toDouble())).toInt()
- val pre = "KMGTPE"[exp - 1].toString()
- return String.format(Locale.getDefault(), "%.1f %sB", bytes / Math.pow(unit.toDouble(), exp.toDouble()), pre)
- }
-}
-
-
diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/dashboard/notifications/DashboardNotificationsFragment.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/dashboard/notifications/DashboardNotificationsFragment.kt
index 639ed2c456..800a10c748 100644
--- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/dashboard/notifications/DashboardNotificationsFragment.kt
+++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/dashboard/notifications/DashboardNotificationsFragment.kt
@@ -16,6 +16,7 @@
package com.instructure.pandautils.features.dashboard.notifications
+import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.view.LayoutInflater
@@ -29,6 +30,8 @@ import androidx.fragment.app.viewModels
import com.instructure.pandautils.analytics.SCREEN_VIEW_DASHBOARD_NOTIFICATIONS
import com.instructure.pandautils.analytics.ScreenView
import com.instructure.pandautils.databinding.FragmentDashboardNotificationsBinding
+import com.instructure.pandautils.features.shareextension.ShareExtensionActivity
+import com.instructure.pandautils.features.shareextension.ShareExtensionRouter
import com.instructure.pandautils.utils.ColorKeeper
import com.instructure.pandautils.utils.asChooserExcludingInstructure
import dagger.hilt.android.AndroidEntryPoint
@@ -38,6 +41,9 @@ import javax.inject.Inject
@AndroidEntryPoint
class DashboardNotificationsFragment : Fragment() {
+ @Inject
+ lateinit var shareExtensionRouter: ShareExtensionRouter
+
@Inject
lateinit var dashboardRouter: DashboardRouter
@@ -59,11 +65,11 @@ class DashboardNotificationsFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
- viewModel.events.observe(viewLifecycleOwner, { event ->
+ viewModel.events.observe(viewLifecycleOwner) { event ->
event.getContentIfNotHandled()?.let {
handleAction(it)
}
- })
+ }
}
fun refresh() {
@@ -74,14 +80,14 @@ class DashboardNotificationsFragment : Fragment() {
when (action) {
is DashboardNotificationsActions.LaunchConference -> {
val colorSchemeParams = CustomTabColorSchemeParams.Builder()
- .setToolbarColor(ColorKeeper.getOrGenerateColor(action.canvasContext))
- .build()
+ .setToolbarColor(ColorKeeper.getOrGenerateColor(action.canvasContext))
+ .build()
var intent = CustomTabsIntent.Builder()
- .setDefaultColorSchemeParams(colorSchemeParams)
- .setShowTitle(true)
- .build()
- .intent
+ .setDefaultColorSchemeParams(colorSchemeParams)
+ .setShowTitle(true)
+ .build()
+ .intent
intent.data = Uri.parse(action.url)
@@ -97,6 +103,9 @@ class DashboardNotificationsFragment : Fragment() {
action.subject,
action.message
)
+ is DashboardNotificationsActions.OpenProgressDialog -> {
+ startActivity(shareExtensionRouter.routeToProgressScreen(requireActivity(), action.uuid))
+ }
}
}
diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/dashboard/notifications/DashboardNotificationsViewData.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/dashboard/notifications/DashboardNotificationsViewData.kt
index 58fe29dc3b..7b0030c0f9 100644
--- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/dashboard/notifications/DashboardNotificationsViewData.kt
+++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/dashboard/notifications/DashboardNotificationsViewData.kt
@@ -17,13 +17,21 @@
package com.instructure.pandautils.features.dashboard.notifications
import androidx.annotation.DrawableRes
+import androidx.databinding.BaseObservable
+import androidx.databinding.Bindable
import com.instructure.canvasapi2.models.CanvasContext
import com.instructure.canvasapi2.models.Conference
+import com.instructure.pandautils.features.dashboard.notifications.itemviewmodels.UploadItemViewModel
import com.instructure.pandautils.mvvm.ItemViewModel
+import java.util.*
data class DashboardNotificationsViewData(
- val items: List
-)
+ val items: List,
+ var uploadItems: List
+) : BaseObservable() {
+ @Bindable
+ fun getConcatenatedItems() = uploadItems + items
+}
data class InvitationViewData(
val title: String,
@@ -45,8 +53,15 @@ data class AnnouncementViewData(
@DrawableRes val icon: Int
)
+data class UploadViewData(
+ var title: String = "",
+ var subTitle: String = "",
+ var color: String = ""
+)
+
sealed class DashboardNotificationsActions {
- data class ShowToast(val toast: String): DashboardNotificationsActions()
- data class LaunchConference(val canvasContext: CanvasContext, val url: String): DashboardNotificationsActions()
- data class OpenAnnouncement(val subject: String, val message: String): DashboardNotificationsActions()
+ data class ShowToast(val toast: String) : DashboardNotificationsActions()
+ data class LaunchConference(val canvasContext: CanvasContext, val url: String) : DashboardNotificationsActions()
+ data class OpenAnnouncement(val subject: String, val message: String) : DashboardNotificationsActions()
+ data class OpenProgressDialog(val uuid: UUID): DashboardNotificationsActions()
}
diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/dashboard/notifications/DashboardNotificationsViewModel.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/dashboard/notifications/DashboardNotificationsViewModel.kt
index b2c31b1ab0..18754861e8 100644
--- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/dashboard/notifications/DashboardNotificationsViewModel.kt
+++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/dashboard/notifications/DashboardNotificationsViewModel.kt
@@ -19,21 +19,13 @@ package com.instructure.pandautils.features.dashboard.notifications
import android.content.res.Resources
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
+import androidx.work.WorkManager
import com.instructure.canvasapi2.apis.EnrollmentAPI
-import com.instructure.canvasapi2.managers.AccountNotificationManager
-import com.instructure.canvasapi2.managers.ConferenceManager
-import com.instructure.canvasapi2.managers.CourseManager
-import com.instructure.canvasapi2.managers.EnrollmentManager
-import com.instructure.canvasapi2.managers.GroupManager
-import com.instructure.canvasapi2.managers.OAuthManager
-import com.instructure.canvasapi2.models.AccountNotification
-import com.instructure.canvasapi2.models.CanvasContext
-import com.instructure.canvasapi2.models.Conference
-import com.instructure.canvasapi2.models.Course
-import com.instructure.canvasapi2.models.Enrollment
-import com.instructure.canvasapi2.models.Group
+import com.instructure.canvasapi2.managers.*
+import com.instructure.canvasapi2.models.*
import com.instructure.canvasapi2.utils.ApiPrefs
import com.instructure.canvasapi2.utils.isValidTerm
import com.instructure.pandautils.BR
@@ -41,6 +33,10 @@ import com.instructure.pandautils.R
import com.instructure.pandautils.features.dashboard.notifications.itemviewmodels.AnnouncementItemViewModel
import com.instructure.pandautils.features.dashboard.notifications.itemviewmodels.ConferenceItemViewModel
import com.instructure.pandautils.features.dashboard.notifications.itemviewmodels.InvitationItemViewModel
+import com.instructure.pandautils.features.dashboard.notifications.itemviewmodels.UploadItemViewModel
+import com.instructure.pandautils.features.file.upload.preferences.FileUploadPreferences
+import com.instructure.pandautils.features.file.upload.worker.FileUploadWorker.Companion.PROGRESS_DATA_ASSIGNMENT_NAME
+import com.instructure.pandautils.features.file.upload.worker.FileUploadWorker.Companion.PROGRESS_DATA_TITLE
import com.instructure.pandautils.models.ConferenceDashboardBlacklist
import com.instructure.pandautils.mvvm.Event
import com.instructure.pandautils.mvvm.ItemViewModel
@@ -63,7 +59,9 @@ class DashboardNotificationsViewModel @Inject constructor(
private val accountNotificationManager: AccountNotificationManager,
private val oauthManager: OAuthManager,
private val conferenceDashboardBlacklist: ConferenceDashboardBlacklist,
- private val apiPrefs: ApiPrefs
+ private val apiPrefs: ApiPrefs,
+ private val workManager: WorkManager,
+ private val fileUploadPreferences: FileUploadPreferences
) : ViewModel() {
val state: LiveData
@@ -81,6 +79,22 @@ class DashboardNotificationsViewModel @Inject constructor(
private var coursesMap: Map = emptyMap()
private var groupMap: Map = emptyMap()
+ private val runningWorkersObserver = Observer> {
+ _data.value?.uploadItems?.forEach { it.clear() }
+ _data.value?.uploadItems = getUploads(it)
+ _data.value?.notifyPropertyChanged(BR.concatenatedItems)
+ }
+
+ init {
+ fileUploadPreferences.getRunningWorkersLiveData().observeForever(runningWorkersObserver)
+ }
+
+ override fun onCleared() {
+ _data.value?.uploadItems?.forEach { it.clear() }
+ fileUploadPreferences.getRunningWorkersLiveData().removeObserver(runningWorkersObserver)
+ super.onCleared()
+ }
+
fun loadData(forceNetwork: Boolean = false) {
viewModelScope.launch {
@@ -102,7 +116,9 @@ class DashboardNotificationsViewModel @Inject constructor(
val conferenceViewModels = getConferences(forceNetwork)
items.addAll(conferenceViewModels)
- _data.postValue(DashboardNotificationsViewData(items))
+ val uploadViewModels = getUploads(fileUploadPreferences.getRunningWorkerIds())
+
+ _data.postValue(DashboardNotificationsViewData(items, uploadViewModels))
}
}
@@ -204,6 +220,18 @@ class DashboardNotificationsViewModel @Inject constructor(
}
}
+ private fun getUploads(runningWorkerIds: List) = runningWorkerIds.map {
+ val workInfo = workManager.getWorkInfoById(it).get()
+ val uploadViewData = UploadViewData(
+ workInfo.progress.getString(PROGRESS_DATA_TITLE).orEmpty(),
+ workInfo.progress.getString(PROGRESS_DATA_ASSIGNMENT_NAME).orEmpty(),
+ "#${resources.getColor(R.color.backgroundInfo).toHexString()}",
+ )
+ UploadItemViewModel(it, workManager, uploadViewData) { uuid ->
+ _events.postValue(Event(DashboardNotificationsActions.OpenProgressDialog(uuid)))
+ }
+ }
+
private fun hasValidCourseForEnrollment(enrollment: Enrollment): Boolean {
return coursesMap[enrollment.courseId]?.let { course ->
course.isValidTerm() && !course.accessRestrictedByDate && isEnrollmentBeforeEndDateOrNotRestricted(course)
diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/dashboard/notifications/itemviewmodels/UploadItemViewModel.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/dashboard/notifications/itemviewmodels/UploadItemViewModel.kt
new file mode 100644
index 0000000000..7b78ef1671
--- /dev/null
+++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/dashboard/notifications/itemviewmodels/UploadItemViewModel.kt
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2022 - present Instructure, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+package com.instructure.pandautils.features.dashboard.notifications.itemviewmodels
+
+import androidx.databinding.BaseObservable
+import androidx.databinding.Bindable
+import androidx.lifecycle.Observer
+import androidx.work.WorkInfo
+import androidx.work.WorkManager
+import com.instructure.pandautils.BR
+import com.instructure.pandautils.R
+import com.instructure.pandautils.features.dashboard.notifications.UploadViewData
+import com.instructure.pandautils.features.file.upload.worker.FileUploadWorker.Companion.PROGRESS_DATA_FULL_SIZE
+import com.instructure.pandautils.features.file.upload.worker.FileUploadWorker.Companion.PROGRESS_DATA_UPLOADED_SIZE
+import com.instructure.pandautils.mvvm.ItemViewModel
+import java.util.*
+
+class UploadItemViewModel(
+ private val workerId: UUID,
+ val workManager: WorkManager,
+ val data: UploadViewData,
+ @get:Bindable var progress: Int = 0,
+ val open: (UUID) -> Unit
+) : ItemViewModel, BaseObservable() {
+
+ private val observer = Observer {
+ val uploadedSize = it.progress.getLong(PROGRESS_DATA_UPLOADED_SIZE, 0L)
+ val fullSize = it.progress.getLong(PROGRESS_DATA_FULL_SIZE, 1L)
+
+ progress = ((uploadedSize.toDouble() / fullSize.toDouble()) * 100.0).toInt()
+ notifyPropertyChanged(BR.progress)
+ }
+ override val layoutId = R.layout.item_dashboard_upload
+
+ init {
+ workManager.getWorkInfoByIdLiveData(workerId).observeForever(observer)
+ }
+
+ fun clear() {
+ workManager.getWorkInfoByIdLiveData(workerId).removeObserver(observer)
+ }
+
+
+ fun open() = open.invoke(workerId)
+}
\ No newline at end of file
diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/discussion/details/DiscussionDetailsWebViewFragment.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/discussion/details/DiscussionDetailsWebViewFragment.kt
index 4355e0a9f8..0a2d6a4277 100644
--- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/discussion/details/DiscussionDetailsWebViewFragment.kt
+++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/discussion/details/DiscussionDetailsWebViewFragment.kt
@@ -75,7 +75,7 @@ class DiscussionDetailsWebViewFragment : Fragment() {
override fun onPageFinishedCallback(webView: WebView, url: String) {
viewModel.setLoading(false)
- discussionSwipeRefreshLayout.isRefreshing = false
+ discussionSwipeRefreshLayout?.isRefreshing = false
}
override fun routeInternallyCallback(url: String) {
diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/file/upload/FileUploadDialogFragment.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/file/upload/FileUploadDialogFragment.kt
new file mode 100644
index 0000000000..f353451c72
--- /dev/null
+++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/file/upload/FileUploadDialogFragment.kt
@@ -0,0 +1,387 @@
+/*
+ * Copyright (C) 2022 - present Instructure, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.instructure.pandautils.features.file.upload
+
+import android.Manifest
+import android.app.Dialog
+import android.content.DialogInterface
+import android.content.pm.PackageManager
+import android.net.Uri
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.view.WindowManager
+import android.widget.Toast
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.appcompat.app.AlertDialog
+import androidx.core.content.ContextCompat
+import androidx.core.content.FileProvider
+import androidx.fragment.app.DialogFragment
+import androidx.fragment.app.viewModels
+import androidx.lifecycle.LiveData
+import androidx.work.WorkInfo
+import com.instructure.canvasapi2.models.*
+import com.instructure.canvasapi2.models.postmodels.FileSubmitObject
+import com.instructure.canvasapi2.utils.ApiPrefs
+import com.instructure.pandautils.R
+import com.instructure.pandautils.databinding.FragmentFileUploadDialogBinding
+import com.instructure.pandautils.features.shareextension.ShareExtensionActivity
+import com.instructure.pandautils.utils.*
+import dagger.hilt.android.AndroidEntryPoint
+import java.io.File
+import java.util.*
+
+@AndroidEntryPoint
+class FileUploadDialogFragment : DialogFragment() {
+
+ private val viewModel: FileUploadDialogViewModel by viewModels()
+
+ private lateinit var binding: FragmentFileUploadDialogBinding
+
+ private var uploadType: FileUploadType by SerializableArg(FileUploadType.ASSIGNMENT)
+ private var canvasContext: CanvasContext by ParcelableArg(ApiPrefs.user)
+ private var position: Int by IntArg()
+
+ private var fileSubmitUris: ArrayList? = arrayListOf()
+ private var cameraImageUri: Uri? = null
+
+ private var assignment: Assignment? by NullableParcelableArg()
+ private var parentFolderId: Long by LongArg()
+ private var quizQuestionId: Long by LongArg()
+ private var quizId: Long by LongArg()
+ private var courseId: Long by LongArg()
+ private var userId: Long by LongArg()
+
+ private var dialogCallback: ((Int) -> Unit)? = null
+
+ private val cameraPermissionContract = registerForActivityResult(ActivityResultContracts.RequestPermission()) { isPermissionGranted ->
+ if (isPermissionGranted) takePicture()
+ }
+
+ private val takePictureContract = registerForActivityResult(ActivityResultContracts.TakePicture()) { imageSaved ->
+ if (imageSaved) {
+ cameraImageUri?.let {
+ viewModel.addFile(it)
+ }
+ }
+ }
+
+ private val filePickerContract = registerForActivityResult(ActivityResultContracts.GetContent()) {
+ it?.let {
+ viewModel.addFile(it)
+ }
+ }
+
+ private val multipleFilePickerContract = registerForActivityResult(ActivityResultContracts.GetMultipleContents()) {
+ it?.let {
+ viewModel.addFiles(it)
+ }
+ }
+
+ override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
+ savedInstanceState: Bundle?): View {
+ binding.lifecycleOwner = this
+ binding.viewModel = viewModel
+
+ return binding.root
+ }
+
+ override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
+ binding = FragmentFileUploadDialogBinding.inflate(layoutInflater, null, false)
+
+ val title: String
+ val positiveText: String
+
+ // Get dialog headers
+ when (uploadType) {
+ FileUploadType.ASSIGNMENT -> {
+ title = getString(R.string.submission)
+ positiveText = getString(R.string.turnIn)
+ }
+ FileUploadType.COURSE -> {
+ title = getString(R.string.utils_uploadTo) + " " + getString(R.string.utils_uploadCourseFiles)
+ positiveText = getString(R.string.upload)
+ }
+ FileUploadType.GROUP -> {
+ title = getString(R.string.utils_uploadTo) + " " + getString(R.string.utils_uploadGroupFiles)
+ positiveText = getString(R.string.upload)
+ }
+ FileUploadType.MESSAGE -> {
+ title = getString(R.string.utils_attachFile)
+ positiveText = getString(R.string.utils_okay)
+ }
+ FileUploadType.DISCUSSION -> {
+ title = getString(R.string.utils_attachFile)
+ positiveText = getString(R.string.utils_okay)
+ }
+ FileUploadType.QUIZ -> {
+ title = getString(R.string.utils_uploadTo) + " " + getString(R.string.utils_uploadMyFiles)
+ positiveText = getString(R.string.utils_upload)
+ }
+ FileUploadType.SUBMISSION_COMMENT, FileUploadType.TEACHER_SUBMISSION_COMMENT -> {
+ title = getString(R.string.utils_uploadToSubmissionComment)
+ positiveText = getString(R.string.utils_upload)
+ }
+ else -> {
+ title = getString(R.string.utils_uploadTo) + " " + getString(R.string.utils_uploadMyFiles)
+ positiveText = getString(R.string.utils_upload)
+ }
+ }
+
+ val dialog = AlertDialog.Builder(requireContext())
+ .setTitle(title)
+ .setView(binding.root)
+ .setPositiveButton(positiveText, null)
+ .setNegativeButton(R.string.utils_cancel, null)
+ .create()
+
+ dialog.setCanceledOnTouchOutside(false)
+
+ dialog.setOnShowListener {
+ val positive = dialog.getButton(DialogInterface.BUTTON_POSITIVE)
+ positive.setTextColor(ThemePrefs.buttonColor)
+ positive.setOnClickListener { uploadClicked() }
+ val negative = dialog.getButton(DialogInterface.BUTTON_NEGATIVE)
+ negative.setTextColor(ThemePrefs.buttonColor)
+ negative.setOnClickListener {
+ cancelClicked()
+ }
+ }
+
+ if (requireActivity() is ShareExtensionActivity) {
+ dialog.window?.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND)
+ }
+
+ return dialog
+ }
+
+ override fun onCancel(dialog: DialogInterface) {
+ super.onCancel(dialog)
+ if (requireActivity() is ShareExtensionActivity) {
+ requireActivity().onBackPressed()
+ }
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+
+ viewModel.events.observe(this) { event ->
+ event.getContentIfNotHandled()?.let {
+ handleAction(it)
+ }
+ }
+
+ viewModel.setData(
+ assignment, fileSubmitUris, uploadType, canvasContext, parentFolderId, quizQuestionId,
+ position, quizId, userId, dialogCallback
+ )
+ }
+
+ private fun uploadClicked() {
+ viewModel.uploadFiles()
+ }
+
+ private fun cancelClicked() {
+ dismissAllowingStateLoss()
+ viewModel.onCancelClicked()
+ }
+
+ private fun handleAction(action: FileUploadAction) {
+ when (action) {
+ is FileUploadAction.TakePhoto -> takePicture()
+ is FileUploadAction.PickImage -> pickFromGallery()
+ is FileUploadAction.PickFile -> pickFromFiles()
+ is FileUploadAction.PickMultipleFile -> pickMultipleFile()
+ is FileUploadAction.PickMultipleImage -> pickMultipleImage()
+ is FileUploadAction.ShowToast -> Toast.makeText(requireContext(), action.toast, Toast.LENGTH_SHORT).show()
+ is FileUploadAction.UploadStarted -> dismiss()
+ is FileUploadAction.AttachmentSelectedAction -> getParent()?.attachmentCallback(action.event, action.attachment)
+ is FileUploadAction.UploadStartedAction -> {
+ getParent()?.selectedUriStringsCallback(action.selectedUris)
+ getParent()?.workInfoLiveDataCallback(action.id, action.liveData)
+ }
+ }
+ }
+
+ private fun getParent(): FileUploadDialogParent? {
+ var parent = parentFragment as? FileUploadDialogParent
+ if (parent == null) {
+ parent = activity as? FileUploadDialogParent
+ }
+
+ return parent
+ }
+
+ private fun takePicture() {
+ if (ContextCompat.checkSelfPermission(requireContext(), Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) {
+ cameraPermissionContract.launch(Manifest.permission.CAMERA)
+ } else {
+ val fileName = "pic_" + System.currentTimeMillis().toString() + ".jpg"
+ val file = File(FileUploadUtils.getExternalCacheDir(requireContext()), fileName)
+
+ cameraImageUri = FileProvider.getUriForFile(requireContext(), requireContext().packageName + Const.FILE_PROVIDER_AUTHORITY, file)
+ takePictureContract.launch(cameraImageUri)
+ }
+ }
+
+ private fun pickFromGallery() {
+ filePickerContract.launch("image/*")
+ }
+
+ private fun pickFromFiles() {
+ filePickerContract.launch("*/*")
+ }
+
+ private fun pickMultipleFile() {
+ multipleFilePickerContract.launch("*/*")
+ }
+
+ private fun pickMultipleImage() {
+ multipleFilePickerContract.launch("image/*")
+ }
+
+ companion object {
+
+ const val TAG = "FileUploadDialogFragment"
+
+ private const val INVALID_ID = -1L
+ private const val INVALID_ID_INT = -1
+
+ const val EVENT_DIALOG_CANCELED = 1
+ const val EVENT_ON_UPLOAD_BEGIN = 2
+ const val EVENT_ON_FILE_SELECTED = 3
+
+ fun newInstance(): FileUploadDialogFragment = FileUploadDialogFragment()
+
+ fun newInstance(args: Bundle, callback: ((Int) -> Unit)? = null): FileUploadDialogFragment {
+ return FileUploadDialogFragment().apply {
+ arguments = args
+
+ fileSubmitUris = args.getParcelableArrayList(Const.URIS)
+ uploadType = args.getSerializable(Const.UPLOAD_TYPE) as FileUploadType
+ parentFolderId = args.getLong(Const.PARENT_FOLDER_ID, INVALID_ID)
+ quizQuestionId = args.getLong(Const.QUIZ_ANSWER_ID, INVALID_ID)
+ quizId = args.getLong(Const.QUIZ, INVALID_ID)
+ courseId = args.getLong(Const.COURSE_ID, INVALID_ID)
+ position = args.getInt(Const.POSITION, INVALID_ID_INT)
+ dialogCallback = callback
+ userId = args.getLong(Const.USER_ID, INVALID_ID)
+ }
+ }
+
+ fun createBundle(submitURIs: ArrayList, type: FileUploadType, parentFolderId: Long?): Bundle {
+ val bundle = Bundle()
+ if (submitURIs.isNotEmpty()) bundle.putParcelableArrayList(Const.URIS, submitURIs)
+ if (parentFolderId != null) bundle.putLong(Const.PARENT_FOLDER_ID, parentFolderId)
+ bundle.putSerializable(Const.UPLOAD_TYPE, type)
+ return bundle
+ }
+
+ fun createMessageAttachmentsBundle(defaultFileList: ArrayList): Bundle {
+ val bundle = createBundle(arrayListOf(), FileUploadType.MESSAGE, null)
+ bundle.putParcelableArrayList(Const.FILES, defaultFileList)
+ return bundle
+ }
+
+ fun createDiscussionsBundle(defaultFileList: ArrayList): Bundle {
+ val bundle = createBundle(arrayListOf(), FileUploadType.DISCUSSION, null)
+ bundle.putParcelableArrayList(Const.FILES, defaultFileList)
+ return bundle
+ }
+
+ fun createFilesBundle(submitUris: ArrayList, parentFolderId: Long?): Bundle {
+ return createBundle(submitUris, FileUploadType.USER, parentFolderId)
+ }
+
+ fun createContextBundle(submitURI: Uri?, context: CanvasContext, parentFolderId: Long?): Bundle {
+ return when {
+ context.isCourse -> createCourseBundle(submitURI, context as Course, parentFolderId)
+ context.isGroup -> createGroupBundle(submitURI, context as Group, parentFolderId)
+ else -> createUserBundle(submitURI, context as User, parentFolderId)
+ }
+ }
+
+ private fun createCourseBundle(submitURI: Uri?, course: Course, parentFolderId: Long?): Bundle {
+ val submitUris = submitURI?.let {
+ arrayListOf(it)
+ } ?: arrayListOf()
+ val bundle = createBundle(submitUris, FileUploadType.COURSE, parentFolderId)
+ bundle.putParcelable(Const.CANVAS_CONTEXT, course)
+ return bundle
+ }
+
+ private fun createGroupBundle(submitURI: Uri?, group: Group, parentFolderId: Long?): Bundle {
+ val submitUris = submitURI?.let {
+ arrayListOf(it)
+ } ?: arrayListOf()
+ val bundle = createBundle(submitUris, FileUploadType.GROUP, parentFolderId)
+ bundle.putParcelable(Const.CANVAS_CONTEXT, group)
+ return bundle
+ }
+
+ private fun createUserBundle(submitURI: Uri?, user: User, parentFolderId: Long?): Bundle {
+ val submitUris = submitURI?.let {
+ arrayListOf(it)
+ } ?: arrayListOf()
+ val bundle = createBundle(submitUris, FileUploadType.USER, parentFolderId)
+ bundle.putParcelable(Const.CANVAS_CONTEXT, user)
+ return bundle
+ }
+
+ fun createAssignmentBundle(submitURIs: ArrayList, course: Course, assignment: Assignment): Bundle {
+ val bundle = createBundle(submitURIs, FileUploadType.ASSIGNMENT, null)
+ bundle.putParcelable(Const.CANVAS_CONTEXT, course)
+ bundle.putParcelable(Const.ASSIGNMENT, assignment)
+ return bundle
+ }
+
+ fun createQuizFileBundle(quizQuestionId: Long, courseId: Long, quizId: Long, position: Int): Bundle {
+ val bundle = createBundle(arrayListOf(), FileUploadType.QUIZ, null)
+ bundle.putLong(Const.QUIZ_ANSWER_ID, quizQuestionId)
+ bundle.putLong(Const.QUIZ, quizId)
+ bundle.putLong(Const.COURSE_ID, courseId)
+ bundle.putInt(Const.POSITION, position)
+ return bundle
+ }
+
+ fun createSubmissionCommentBundle(course: Course, assignment: Assignment, defaultFileList: ArrayList): Bundle {
+ val bundle = createBundle(arrayListOf(), FileUploadType.SUBMISSION_COMMENT, null)
+ bundle.putParcelable(Const.CANVAS_CONTEXT, course)
+ bundle.putParcelable(Const.ASSIGNMENT, assignment)
+ bundle.putParcelableArrayList(Const.FILES, defaultFileList)
+ return bundle
+ }
+
+ fun createAttachmentsBundle(defaultFileList: ArrayList = ArrayList()): Bundle {
+ val bundle = createBundle(arrayListOf(), FileUploadType.MESSAGE, null)
+ bundle.putParcelableArrayList(Const.FILES, defaultFileList)
+ return bundle
+ }
+
+ fun createTeacherSubmissionCommentBundle(
+ courseId: Long,
+ assignmentId: Long,
+ userId: Long
+ ): Bundle {
+ val bundle = createBundle(arrayListOf(), FileUploadType.TEACHER_SUBMISSION_COMMENT, null)
+ bundle.putParcelable(Const.ASSIGNMENT, Assignment(assignmentId, courseId = courseId))
+ bundle.putLong(Const.USER_ID, userId)
+ return bundle
+ }
+ }
+}
\ No newline at end of file
diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/file/upload/FileUploadDialogParent.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/file/upload/FileUploadDialogParent.kt
new file mode 100644
index 0000000000..11d4099ef7
--- /dev/null
+++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/file/upload/FileUploadDialogParent.kt
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2022 - present Instructure, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+package com.instructure.pandautils.features.file.upload
+
+import androidx.lifecycle.LiveData
+import androidx.work.WorkInfo
+import com.instructure.canvasapi2.models.postmodels.FileSubmitObject
+import java.util.*
+
+interface FileUploadDialogParent {
+
+ fun attachmentCallback(event: Int, attachment: FileSubmitObject?) = Unit
+
+ fun selectedUriStringsCallback(filePaths: List) = Unit
+
+ fun workInfoLiveDataCallback(uuid: UUID? = null, workInfoLiveData: LiveData) = Unit
+}
\ No newline at end of file
diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/file/upload/FileUploadDialogViewData.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/file/upload/FileUploadDialogViewData.kt
new file mode 100644
index 0000000000..ee87c7cba7
--- /dev/null
+++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/file/upload/FileUploadDialogViewData.kt
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2022 - present Instructure, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.instructure.pandautils.features.file.upload
+
+import android.net.Uri
+import androidx.lifecycle.LiveData
+import androidx.work.WorkInfo
+import com.instructure.canvasapi2.models.postmodels.FileSubmitObject
+import com.instructure.pandautils.features.file.upload.itemviewmodels.FileItemViewModel
+import java.util.UUID
+
+data class FileUploadDialogViewData(
+ val allowedExtensions: String?,
+ val files: List
+)
+
+data class FileItemViewData(
+ val fileName: String,
+ val fileSize: String,
+ val fullPath: String
+)
+
+data class FileUploadData(
+ val uri: Uri,
+ val fileSubmitObject: FileSubmitObject
+)
+
+sealed class FileUploadAction {
+ object TakePhoto : FileUploadAction()
+ object PickImage : FileUploadAction()
+ object PickFile : FileUploadAction()
+ object PickMultipleImage : FileUploadAction()
+ object PickMultipleFile : FileUploadAction()
+ object UploadStarted : FileUploadAction()
+ data class ShowToast(val toast: String) : FileUploadAction()
+ data class AttachmentSelectedAction(val event: Int, val attachment: FileSubmitObject?) : FileUploadAction()
+ data class UploadStartedAction(val id: UUID, val liveData: LiveData, val selectedUris: List) : FileUploadAction()
+}
+
diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/file/upload/FileUploadDialogViewModel.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/file/upload/FileUploadDialogViewModel.kt
new file mode 100644
index 0000000000..ac30b8430c
--- /dev/null
+++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/file/upload/FileUploadDialogViewModel.kt
@@ -0,0 +1,355 @@
+/*
+ * Copyright (C) 2022 - present Instructure, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.instructure.pandautils.features.file.upload
+
+import android.content.res.Resources
+import android.net.Uri
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.ViewModel
+import androidx.work.Data
+import androidx.work.OneTimeWorkRequestBuilder
+import androidx.work.WorkInfo
+import androidx.work.WorkManager
+import com.instructure.canvasapi2.models.Assignment
+import com.instructure.canvasapi2.models.CanvasContext
+import com.instructure.canvasapi2.models.postmodels.FileSubmitObject
+import com.instructure.pandautils.R
+import com.instructure.pandautils.features.file.upload.itemviewmodels.FileItemViewModel
+import com.instructure.pandautils.features.file.upload.worker.FileUploadBundleCreator
+import com.instructure.pandautils.features.file.upload.worker.FileUploadWorker
+import com.instructure.pandautils.mvvm.Event
+import com.instructure.pandautils.utils.humanReadableByteCount
+import com.instructure.pandautils.utils.orDefault
+import dagger.hilt.android.lifecycle.HiltViewModel
+import java.util.*
+import javax.inject.Inject
+
+@HiltViewModel
+class FileUploadDialogViewModel @Inject constructor(
+ private val fileUploadUtils: FileUploadUtilsHelper,
+ private val resources: Resources,
+ private val workManager: WorkManager,
+ private val fileUploadBundleCreator: FileUploadBundleCreator
+) : ViewModel() {
+
+ val data: LiveData
+ get() = _data
+ private val _data = MutableLiveData()
+
+ val events: LiveData>
+ get() = _events
+ private val _events = MutableLiveData>()
+
+ private var assignment: Assignment? = null
+ private var uploadType: FileUploadType = FileUploadType.ASSIGNMENT
+ private var canvasContext: CanvasContext = CanvasContext.defaultCanvasContext()
+ private var userId: Long? = null
+ private var isOneFileOnly = false
+ private var parentFolderId: Long? = null
+ private var quizQuestionId: Long = -1L
+ private var quizId: Long = -1L
+ private var position: Int = -1
+
+ var dialogCallback: ((Int) -> Unit)? = null
+
+ private var filesToUpload = mutableListOf()
+
+ fun setData(
+ assignment: Assignment?,
+ files: ArrayList?,
+ uploadType: FileUploadType,
+ canvasContext: CanvasContext,
+ parentFolderId: Long,
+ quizQuestionId: Long,
+ position: Int,
+ quizId: Long,
+ userId: Long,
+ dialogCallback: ((Int) -> Unit)? = null
+ ) {
+ this.assignment = assignment
+ files?.forEach { uri ->
+ val submitObject = getUriContents(uri)
+ submitObject?.let { fso ->
+ this.filesToUpload.add(FileUploadData(uri, fso))
+ }
+ }
+ this.uploadType = uploadType
+ this.canvasContext = canvasContext
+ this.isOneFileOnly = uploadType == FileUploadType.QUIZ || uploadType == FileUploadType.DISCUSSION
+ this.parentFolderId = parentFolderId
+ this.quizQuestionId = quizQuestionId
+ this.quizId = quizId
+ this.position = position
+ this.userId = userId
+ dialogCallback?.let {
+ this.dialogCallback = it
+ }
+ updateItems()
+ }
+
+ fun onCameraClicked() {
+ if (isOneFileOnly && filesToUpload.isNotEmpty()) {
+ _events.value = Event(FileUploadAction.ShowToast(resources.getString(R.string.oneFileOnly)))
+ return
+ }
+ _events.value = Event(FileUploadAction.TakePhoto)
+ }
+
+ fun onGalleryClicked() {
+ if (isOneFileOnly && filesToUpload.isNotEmpty()) {
+ _events.value = Event(FileUploadAction.ShowToast(resources.getString(R.string.oneFileOnly)))
+ return
+ }
+
+ if (isOneFileOnly) {
+ _events.value = Event(FileUploadAction.PickImage)
+ } else {
+ _events.value = Event(FileUploadAction.PickMultipleImage)
+ }
+ }
+
+ fun onFilesClicked() {
+ if (isOneFileOnly && filesToUpload.isNotEmpty()) {
+ _events.value = Event(FileUploadAction.ShowToast(resources.getString(R.string.oneFileOnly)))
+ return
+ }
+
+ if (isOneFileOnly) {
+ _events.value = Event(FileUploadAction.PickFile)
+ } else {
+ _events.value = Event(FileUploadAction.PickMultipleFile)
+ }
+ }
+
+ fun addFile(fileUri: Uri) {
+ val submitObject = getUriContents(fileUri)
+ if (submitObject != null) {
+ if (submitObject.errorMessage.isNullOrEmpty()) {
+ val added = addIfExtensionAllowed(fileUri, submitObject)
+ if (added) {
+ updateItems()
+ }
+ } else {
+ _events.value = Event(FileUploadAction.ShowToast(submitObject.errorMessage
+ ?: resources.getString(R.string.errorOccurred)))
+ }
+ } else {
+ _events.value = Event(FileUploadAction.ShowToast(resources.getString(R.string.errorOccurred)))
+ }
+ }
+
+ fun addFiles(fileUris: List) {
+ fileUris.forEach {
+ addFile(it)
+ }
+ }
+
+ private fun updateItems() {
+ val itemViewModels = filesToUpload.map {
+ FileItemViewModel(FileItemViewData(
+ it.fileSubmitObject.name,
+ it.fileSubmitObject.size.humanReadableByteCount(),
+ it.fileSubmitObject.fullPath
+ ), this::onRemoveFileClicked)
+ }
+ _data.postValue(FileUploadDialogViewData(
+ setupAllowedExtensions(),
+ itemViewModels))
+ }
+
+ private fun onRemoveFileClicked(fullPath: String) {
+ filesToUpload.removeIf {
+ it.fileSubmitObject.fullPath == fullPath
+ }
+ updateItems()
+ }
+
+ private fun getUriContents(fileUri: Uri): FileSubmitObject? {
+ val mimeType = fileUploadUtils.getFileMimeType(fileUri)
+ val fileName = fileUploadUtils.getFileNameWithDefault(fileUri)
+
+ return fileUploadUtils.getFileSubmitObjectFromInputStream(fileUri, fileName, mimeType)
+ }
+
+ private fun addIfExtensionAllowed(uri: Uri, fileSubmitObject: FileSubmitObject): Boolean {
+ if (assignment != null && (assignment?.allowedExtensions == null || assignment?.allowedExtensions?.size == 0)) {
+ filesToUpload.add(FileUploadData(uri, fileSubmitObject))
+ return true
+ }
+
+ //get the extension and compare it to the list of allowed extensions
+ val index = fileSubmitObject.fullPath.lastIndexOf(".")
+ if (assignment != null && index != -1) {
+ val ext = fileSubmitObject.fullPath.substring(index + 1)
+ for (i in 0 until (assignment?.allowedExtensions?.size ?: 0)) {
+ if (assignment!!.allowedExtensions[i].trim { it <= ' ' }.equals(ext, ignoreCase = true)) {
+ filesToUpload.add(FileUploadData(uri, fileSubmitObject))
+ return true
+ }
+ }
+ _events.value = Event(FileUploadAction.ShowToast(resources.getString(R.string.extensionNotAllowed)))
+ return false
+ }
+
+ //if we're sharing it from an external source we won't know which assignment they're trying to
+ //submit to, so we won't know if there are any extension limits
+ //also, the assignment and/or course could be null due to memory pressures
+ if (assignment == null || canvasContext.id != 0L) {
+ filesToUpload.add(FileUploadData(uri, fileSubmitObject))
+ return true
+ }
+
+ _events.value = Event(FileUploadAction.ShowToast(resources.getString(R.string.extensionNotAllowed)))
+ return false
+ }
+
+ private fun checkIfFileSubmissionAllowed(): Boolean {
+ return assignment?.submissionTypesRaw?.contains(Assignment.SubmissionType.ONLINE_UPLOAD.apiString) ?: false
+ }
+
+ private fun setupAllowedExtensions(): String? {
+ return if (uploadType != FileUploadType.SUBMISSION_COMMENT && assignment != null && !assignment?.allowedExtensions.isNullOrEmpty()) {
+ assignment?.let {
+ var extensions = resources.getString(R.string.allowedExtensions)
+
+ for (i in 0 until it.allowedExtensions.size) {
+ extensions += it.allowedExtensions[i]
+ if (it.allowedExtensions.size > 1 && i < it.allowedExtensions.size - 1) {
+ extensions += ","
+ }
+ }
+ extensions
+ }
+ } else {
+ null
+ }
+ }
+
+ private fun isExtensionAllowed(filePath: String): Boolean {
+ if (assignment == null) {
+ _events.value = Event(FileUploadAction.ShowToast(resources.getString(R.string.noAssignmentSelected)))
+ return false
+ }
+ if (assignment!!.allowedExtensions.isEmpty()) return true
+
+ val extension = filePath.substringAfterLast(".")
+
+ return assignment!!.allowedExtensions.contains(extension)
+ }
+
+ fun uploadFiles() {
+ if (filesToUpload.size == 0) {
+ _events.value = Event(FileUploadAction.ShowToast(resources.getString(R.string.noFilesUploaded)))
+ } else {
+ if (uploadType == FileUploadType.ASSIGNMENT) {
+
+ if (!checkIfFileSubmissionAllowed()) { //see if we can actually submit files to this assignment
+ _events.value = Event(FileUploadAction.ShowToast(resources.getString(R.string.fileUploadNotSupported)))
+ return
+ }
+
+ filesToUpload.forEach {
+ if (!isExtensionAllowed(it.fileSubmitObject.fullPath)) {
+ _events.value = Event(FileUploadAction.ShowToast(resources.getString(R.string.oneOrMoreExtensionNotAllowed)))
+ return
+ }
+ }
+ }
+
+ val uris = filesToUpload.map { it.uri }
+
+ val data: Data = when (uploadType) {
+ FileUploadType.USER -> {
+ fileUploadBundleCreator.getUserFilesBundle(uris, parentFolderId)
+ .putString(FileUploadWorker.FILE_SUBMIT_ACTION, FileUploadWorker.ACTION_USER_FILE)
+ .build()
+ }
+ FileUploadType.COURSE -> {
+ fileUploadBundleCreator.getCourseFilesBundle(uris, canvasContext.id, parentFolderId)
+ .putString(FileUploadWorker.FILE_SUBMIT_ACTION, FileUploadWorker.ACTION_COURSE_FILE)
+ .build()
+ }
+ FileUploadType.GROUP -> {
+ fileUploadBundleCreator.getCourseFilesBundle(uris, canvasContext.id, parentFolderId)
+ .putString(FileUploadWorker.FILE_SUBMIT_ACTION, FileUploadWorker.ACTION_GROUP_FILE)
+ .build()
+ }
+ FileUploadType.MESSAGE -> {
+ fileUploadBundleCreator.getUserFilesBundle(uris, null)
+ .putString(FileUploadWorker.FILE_SUBMIT_ACTION, FileUploadWorker.ACTION_MESSAGE_ATTACHMENTS)
+ .build()
+ }
+ FileUploadType.DISCUSSION -> {
+ fileUploadBundleCreator.getUserFilesBundle(uris, null)
+ .putString(FileUploadWorker.FILE_SUBMIT_ACTION, FileUploadWorker.ACTION_DISCUSSION_ATTACHMENT)
+ .build()
+ }
+ FileUploadType.QUIZ -> {
+ fileUploadBundleCreator.getQuizFileBundle(uris, parentFolderId, quizQuestionId, position, canvasContext.id, quizId)
+ .putString(FileUploadWorker.FILE_SUBMIT_ACTION, FileUploadWorker.ACTION_QUIZ_FILE)
+ .build()
+ }
+ FileUploadType.SUBMISSION_COMMENT -> {
+ fileUploadBundleCreator.getSubmissionCommentBundle(uris, canvasContext.id, assignment!!)
+ .putString(FileUploadWorker.FILE_SUBMIT_ACTION, FileUploadWorker.ACTION_SUBMISSION_COMMENT)
+ .build()
+ }
+ FileUploadType.TEACHER_SUBMISSION_COMMENT -> {
+ fileUploadBundleCreator.getTeacherSubmissionCommentBundle(
+ uris,
+ assignment?.courseId.orDefault(),
+ assignment?.id.orDefault(),
+ userId.orDefault()
+ ).build()
+ }
+ else -> {
+ fileUploadBundleCreator.getAssignmentSubmissionBundle(uris, canvasContext.id, assignment!!)
+ .putString(FileUploadWorker.FILE_SUBMIT_ACTION, FileUploadWorker.ACTION_ASSIGNMENT_SUBMISSION)
+ .build()
+
+ }
+ }
+
+ startUpload(data)
+ }
+ }
+
+ private fun getAttachmentUri(): FileSubmitObject? {
+ return filesToUpload.firstOrNull()?.fileSubmitObject
+ }
+
+ private fun startUpload(data: Data) {
+ if (uploadType == FileUploadType.DISCUSSION) {
+ _events.value = Event(FileUploadAction.AttachmentSelectedAction(FileUploadDialogFragment.EVENT_ON_FILE_SELECTED, getAttachmentUri()))
+ } else {
+ val worker = OneTimeWorkRequestBuilder()
+ .setInputData(data)
+ .build()
+
+ _events.value = Event(FileUploadAction.UploadStartedAction(worker.id, workManager.getWorkInfoByIdLiveData(worker.id), filesToUpload.map { it.uri.toString() }))
+ workManager.enqueue(worker)
+ dialogCallback?.invoke(FileUploadDialogFragment.EVENT_ON_UPLOAD_BEGIN)
+ }
+ _events.value = Event(FileUploadAction.UploadStarted)
+ }
+
+ fun onCancelClicked() {
+ dialogCallback?.invoke(FileUploadDialogFragment.EVENT_DIALOG_CANCELED)
+ _events.value = Event(FileUploadAction.AttachmentSelectedAction(FileUploadDialogFragment.EVENT_DIALOG_CANCELED, null))
+ }
+}
\ No newline at end of file
diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/file/upload/FileUploadType.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/file/upload/FileUploadType.kt
new file mode 100644
index 0000000000..ca3b228e71
--- /dev/null
+++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/file/upload/FileUploadType.kt
@@ -0,0 +1,21 @@
+/*
+ * Copyright (C) 2022 - present Instructure, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.instructure.pandautils.features.file.upload
+
+enum class FileUploadType {
+ ASSIGNMENT, COURSE, USER, MESSAGE, DISCUSSION, QUIZ, SUBMISSION_COMMENT, GROUP, TEACHER_SUBMISSION_COMMENT
+}
\ No newline at end of file
diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/file/upload/FileUploadUtilsHelper.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/file/upload/FileUploadUtilsHelper.kt
new file mode 100644
index 0000000000..81ed056b19
--- /dev/null
+++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/file/upload/FileUploadUtilsHelper.kt
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2022 - present Instructure, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.instructure.pandautils.features.file.upload
+
+import android.content.ContentResolver
+import android.content.Context
+import android.net.Uri
+import com.instructure.canvasapi2.models.postmodels.FileSubmitObject
+import com.instructure.pandautils.utils.FileUploadUtils
+
+class FileUploadUtilsHelper(private val fileUploadUtils: FileUploadUtils, private val context: Context, private val contentResolver: ContentResolver) {
+ fun getFileMimeType(fileUri: Uri): String {
+ return fileUploadUtils.getFileMimeType(contentResolver, fileUri)
+ }
+
+ fun getFileNameWithDefault(fileUri: Uri): String {
+ return fileUploadUtils.getFileNameWithDefault(contentResolver, fileUri)
+ }
+
+ fun getFileSubmitObjectFromInputStream(fileUri: Uri, fileName: String, mimeType: String): FileSubmitObject? {
+ return fileUploadUtils.getFileSubmitObjectFromInputStream(context, fileUri, fileName, mimeType)
+ }
+}
\ No newline at end of file
diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/file/upload/itemviewmodels/FileItemViewModel.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/file/upload/itemviewmodels/FileItemViewModel.kt
new file mode 100644
index 0000000000..b2672a21ab
--- /dev/null
+++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/file/upload/itemviewmodels/FileItemViewModel.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2022 - present Instructure, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.instructure.pandautils.features.file.upload.itemviewmodels
+
+import com.instructure.pandautils.R
+import com.instructure.pandautils.features.file.upload.FileItemViewData
+import com.instructure.pandautils.mvvm.ItemViewModel
+
+class FileItemViewModel(
+ val data: FileItemViewData,
+ val onRemoveClick: (String) -> Unit
+) : ItemViewModel {
+ override val layoutId: Int
+ get() = R.layout.adapter_file_uploads
+}
\ No newline at end of file
diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/file/upload/preferences/FileUploadPreferences.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/file/upload/preferences/FileUploadPreferences.kt
new file mode 100644
index 0000000000..54b0ecc650
--- /dev/null
+++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/file/upload/preferences/FileUploadPreferences.kt
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2022 - present Instructure, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.instructure.pandautils.features.file.upload.preferences
+
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import com.instructure.canvasapi2.utils.PrefManager
+import com.instructure.canvasapi2.utils.StringSetPref
+import java.util.*
+
+object FileUploadPreferences : PrefManager("fileUploadPrefs") {
+
+ private var runningWorkerIds by StringSetPref()
+ private var runningWorkersLiveData: MutableLiveData>? = null
+
+ fun addWorkerId(id: UUID) {
+ runningWorkerIds = runningWorkerIds + id.toString()
+ runningWorkersLiveData?.postValue(getRunningWorkerIds())
+ }
+
+ fun removeWorkerId(id: UUID) {
+ val idString = id.toString()
+ if (runningWorkerIds.contains(idString)) {
+ runningWorkerIds = runningWorkerIds - id.toString()
+ runningWorkersLiveData?.postValue(getRunningWorkerIds())
+ }
+ }
+
+ fun getRunningWorkerIds(): List {
+ return runningWorkerIds.map { UUID.fromString(it) }
+ }
+
+ fun getRunningWorkersLiveData(): LiveData> {
+ if (runningWorkersLiveData == null) runningWorkersLiveData = MutableLiveData(getRunningWorkerIds())
+ return runningWorkersLiveData!!
+ }
+}
\ No newline at end of file
diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/file/upload/worker/FileUploadBundleCreator.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/file/upload/worker/FileUploadBundleCreator.kt
new file mode 100644
index 0000000000..853d455934
--- /dev/null
+++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/file/upload/worker/FileUploadBundleCreator.kt
@@ -0,0 +1,114 @@
+/*
+ * Copyright (C) 2022 - present Instructure, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.instructure.pandautils.features.file.upload.worker
+
+import android.net.Uri
+import androidx.work.Data
+import com.instructure.canvasapi2.models.Assignment
+import com.instructure.pandautils.utils.Const
+
+class FileUploadBundleCreator {
+
+ fun getUserFilesBundle(
+ fileSubmitObjects: List,
+ parentFolderId: Long?
+ ) = Data.Builder()
+ .putStringArray(FileUploadWorker.FILE_PATHS, fileSubmitObjects.map { it.toString() }.toTypedArray())
+ .putLong(Const.PARENT_FOLDER_ID, parentFolderId ?: FileUploadWorker.INVALID_ID)
+
+ fun getQuizFileBundle(
+ fileSubmitObjects: List,
+ parentFolderId: Long?,
+ quizQuestionId: Long,
+ position: Int,
+ courseId: Long,
+ quizId: Long
+ ) = Data.Builder()
+ .putStringArray(FileUploadWorker.FILE_PATHS, fileSubmitObjects.map { it.toString() }.toTypedArray())
+ .putLong(Const.QUIZ_ANSWER_ID, quizQuestionId)
+ .putLong(Const.QUIZ, quizId)
+ .putLong(Const.COURSE_ID, courseId)
+ .putInt(Const.POSITION, position)
+ .putLong(Const.PARENT_FOLDER_ID, parentFolderId ?: FileUploadWorker.INVALID_ID)
+
+ fun getCourseFilesBundle(
+ fileSubmitObjects: List,
+ courseId: Long,
+ parentFolderId: Long?
+ ) = Data.Builder()
+ .putStringArray(FileUploadWorker.FILE_PATHS, fileSubmitObjects.map { it.toString() }.toTypedArray())
+ .putLong(Const.COURSE_ID, courseId)
+ .putLong(Const.PARENT_FOLDER_ID, parentFolderId ?: FileUploadWorker.INVALID_ID)
+
+ fun getAssignmentSubmissionBundle(
+ fileSubmitObjects: List,
+ courseId: Long,
+ assignment: Assignment,
+ dbSubmissionId: Long? = null,
+ additionalAttachmentIds: ArrayList? = null
+ ) = Data.Builder()
+ .putStringArray(FileUploadWorker.FILE_PATHS, fileSubmitObjects.map { it.toString() }.toTypedArray())
+ .putLong(Const.COURSE_ID, courseId)
+ .putLong(Const.ASSIGNMENT_ID, assignment.id)
+ .putLong(Const.SUBMISSION_ID, dbSubmissionId ?: FileUploadWorker.INVALID_ID)
+ .putLongArray(Const.ATTACHMENTS, additionalAttachmentIds?.toLongArray() ?: longArrayOf())
+
+ fun getMessageBundle(
+ fileSubmitObjects: List,
+ messageText: String,
+ conversationId: Long
+ ) = Data.Builder()
+ .putStringArray(FileUploadWorker.FILE_PATHS, fileSubmitObjects.map { it.toString() }.toTypedArray())
+ .putLong(Const.CONVERSATION, conversationId)
+ .putString(Const.MESSAGE, messageText)
+
+ fun getNewMessageBundle(
+ fileSubmitObjects: List,
+ userIds: ArrayList,
+ subject: String,
+ messageText: String,
+ isGroup: Boolean,
+ contextId: String
+ ) = Data.Builder()
+ .putStringArray(FileUploadWorker.FILE_PATHS, fileSubmitObjects.map { it.toString() }.toTypedArray())
+ .putStringArray(Const.USER_IDS, userIds.toTypedArray())
+ .putString(Const.SUBJECT, subject)
+ .putString(Const.MESSAGE, messageText)
+ .putBoolean(Const.IS_GROUP, isGroup)
+ .putString(Const.CONTEXT_ID, contextId)
+
+ fun getSubmissionCommentBundle(
+ fileSubmitObjects: List,
+ courseId: Long,
+ assignment: Assignment
+ ) = Data.Builder()
+ .putStringArray(FileUploadWorker.FILE_PATHS, fileSubmitObjects.map { it.toString() }.toTypedArray())
+ .putLong(Const.COURSE_ID, courseId)
+ .putLong(Const.ASSIGNMENT_ID, assignment.id)
+
+ fun getTeacherSubmissionCommentBundle(
+ fileSubmitObjects: List,
+ courseId: Long,
+ assignmentId: Long,
+ userId: Long
+ ) = Data.Builder()
+ .putStringArray(FileUploadWorker.FILE_PATHS, fileSubmitObjects.map { it.toString() }.toTypedArray())
+ .putLong(Const.COURSE_ID, courseId)
+ .putLong(Const.ASSIGNMENT_ID, assignmentId)
+ .putLong(Const.USER_ID, userId)
+ .putString(FileUploadWorker.FILE_SUBMIT_ACTION, FileUploadWorker.ACTION_TEACHER_SUBMISSION_COMMENT)
+}
\ No newline at end of file
diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/file/upload/worker/FileUploadWorker.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/file/upload/worker/FileUploadWorker.kt
new file mode 100644
index 0000000000..2926d8e0bc
--- /dev/null
+++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/file/upload/worker/FileUploadWorker.kt
@@ -0,0 +1,364 @@
+/*
+ * Copyright (C) 2022 - present Instructure, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.instructure.pandautils.features.file.upload.worker
+
+import android.app.NotificationChannel
+import android.app.NotificationManager
+import android.content.Context
+import android.net.Uri
+import androidx.core.app.NotificationCompat
+import androidx.work.*
+import com.instructure.canvasapi2.managers.*
+import com.instructure.canvasapi2.models.Assignment
+import com.instructure.canvasapi2.models.Attachment
+import com.instructure.canvasapi2.models.Submission
+import com.instructure.canvasapi2.models.postmodels.FileSubmitObject
+import com.instructure.canvasapi2.utils.ContextKeeper
+import com.instructure.canvasapi2.utils.ProgressRequestUpdateListener
+import com.instructure.canvasapi2.utils.weave.awaitApi
+import com.instructure.pandautils.R
+import com.instructure.pandautils.features.file.upload.FileUploadUtilsHelper
+import com.instructure.pandautils.features.file.upload.preferences.FileUploadPreferences
+import com.instructure.pandautils.utils.Const
+import com.instructure.pandautils.utils.FileUploadUtils
+import com.instructure.pandautils.utils.toJson
+import java.util.*
+
+class FileUploadWorker(private val context: Context, workerParameters: WorkerParameters) :
+ CoroutineWorker(context, workerParameters) {
+
+ private val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
+
+ private val courseId = inputData.getLong(Const.COURSE_ID, INVALID_ID)
+ private val assignmentId = inputData.getLong(Const.ASSIGNMENT_ID, INVALID_ID)
+ private val quizQuestionId = inputData.getLong(Const.QUIZ_ANSWER_ID, INVALID_ID)
+ private val quizId = inputData.getLong(Const.QUIZ, INVALID_ID)
+ private val position = inputData.getInt(Const.POSITION, INVALID_ID.toInt())
+ private val parentFolderId = inputData.getLong(Const.PARENT_FOLDER_ID, INVALID_ID)
+ private val notificationId = notificationId(inputData)
+ private val submissionId = inputData.getLong(Const.SUBMISSION_ID, INVALID_ID)
+ private val action = inputData.getString(FILE_SUBMIT_ACTION)
+ private val userId = inputData.getLong(Const.USER_ID, INVALID_ID)
+
+ private var fullSize = 0L
+ private var currentProgress = 0L
+ private var uploaded = 0L
+
+ private var uploadCount = 0
+
+ private val workDataBuilder = Data.Builder()
+
+ override suspend fun doWork(): Result {
+ try {
+ val filePaths = inputData.getStringArray(FILE_PATHS)
+
+ val fileSubmitObjects = filePaths?.let {
+ getFileSubmitObjects(it)
+ } ?: throw IllegalArgumentException()
+ fullSize = fileSubmitObjects.sumOf { it.size }
+ uploadCount = fileSubmitObjects.size
+
+ val title = context.getString(
+ if (action == ACTION_ASSIGNMENT_SUBMISSION) {
+ R.string.dashboardNotificationUploadingSubmissionTitle
+ } else {
+ R.string.dashboardNotificationUploadingFilesTitle
+ }
+ )
+
+ var assignmentName = ""
+ var groupId: Long? = null
+ if (assignmentId != INVALID_ID && courseId != INVALID_ID) {
+ val assignment = getAssignment(assignmentId, courseId)
+ groupId = getGroupId(assignment, courseId)
+ assignmentName = assignment.name.orEmpty()
+ }
+
+ setProgress(
+ workDataBuilder
+ .putString(PROGRESS_DATA_TITLE, title)
+ .putString(PROGRESS_DATA_ASSIGNMENT_NAME, assignmentName)
+ .putLong(PROGRESS_DATA_FULL_SIZE, fullSize)
+ .putStringArray(PROGRESS_DATA_FILES_TO_UPLOAD, fileSubmitObjects.map {
+ it.toJson()
+ }.toTypedArray())
+ .build()
+ )
+
+ FileUploadPreferences.addWorkerId(id)
+
+ val attachments = uploadFiles(fileSubmitObjects, groupId)
+
+ val attachmentsIds = attachments.map { it.id }.plus(
+ inputData.getLongArray(Const.ATTACHMENTS)?.toList()
+ ?: emptyList()
+ )
+
+ val result = when (action) {
+ ACTION_ASSIGNMENT_SUBMISSION -> {
+ submitAttachmentsToSubmission(attachmentsIds)?.let {
+ updateSubmissionComplete(notificationId)
+ Result.success()
+ } ?: Result.retry()
+ }
+ ACTION_MESSAGE_ATTACHMENTS -> {
+ updateNotificationComplete(notificationId)
+ val attachmentJsons = attachments.map { it.toJson() }.toTypedArray()
+ Result.success(workDataOf(RESULT_ATTACHMENTS to attachmentJsons))
+ }
+ ACTION_TEACHER_SUBMISSION_COMMENT -> {
+ postSubmissionComment(attachmentsIds).let {
+ updateSubmissionComplete(notificationId)
+ Result.success(workDataOf(RESULT_SUBMISSION_COMMENT to it.submissionComments.lastOrNull()?.toJson()))
+ }
+ }
+ else -> {
+ updateNotificationComplete(notificationId)
+ Result.success()
+ }
+ }
+
+ FileUploadPreferences.removeWorkerId(id)
+ return result
+ } catch (e: Exception) {
+ FileUploadPreferences.removeWorkerId(id)
+ e.printStackTrace()
+ return Result.failure()
+ }
+ }
+
+ private fun getFileSubmitObjects(filePaths: Array): List {
+ val fileSubmitObjects = filePaths.map {
+ val uri = Uri.parse(it)
+ val fileUploadUtilsHelper = FileUploadUtilsHelper(FileUploadUtils, context, context.contentResolver)
+ val mimeType = fileUploadUtilsHelper.getFileMimeType(uri)
+ val fileName = fileUploadUtilsHelper.getFileNameWithDefault(uri)
+
+ fileUploadUtilsHelper.getFileSubmitObjectFromInputStream(uri, fileName, mimeType)
+ }
+ if (fileSubmitObjects.contains(null)) throw IllegalArgumentException("Could not parse file.")
+
+ return fileSubmitObjects.filterNotNull()
+ }
+
+ private suspend fun getAssignment(assignmentId: Long, courseId: Long): Assignment {
+ return AssignmentManager.getAssignmentAsync(assignmentId, courseId, true).await().dataOrThrow
+ }
+
+ private suspend fun getGroupId(assignment: Assignment, courseId: Long): Long? {
+ return if (assignment.groupCategoryId != 0L) {
+ GroupManager.getAllGroupsForCourseAsync(courseId, true).await().dataOrThrow
+ .find { it.groupCategoryId == assignment.groupCategoryId }?.id ?: throw IllegalArgumentException()
+ } else {
+ null
+ }
+ }
+
+ private suspend fun uploadFiles(fileSubmitObjects: List, groupId: Long?): List {
+ val attachments = mutableListOf()
+
+ fileSubmitObjects.forEachIndexed { index, fileSubmitObject ->
+ setForeground(createForegroundInfo(notificationId, fileSubmitObject.name, index + 1))
+ val config: FileUploadConfig = when (action) {
+ ACTION_ASSIGNMENT_SUBMISSION -> {
+ if (groupId == null) {
+ FileUploadConfig.forSubmission(fileSubmitObject, courseId, assignmentId)
+ } else {
+ FileUploadConfig.forGroup(fileSubmitObject, groupId)
+ }
+ }
+ ACTION_COURSE_FILE -> FileUploadConfig.forCourse(
+ fileSubmitObject,
+ courseId,
+ if (parentFolderId != INVALID_ID) parentFolderId else null
+ )
+ ACTION_GROUP_FILE -> FileUploadConfig.forGroup(
+ fileSubmitObject,
+ courseId,
+ if (parentFolderId != INVALID_ID) parentFolderId else null
+ )
+ ACTION_USER_FILE -> FileUploadConfig.forUser(
+ fileSubmitObject,
+ if (parentFolderId != INVALID_ID) parentFolderId else null
+ )
+ ACTION_MESSAGE_ATTACHMENTS -> FileUploadConfig.forUser(
+ fileSubmitObject,
+ parentFolderPath = MESSAGE_ATTACHMENT_PATH
+ )
+ ACTION_QUIZ_FILE -> FileUploadConfig.forQuiz(fileSubmitObject, courseId, quizId)
+ ACTION_DISCUSSION_ATTACHMENT -> FileUploadConfig.forUser(
+ fileSubmitObject,
+ parentFolderPath = DISCUSSION_ATTACHMENT_PATH
+ )
+ ACTION_SUBMISSION_COMMENT -> FileUploadConfig.forSubmissionComment(
+ fileSubmitObject,
+ courseId,
+ assignmentId
+ )
+ ACTION_TEACHER_SUBMISSION_COMMENT -> FileUploadConfig.forSubmissionCommentFromTeacher(
+ fileSubmitObject,
+ courseId,
+ assignmentId,
+ userId
+ )
+ else -> throw IllegalArgumentException("Unknown file upload action: $action")
+ }
+
+ attachments += FileUploadManager.uploadFile(config, object : ProgressRequestUpdateListener {
+ override fun onProgressUpdated(progressPercent: Float, length: Long): Boolean {
+ currentProgress = uploaded + (fileSubmitObject.size * progressPercent).toLong()
+ setProgressAsync(
+ workDataBuilder
+ .putLong(PROGRESS_DATA_UPLOADED_SIZE, currentProgress)
+ .build()
+ )
+ return !this@FileUploadWorker.isStopped
+ }
+ }).dataOrThrow
+
+ val updatedList =
+ workDataBuilder.build().getStringArray(PROGRESS_DATA_UPLOADED_FILES).orEmpty().toMutableList().apply {
+ add(fileSubmitObject.toJson())
+ }.toTypedArray()
+ uploaded += fileSubmitObject.size
+ currentProgress = uploaded
+
+ setProgress(
+ workDataBuilder
+ .putStringArray(PROGRESS_DATA_UPLOADED_FILES, updatedList)
+ .putLong(PROGRESS_DATA_UPLOADED_SIZE, currentProgress)
+ .build()
+ )
+ }
+ return attachments
+ }
+
+ private fun submitAttachmentsToSubmission(attachmentIds: List): Submission? {
+ return SubmissionManager.postSubmissionAttachmentsSynchronous(courseId, assignmentId, attachmentIds)
+ }
+
+ private suspend fun postSubmissionComment(attachmentIds: List) = awaitApi {
+ SubmissionManager.postSubmissionComment(
+ courseId,
+ assignmentId,
+ userId,
+ "",
+ false,
+ attachmentIds,
+ it
+ )
+ }
+
+ private fun notificationId(data: Data): Int {
+ return if (data.getLong(Const.SUBMISSION_ID, INVALID_ID) != INVALID_ID) {
+ data.getLong(Const.SUBMISSION_ID, INVALID_ID).toInt()
+ } else {
+ NOTIFICATION_ID
+ }
+ }
+
+ private fun createNotificationChannel(notificationManager: NotificationManager, channelId: String = CHANNEL_ID) {
+ // Prevents recreation of notification channel if it exists.
+ if (notificationManager.notificationChannels.any { it.id == channelId }) return
+
+ val name = ContextKeeper.appContext.getString(R.string.notificationChannelNameFileUploadsName)
+ val description = ContextKeeper.appContext.getString(R.string.notificationChannelNameFileUploadsDescription)
+
+ // Create the channel and add the group
+ val importance = NotificationManager.IMPORTANCE_HIGH
+ val channel = NotificationChannel(channelId, name, importance)
+ channel.description = description
+ channel.enableLights(false)
+ channel.enableVibration(false)
+ channel.setSound(null, null)
+
+ // Create the channel
+ notificationManager.createNotificationChannel(channel)
+ }
+
+ private fun createForegroundInfo(notificationId: Int, fileName: String, currentItem: Int): ForegroundInfo {
+ createNotificationChannel(notificationManager)
+
+ val notification = NotificationCompat.Builder(applicationContext, CHANNEL_ID)
+ .setSmallIcon(R.drawable.ic_notification_canvas_logo)
+ .setContentTitle(
+ String.format(
+ Locale.US,
+ context.getString(R.string.uploadingFileNum),
+ currentItem,
+ uploadCount
+ )
+ )
+ .setContentText(fileName)
+ .setProgress(0, 0, true)
+ .setOngoing(true)
+ .build()
+
+ return ForegroundInfo(notificationId, notification)
+ }
+
+ private fun updateNotificationComplete(notificationId: Int) {
+ val notification = NotificationCompat.Builder(applicationContext, CHANNEL_ID)
+ .setSmallIcon(R.drawable.ic_notification_canvas_logo)
+ .setProgress(0, 0, false)
+ .setOngoing(false)
+ .setContentTitle(context.getString(R.string.filesUploadedSuccessfully))
+ .build()
+ notificationManager.notify(notificationId + 1, notification)
+ }
+
+ private fun updateSubmissionComplete(notificationId: Int) {
+ val notification = NotificationCompat.Builder(applicationContext, CHANNEL_ID)
+ .setSmallIcon(R.drawable.ic_notification_canvas_logo)
+ .setProgress(0, 0, false)
+ .setOngoing(false)
+ .setContentTitle(context.getString(R.string.filesSubmittedSuccessfully))
+ .build()
+ notificationManager.notify(notificationId + 1, notification)
+ }
+
+ companion object {
+ private const val NOTIFICATION_ID = -2
+ const val INVALID_ID = -1L
+ const val FILE_SUBMIT_ACTION = "fileSubmitAction"
+ const val FILE_PATHS = "filePaths"
+ const val CHANNEL_ID = "uploadChannel"
+
+ const val ACTION_ASSIGNMENT_SUBMISSION = "ACTION_ASSIGNMENT_SUBMISSION"
+ const val ACTION_MESSAGE_ATTACHMENTS = "ACTION_MESSAGE_ATTACHMENTS"
+ const val ACTION_COURSE_FILE = "ACTION_COURSE_FILE"
+ const val ACTION_GROUP_FILE = "ACTION_GROUP_FILE"
+ const val ACTION_USER_FILE = "ACTION_USER_FILE"
+ const val ACTION_QUIZ_FILE = "ACTION_QUIZ_FILE"
+ const val ACTION_DISCUSSION_ATTACHMENT = "ACTION_DISCUSSION_ATTACHMENT"
+ const val ACTION_SUBMISSION_COMMENT = "ACTION_SUBMISSION_COMMENT"
+ const val ACTION_TEACHER_SUBMISSION_COMMENT = "ACTION_SUBMISSION_COMMENT_TEACHER"
+
+ const val MESSAGE_ATTACHMENT_PATH = "conversation attachments"
+ const val DISCUSSION_ATTACHMENT_PATH = "discussion attachments"
+
+ const val RESULT_ATTACHMENTS = "attachments"
+ const val RESULT_SUBMISSION_COMMENT = "submission-comment"
+
+ const val PROGRESS_DATA_FILES_TO_UPLOAD = "filesToUpload"
+ const val PROGRESS_DATA_UPLOADED_FILES = "uploadedFiles"
+ const val PROGRESS_DATA_ASSIGNMENT_NAME = "assignmentName"
+ const val PROGRESS_DATA_FULL_SIZE = "fullSize"
+ const val PROGRESS_DATA_UPLOADED_SIZE = "uploadedSize"
+ const val PROGRESS_DATA_TITLE = "PROGRESS_DATA_TITLE"
+ }
+}
\ No newline at end of file
diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/notification/preferences/EmailNotificationPreferencesFragment.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/notification/preferences/EmailNotificationPreferencesFragment.kt
new file mode 100644
index 0000000000..71b0c8cff1
--- /dev/null
+++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/notification/preferences/EmailNotificationPreferencesFragment.kt
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2022 - present Instructure, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+package com.instructure.pandautils.features.notification.preferences
+
+import android.content.DialogInterface
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.appcompat.app.AlertDialog
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.viewModels
+import com.google.android.material.snackbar.Snackbar
+import com.instructure.canvasapi2.managers.NotificationPreferencesFrequency
+import com.instructure.pandautils.R
+import com.instructure.pandautils.databinding.FragmentNotificationPreferencesBinding
+import com.instructure.pandautils.utils.ViewStyler
+import com.instructure.pandautils.utils.setupAsBackButton
+import dagger.hilt.android.AndroidEntryPoint
+
+@AndroidEntryPoint
+class EmailNotificationPreferencesFragment : Fragment() {
+
+ private val viewModel: EmailNotificationPreferencesViewModel by viewModels()
+
+ private lateinit var binding: FragmentNotificationPreferencesBinding
+
+ override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
+ savedInstanceState: Bundle?): View {
+ binding = FragmentNotificationPreferencesBinding.inflate(inflater, container, false)
+ binding.lifecycleOwner = this.viewLifecycleOwner
+ binding.viewModel = viewModel
+ binding.title = resources.getString(R.string.emailNotifications)
+ return binding.root
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ setupToolbar()
+
+ viewModel.events.observe(viewLifecycleOwner) { event ->
+ event.getContentIfNotHandled()?.let {
+ handleAction(it)
+ }
+ }
+ }
+
+ private fun handleAction(action: NotificationPreferencesAction) {
+ when (action) {
+ is NotificationPreferencesAction.ShowSnackbar -> Snackbar.make(requireView(), action.snackbar, Snackbar.LENGTH_LONG).show()
+ is NotificationPreferencesAction.ShowFrequencySelectionDialog -> showFrequencySelectionDialog(action.categoryName, action.selectedFrequency)
+ }
+ }
+
+ private fun showFrequencySelectionDialog(categoryName: String, selectedFrequency: NotificationPreferencesFrequency) {
+ val items = NotificationPreferencesFrequency.values().map { resources.getString(it.stringRes) }.toTypedArray()
+ val selectedIndex = NotificationPreferencesFrequency.values().indexOf(selectedFrequency)
+ AlertDialog.Builder(requireContext(), R.style.AccentDialogTheme)
+ .setTitle(R.string.selectFrequency)
+ .setSingleChoiceItems(items, selectedIndex, { dialog, index -> frequencySelected(dialog, index, categoryName)})
+ .setNegativeButton(R.string.sortByDialogCancel) { dialog, _ -> dialog.dismiss() }
+ .show()
+ }
+
+ private fun frequencySelected(dialog: DialogInterface, index: Int, categoryName: String) {
+ val selectedFrequency = NotificationPreferencesFrequency.values()[index]
+ viewModel.updateFrequency(categoryName, selectedFrequency)
+ dialog.dismiss()
+ }
+
+ private fun setupToolbar() {
+ binding.toolbar.setupAsBackButton { requireActivity().onBackPressed() }
+ ViewStyler.themeToolbarLight(requireActivity(), binding.toolbar)
+ }
+
+ companion object {
+ fun newInstance() = EmailNotificationPreferencesFragment()
+ }
+}
\ No newline at end of file
diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/notification/preferences/EmailNotificationPreferencesViewModel.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/notification/preferences/EmailNotificationPreferencesViewModel.kt
new file mode 100644
index 0000000000..d9b1dc0b57
--- /dev/null
+++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/notification/preferences/EmailNotificationPreferencesViewModel.kt
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2022 - present Instructure, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+package com.instructure.pandautils.features.notification.preferences
+
+import android.content.res.Resources
+import androidx.lifecycle.viewModelScope
+import com.instructure.canvasapi2.managers.CommunicationChannelsManager
+import com.instructure.canvasapi2.managers.NotificationPreferencesFrequency
+import com.instructure.canvasapi2.managers.NotificationPreferencesManager
+import com.instructure.canvasapi2.utils.ApiPrefs
+import com.instructure.pandautils.BR
+import com.instructure.pandautils.R
+import com.instructure.pandautils.features.notification.preferences.itemviewmodels.EmailNotificationCategoryItemViewModel
+import com.instructure.pandautils.features.notification.preferences.itemviewmodels.NotificationCategoryItemViewModel
+import com.instructure.pandautils.mvvm.Event
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.launch
+import javax.inject.Inject
+
+@HiltViewModel
+class EmailNotificationPreferencesViewModel @Inject constructor(
+ communicationChannelsManager: CommunicationChannelsManager,
+ notificationPreferencesManager: NotificationPreferencesManager,
+ apiPrefs: ApiPrefs,
+ notificationPreferenceUtils: NotificationPreferenceUtils,
+ resources: Resources
+): NotificationPreferencesViewModel(communicationChannelsManager, notificationPreferencesManager, apiPrefs, notificationPreferenceUtils, resources) {
+
+ override val notificationChannelType: String
+ get() = "email"
+
+ override fun createCategoryItemViewModel(viewData: NotificationCategoryViewData): NotificationCategoryItemViewModel {
+ return EmailNotificationCategoryItemViewModel(viewData, resources, ::notificationCategorySelected)
+ }
+
+ private fun notificationCategorySelected(categoryName: String, frequency: NotificationPreferencesFrequency) {
+ _events.postValue(Event(NotificationPreferencesAction.ShowFrequencySelectionDialog(categoryName, frequency)))
+ }
+
+ fun updateFrequency(categoryName: String, selectedFrequency: NotificationPreferencesFrequency) {
+ val selectedItem = _data.value?.items?.flatMap { it.itemViewModels }?.find { it.data.categoryName == categoryName } as? EmailNotificationCategoryItemViewModel
+ if (selectedItem == null) return
+
+ val previousFrequency = selectedItem.data.frequency
+ updateItemFrequency(selectedItem, selectedFrequency)
+
+ viewModelScope.launch {
+ try {
+ val channel = communicationChannel
+ if (channel != null) {
+ notificationPreferencesManager.updatePreferenceCategoryAsync(categoryName, channel.id, selectedFrequency.apiString).await().dataOrThrow
+ } else {
+ _events.postValue(Event(NotificationPreferencesAction.ShowSnackbar(resources.getString(R.string.errorOccurred))))
+ }
+ } catch (e: Exception) {
+ e.printStackTrace()
+ updateItemFrequency(selectedItem, previousFrequency)
+ _events.postValue(Event(NotificationPreferencesAction.ShowSnackbar(resources.getString(R.string.errorOccurred))))
+ }
+ }
+ }
+
+ private fun updateItemFrequency(item: NotificationCategoryItemViewModel, frequency: NotificationPreferencesFrequency) {
+ item.data.frequency = frequency
+ item.notifyPropertyChanged(BR.frequency)
+ }
+}
\ No newline at end of file
diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/notification/preferences/NotificationPreferencesViewData.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/notification/preferences/NotificationPreferencesViewData.kt
index 905b0db23f..c64c203a4c 100644
--- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/notification/preferences/NotificationPreferencesViewData.kt
+++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/notification/preferences/NotificationPreferencesViewData.kt
@@ -16,6 +16,7 @@
package com.instructure.pandautils.features.notification.preferences
+import com.instructure.canvasapi2.managers.NotificationPreferencesFrequency
import com.instructure.pandautils.features.notification.preferences.itemviewmodels.NotificationCategoryHeaderItemViewModel
data class NotificationPreferencesViewData(val items: List)
@@ -26,7 +27,7 @@ data class NotificationCategoryViewData(
val name: String,
val title: String?,
val description: String?,
- var frequency: String,
+ var frequency: NotificationPreferencesFrequency,
val position: Int,
val notification: String?
) {
@@ -36,9 +37,11 @@ data class NotificationCategoryViewData(
enum class NotificationPreferencesViewType(val viewType: Int) {
HEADER(0),
- CATEGORY(1)
+ PUSH_CATEGORY(1),
+ EMAIL_CATEGORY(2)
}
sealed class NotificationPreferencesAction {
data class ShowSnackbar(val snackbar: String): NotificationPreferencesAction()
+ data class ShowFrequencySelectionDialog(val categoryName: String, val selectedFrequency: NotificationPreferencesFrequency): NotificationPreferencesAction()
}
\ No newline at end of file
diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/notification/preferences/NotificationPreferencesViewModel.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/notification/preferences/NotificationPreferencesViewModel.kt
index 572ea68a1f..31388183c0 100644
--- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/notification/preferences/NotificationPreferencesViewModel.kt
+++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/notification/preferences/NotificationPreferencesViewModel.kt
@@ -22,27 +22,24 @@ import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.instructure.canvasapi2.managers.CommunicationChannelsManager
+import com.instructure.canvasapi2.managers.NotificationPreferencesFrequency
import com.instructure.canvasapi2.managers.NotificationPreferencesManager
import com.instructure.canvasapi2.models.CommunicationChannel
import com.instructure.canvasapi2.models.NotificationPreference
import com.instructure.canvasapi2.utils.ApiPrefs
-import com.instructure.pandautils.BR
import com.instructure.pandautils.R
import com.instructure.pandautils.features.notification.preferences.itemviewmodels.NotificationCategoryHeaderItemViewModel
import com.instructure.pandautils.features.notification.preferences.itemviewmodels.NotificationCategoryItemViewModel
import com.instructure.pandautils.mvvm.Event
import com.instructure.pandautils.mvvm.ViewState
-import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.launch
-import javax.inject.Inject
-@HiltViewModel
-class NotificationPreferencesViewModel @Inject constructor(
+abstract class NotificationPreferencesViewModel (
private val communicationChannelsManager: CommunicationChannelsManager,
- private val notificationPreferencesManager: NotificationPreferencesManager,
+ protected val notificationPreferencesManager: NotificationPreferencesManager,
private val apiPrefs: ApiPrefs,
private val notificationPreferenceUtils: NotificationPreferenceUtils,
- private val resources: Resources
+ protected val resources: Resources
) : ViewModel() {
val state: LiveData
get() = _state
@@ -50,13 +47,15 @@ class NotificationPreferencesViewModel @Inject constructor(
val data: LiveData
get() = _data
- private val _data = MutableLiveData()
+ protected val _data = MutableLiveData()
val events: LiveData>
get() = _events
- private val _events = MutableLiveData>()
+ protected val _events = MutableLiveData>()
- private var pushChannel: CommunicationChannel? = null
+ protected var communicationChannel: CommunicationChannel? = null
+
+ abstract val notificationChannelType: String
init {
_state.postValue(ViewState.Loading)
@@ -73,8 +72,8 @@ class NotificationPreferencesViewModel @Inject constructor(
try {
apiPrefs.user?.let {
val communicationChannels = communicationChannelsManager.getCommunicationChannelsAsync(it.id, true).await().dataOrThrow
- pushChannel = communicationChannels.first { "push".equals(it.type, true) }
- pushChannel?.let { channel ->
+ communicationChannel = communicationChannels.first { notificationChannelType.equals(it.type, true) }
+ communicationChannel?.let { channel ->
val notificationPreferences = notificationPreferencesManager.getNotificationPreferencesAsync(channel.userId, channel.id, true).await().dataOrThrow
val items = groupNotifications(notificationPreferences.notificationPreferences)
@@ -106,16 +105,15 @@ class NotificationPreferencesViewModel @Inject constructor(
val categoryHelper = categoryHelperMap[categoryName] ?: continue
val header = groupHeaderMap[categoryHelper.categoryGroup] ?: continue
- val categoryItemViewModel = NotificationCategoryItemViewModel(
- data = NotificationCategoryViewData(
+ val categoryItemViewModel = createCategoryItemViewModel(
+ NotificationCategoryViewData(
categoryName,
titleMap[categoryName],
descriptionMap[categoryName],
- prefs[0].frequency,
+ NotificationPreferencesFrequency.fromApiString(prefs[0].frequency),
categoryHelper.position,
prefs[0].notification
- ),
- toggle = this::toggleNotification
+ )
)
if (categories[header] == null) {
categories[header] = arrayListOf(categoryItemViewModel)
@@ -132,33 +130,5 @@ class NotificationPreferencesViewModel @Inject constructor(
}.sortedBy { it.data.position }
}
- private fun toggleNotification(enabled: Boolean, categoryName: String) {
- viewModelScope.launch {
- try {
- pushChannel?.let {
- notificationPreferencesManager.updatePreferenceCategoryAsync(
- categoryName,
- it.id,
- enabled.frequency,
- ).await().dataOrThrow
- } ?: throw IllegalStateException()
- } catch (e: Exception) {
- e.printStackTrace()
- _data.value?.items?.forEach {
- val itemViewModel = it.itemViewModels.firstOrNull { it.data.categoryName == categoryName }
- itemViewModel?.let {
- it.apply {
- data.frequency = enabled.not().frequency
- notifyPropertyChanged(BR.checked)
- }
- return@forEach
- }
- }
- _events.postValue(Event(NotificationPreferencesAction.ShowSnackbar(resources.getString(R.string.errorOccurred))))
- }
- }
- }
-
- private val Boolean.frequency: String
- get() = if (this) NotificationPreferencesManager.IMMEDIATELY else NotificationPreferencesManager.NEVER
+ abstract fun createCategoryItemViewModel(viewData: NotificationCategoryViewData): NotificationCategoryItemViewModel
}
\ No newline at end of file
diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/notification/preferences/NotificationPreferencesFragment.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/notification/preferences/PushNotificationPreferencesFragment.kt
similarity index 81%
rename from libs/pandautils/src/main/java/com/instructure/pandautils/features/notification/preferences/NotificationPreferencesFragment.kt
rename to libs/pandautils/src/main/java/com/instructure/pandautils/features/notification/preferences/PushNotificationPreferencesFragment.kt
index bf65f0f9e2..5a680066ae 100644
--- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/notification/preferences/NotificationPreferencesFragment.kt
+++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/notification/preferences/PushNotificationPreferencesFragment.kt
@@ -24,26 +24,28 @@ import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import com.google.android.material.snackbar.Snackbar
import com.instructure.canvasapi2.utils.pageview.PageView
+import com.instructure.pandautils.R
import com.instructure.pandautils.analytics.SCREEN_VIEW_NOTIFICATION_PREFERENCES
import com.instructure.pandautils.analytics.ScreenView
-import com.instructure.pandautils.databinding.FragmentPushPreferencesBinding
+import com.instructure.pandautils.databinding.FragmentNotificationPreferencesBinding
import com.instructure.pandautils.utils.ViewStyler
import com.instructure.pandautils.utils.setupAsBackButton
import dagger.hilt.android.AndroidEntryPoint
-import kotlinx.android.synthetic.main.fragment_push_preferences.*
+import kotlinx.android.synthetic.main.fragment_notification_preferences.*
@ScreenView(SCREEN_VIEW_NOTIFICATION_PREFERENCES)
@PageView(url = "profile/communication")
@AndroidEntryPoint
-class NotificationPreferencesFragment : Fragment() {
+class PushNotificationPreferencesFragment : Fragment() {
- private val viewModel: NotificationPreferencesViewModel by viewModels()
+ private val viewModel: PushNotificationPreferencesViewModel by viewModels()
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View {
- val binding = FragmentPushPreferencesBinding.inflate(inflater, container, false)
+ val binding = FragmentNotificationPreferencesBinding.inflate(inflater, container, false)
binding.lifecycleOwner = this.viewLifecycleOwner
binding.viewModel = viewModel
+ binding.title = resources.getString(R.string.pushNotifications)
return binding.root
}
@@ -70,6 +72,6 @@ class NotificationPreferencesFragment : Fragment() {
}
companion object {
- fun newInstance() = NotificationPreferencesFragment()
+ fun newInstance() = PushNotificationPreferencesFragment()
}
}
\ No newline at end of file
diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/notification/preferences/PushNotificationPreferencesViewModel.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/notification/preferences/PushNotificationPreferencesViewModel.kt
new file mode 100644
index 0000000000..ab9603eedc
--- /dev/null
+++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/notification/preferences/PushNotificationPreferencesViewModel.kt
@@ -0,0 +1,84 @@
+/*
+ * Copyright (C) 2022 - present Instructure, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+package com.instructure.pandautils.features.notification.preferences
+
+import android.content.res.Resources
+import androidx.lifecycle.viewModelScope
+import com.instructure.canvasapi2.managers.CommunicationChannelsManager
+import com.instructure.canvasapi2.managers.NotificationPreferencesFrequency
+import com.instructure.canvasapi2.managers.NotificationPreferencesFrequency.*
+import com.instructure.canvasapi2.managers.NotificationPreferencesManager
+import com.instructure.canvasapi2.utils.ApiPrefs
+import com.instructure.pandautils.BR
+import com.instructure.pandautils.R
+import com.instructure.pandautils.features.notification.preferences.itemviewmodels.NotificationCategoryItemViewModel
+import com.instructure.pandautils.features.notification.preferences.itemviewmodels.PushNotificationCategoryItemViewModel
+import com.instructure.pandautils.mvvm.Event
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.launch
+import javax.inject.Inject
+
+@HiltViewModel
+class PushNotificationPreferencesViewModel @Inject constructor(
+ communicationChannelsManager: CommunicationChannelsManager,
+ notificationPreferencesManager: NotificationPreferencesManager,
+ apiPrefs: ApiPrefs,
+ notificationPreferenceUtils: NotificationPreferenceUtils,
+ resources: Resources
+) : NotificationPreferencesViewModel(communicationChannelsManager, notificationPreferencesManager, apiPrefs, notificationPreferenceUtils, resources) {
+
+ override val notificationChannelType: String
+ get() = "push"
+
+ override fun createCategoryItemViewModel(viewData: NotificationCategoryViewData): NotificationCategoryItemViewModel {
+ return PushNotificationCategoryItemViewModel(viewData, ::toggleNotification)
+ }
+
+ private fun toggleNotification(enabled: Boolean, categoryName: String) {
+ viewModelScope.launch {
+ try {
+ communicationChannel?.let {
+ notificationPreferencesManager.updatePreferenceCategoryAsync(
+ categoryName,
+ it.id,
+ enabled.frequency.apiString,
+ ).await().dataOrThrow
+ } ?: throw IllegalStateException()
+ } catch (e: Exception) {
+ e.printStackTrace()
+ _data.value?.items?.forEach {
+ val itemViewModel = it.itemViewModels.firstOrNull { it.data.categoryName == categoryName }
+ itemViewModel?.let {
+ it.apply {
+ data.frequency = enabled.not().frequency
+ notifyPropertyChanged(BR.checked)
+ }
+ return@forEach
+ }
+ }
+ _events.postValue(
+ Event(NotificationPreferencesAction.ShowSnackbar(resources.getString(
+ R.string.errorOccurred)))
+ )
+ }
+ }
+ }
+
+ private val Boolean.frequency: NotificationPreferencesFrequency
+ get() = if (this) IMMEDIATELY else NEVER
+
+}
\ No newline at end of file
diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/notification/preferences/itemviewmodels/EmailNotificationCategoryItemViewModel.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/notification/preferences/itemviewmodels/EmailNotificationCategoryItemViewModel.kt
new file mode 100644
index 0000000000..dc8b3df22b
--- /dev/null
+++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/notification/preferences/itemviewmodels/EmailNotificationCategoryItemViewModel.kt
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2022 - present Instructure, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+package com.instructure.pandautils.features.notification.preferences.itemviewmodels
+
+import android.content.res.Resources
+import androidx.databinding.Bindable
+import com.instructure.canvasapi2.managers.NotificationPreferencesFrequency
+import com.instructure.pandautils.R
+import com.instructure.pandautils.features.notification.preferences.NotificationCategoryViewData
+import com.instructure.pandautils.features.notification.preferences.NotificationPreferencesViewType
+
+class EmailNotificationCategoryItemViewModel(
+ data: NotificationCategoryViewData,
+ val resources: Resources,
+ val onClick: (String, NotificationPreferencesFrequency) -> Unit
+) : NotificationCategoryItemViewModel(data) {
+ override val layoutId: Int = R.layout.item_email_notification_preference
+
+ override val viewType: Int = NotificationPreferencesViewType.EMAIL_CATEGORY.viewType
+
+ @get:Bindable
+ val frequency: String
+ get() = resources.getString(data.frequency.stringRes)
+
+ fun onClick() {
+ onClick(data.categoryName, data.frequency)
+ }
+}
\ No newline at end of file
diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/notification/preferences/itemviewmodels/NotificationCategoryHeaderItemViewModel.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/notification/preferences/itemviewmodels/NotificationCategoryHeaderItemViewModel.kt
index 6c30c578a2..5cad5a1598 100644
--- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/notification/preferences/itemviewmodels/NotificationCategoryHeaderItemViewModel.kt
+++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/notification/preferences/itemviewmodels/NotificationCategoryHeaderItemViewModel.kt
@@ -20,7 +20,6 @@ import com.instructure.pandautils.R
import com.instructure.pandautils.binding.GroupItemViewModel
import com.instructure.pandautils.features.notification.preferences.NotificationCategoryHeaderViewData
import com.instructure.pandautils.features.notification.preferences.NotificationPreferencesViewType
-import com.instructure.pandautils.mvvm.ItemViewModel
class NotificationCategoryHeaderItemViewModel(
val data: NotificationCategoryHeaderViewData,
diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/notification/preferences/itemviewmodels/NotificationCategoryItemViewModel.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/notification/preferences/itemviewmodels/NotificationCategoryItemViewModel.kt
index c4a80cbdd6..4d3ce20d79 100644
--- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/notification/preferences/itemviewmodels/NotificationCategoryItemViewModel.kt
+++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/notification/preferences/itemviewmodels/NotificationCategoryItemViewModel.kt
@@ -1,42 +1,23 @@
/*
* Copyright (C) 2022 - present Instructure, Inc.
*
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, version 3 of the License.
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3 of the License.
*
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
*
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
*/
-
package com.instructure.pandautils.features.notification.preferences.itemviewmodels
import androidx.databinding.BaseObservable
-import androidx.databinding.Bindable
-import com.instructure.canvasapi2.managers.NotificationPreferencesManager
-import com.instructure.pandautils.R
import com.instructure.pandautils.features.notification.preferences.NotificationCategoryViewData
-import com.instructure.pandautils.features.notification.preferences.NotificationPreferencesViewType
import com.instructure.pandautils.mvvm.ItemViewModel
-class NotificationCategoryItemViewModel(
- val data: NotificationCategoryViewData,
- val toggle: (Boolean, String) -> Unit
-) : ItemViewModel, BaseObservable() {
- override val layoutId: Int = R.layout.item_notification_preference
-
- override val viewType: Int = NotificationPreferencesViewType.CATEGORY.viewType
-
- @get:Bindable val isChecked: Boolean
- get() = !data.frequency.equals(NotificationPreferencesManager.NEVER, ignoreCase = true)
-
- fun onCheckedChanged(checked: Boolean) {
- data.frequency = if (checked) NotificationPreferencesManager.IMMEDIATELY else NotificationPreferencesManager.NEVER
- toggle(checked, data.categoryName)
- }
-}
\ No newline at end of file
+abstract class NotificationCategoryItemViewModel(val data: NotificationCategoryViewData) : ItemViewModel, BaseObservable()
\ No newline at end of file
diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/notification/preferences/itemviewmodels/PushNotificationCategoryItemViewModel.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/notification/preferences/itemviewmodels/PushNotificationCategoryItemViewModel.kt
new file mode 100644
index 0000000000..66c01e0317
--- /dev/null
+++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/notification/preferences/itemviewmodels/PushNotificationCategoryItemViewModel.kt
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2022 - present Instructure, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.instructure.pandautils.features.notification.preferences.itemviewmodels
+
+import androidx.databinding.Bindable
+import com.instructure.canvasapi2.managers.NotificationPreferencesFrequency.IMMEDIATELY
+import com.instructure.canvasapi2.managers.NotificationPreferencesFrequency.NEVER
+import com.instructure.pandautils.R
+import com.instructure.pandautils.features.notification.preferences.NotificationCategoryViewData
+import com.instructure.pandautils.features.notification.preferences.NotificationPreferencesViewType
+
+class PushNotificationCategoryItemViewModel(
+ data: NotificationCategoryViewData,
+ val toggle: (Boolean, String) -> Unit
+) : NotificationCategoryItemViewModel(data) {
+ override val layoutId: Int = R.layout.item_push_notification_preference
+
+ override val viewType: Int = NotificationPreferencesViewType.PUSH_CATEGORY.viewType
+
+ @get:Bindable
+ val isChecked: Boolean
+ get() = data.frequency != NEVER
+
+ fun onCheckedChanged(checked: Boolean) {
+ data.frequency = if (checked) IMMEDIATELY else NEVER
+ toggle(checked, data.categoryName)
+ }
+}
\ No newline at end of file
diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/shareextension/ShareExtensionActivity.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/shareextension/ShareExtensionActivity.kt
new file mode 100644
index 0000000000..3f73bc87ae
--- /dev/null
+++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/shareextension/ShareExtensionActivity.kt
@@ -0,0 +1,241 @@
+/*
+ * Copyright (C) 2022 - present Instructure, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.instructure.pandautils.features.shareextension
+
+import android.animation.Animator
+import android.animation.AnimatorListenerAdapter
+import android.animation.ArgbEvaluator
+import android.animation.ValueAnimator
+import android.os.Bundle
+import android.os.Parcelable
+import android.view.ViewGroup
+import android.view.ViewTreeObserver
+import android.widget.ImageView
+import androidx.activity.viewModels
+import androidx.appcompat.app.AppCompatActivity
+import androidx.core.content.ContextCompat
+import androidx.fragment.app.DialogFragment
+import androidx.lifecycle.LiveData
+import androidx.work.WorkInfo
+import com.airbnb.lottie.LottieAnimationView
+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.pandautils.R
+import com.instructure.pandautils.features.file.upload.FileUploadDialogFragment
+import com.instructure.pandautils.features.file.upload.FileUploadDialogParent
+import com.instructure.pandautils.features.file.upload.FileUploadType
+import com.instructure.pandautils.features.shareextension.progress.ShareExtensionProgressDialogFragment
+import com.instructure.pandautils.features.shareextension.status.ShareExtensionStatus
+import com.instructure.pandautils.features.shareextension.status.ShareExtensionStatusDialogFragment
+import com.instructure.pandautils.features.shareextension.target.ShareExtensionTargetFragment
+import com.instructure.pandautils.utils.*
+import dagger.hilt.android.AndroidEntryPoint
+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.*
+
+const val WORKER_ID = "workerId"
+
+@Parcelize
+data class ShareFileSubmissionTarget(
+ val course: Course,
+ val assignment: Assignment
+) : Parcelable
+
+@AndroidEntryPoint
+abstract class ShareExtensionActivity : AppCompatActivity(), FileUploadDialogParent {
+
+ private val shareExtensionViewModel: ShareExtensionViewModel by viewModels()
+
+ private var loadCoursesJob: Job? = null
+ private var currentFragment: DialogFragment? = null
+
+ private val submissionTarget: ShareFileSubmissionTarget? by lazy {
+ intent?.extras?.getParcelable(Const.SUBMISSION_TARGET)
+ }
+
+ private val workerId: UUID? by lazy {
+ intent?.extras?.getSerializable(WORKER_ID) as? UUID
+ }
+
+ 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 (shareExtensionViewModel.checkIfLoggedIn()) {
+ revealBackground()
+ if (workerId != null) {
+ showProgressDialog(workerId!!)
+ } else {
+ shareExtensionViewModel.parseIntentType(intent)
+ if (submissionTarget != null) {
+ shareExtensionViewModel.showUploadDialog(
+ submissionTarget!!.course,
+ submissionTarget!!.assignment,
+ FileUploadType.ASSIGNMENT
+ )
+ } else {
+ showDestinationDialog()
+ }
+ }
+ } else {
+ exitActivity()
+ }
+
+ shareExtensionViewModel.events.observe(this) {
+ it.getContentIfNotHandled()?.let {
+ handleAction(it)
+ }
+ }
+ }
+
+ private fun handleAction(action: ShareExtensionAction) {
+ when (action) {
+ is ShareExtensionAction.ShowAssignmentUploadDialog -> {
+ val bundle = FileUploadDialogFragment.createAssignmentBundle(action.fileUris, action.course as Course, action.assignment)
+ showUploadDialog(bundle, action.dialogCallback)
+ }
+ is ShareExtensionAction.ShowMyFilesUploadDialog -> {
+ val bundle = FileUploadDialogFragment.createFilesBundle(action.fileUris, null)
+ showUploadDialog(bundle, action.dialogCallback)
+ }
+ is ShareExtensionAction.ShowToast -> {
+ toast(action.toast)
+ }
+ is ShareExtensionAction.Finish -> {
+ finish()
+ }
+ is ShareExtensionAction.ShowConfetti -> {
+ showConfetti()
+ }
+ is ShareExtensionAction.ShowSuccessDialog -> {
+ rootView?.postDelayed({
+ currentFragment = ShareExtensionStatusDialogFragment.newInstance(ShareExtensionStatus.SUCCEEDED, action.fileUploadType)
+ currentFragment?.show(supportFragmentManager, ShareExtensionStatusDialogFragment.TAG)
+ }, 250)
+ }
+ is ShareExtensionAction.ShowProgressDialog -> {
+ showProgressDialog(action.uuid)
+ }
+ is ShareExtensionAction.ShowErrorDialog -> {
+ currentFragment = ShareExtensionStatusDialogFragment.newInstance(ShareExtensionStatus.FAILED, action.fileUploadType)
+ currentFragment?.show(supportFragmentManager, ShareExtensionStatusDialogFragment.TAG)
+ }
+ }
+ }
+
+ private fun showProgressDialog(uuid: UUID) {
+ currentFragment = ShareExtensionProgressDialogFragment.newInstance(uuid)
+ currentFragment?.show(supportFragmentManager, ShareExtensionProgressDialogFragment.TAG)
+ }
+
+ private fun showUploadDialog(bundle: Bundle, dialogCallback: (Int) -> Unit) {
+ ValueAnimator.ofObject(ArgbEvaluator(), ContextCompat.getColor(this, R.color.studentDocumentSharingColor), getColor(bundle)).let {
+ it.addUpdateListener { animation -> rootView!!.setBackgroundColor(animation.animatedValue as Int) }
+ it.duration = 500
+ it.addListener(object : AnimatorListenerAdapter() {
+ override fun onAnimationStart(animation: Animator) {
+ currentFragment = FileUploadDialogFragment.newInstance(bundle, callback = dialogCallback)
+ currentFragment?.show(supportFragmentManager, FileUploadDialogFragment.TAG)
+ }
+ })
+ it.start()
+ }
+ }
+
+ override fun workInfoLiveDataCallback(uuid: UUID?, workInfoLiveData: LiveData) {
+ uuid?.let {
+ shareExtensionViewModel.workerCallback(it)
+ }
+ }
+
+ private fun revealBackground() {
+ rootView.viewTreeObserver.addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener {
+ override fun onGlobalLayout() {
+ AnimationHelpers.removeGlobalLayoutListeners(rootView, this)
+ AnimationHelpers.createRevealAnimator(rootView).start()
+ }
+ })
+ }
+
+ abstract fun exitActivity()
+
+ override fun onBackPressed() {
+ currentFragment?.dismissAllowingStateLoss()
+ finish()
+ }
+
+ override fun onDestroy() {
+ currentFragment?.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() {
+ currentFragment = ShareExtensionTargetFragment()
+ currentFragment?.show(supportFragmentManager, ShareExtensionTargetFragment.TAG)
+ }
+
+ private fun showConfetti() {
+ runOnUiThread {
+ val root = window.decorView.rootView as ViewGroup
+ val animation = LottieAnimationView(this).apply {
+ setAnimation("confetti.json")
+ scaleType = ImageView.ScaleType.CENTER_CROP;
+ }
+ animation.addAnimatorUpdateListener {
+ if (it.animatedFraction >= 1.0) root.removeView(animation)
+ }
+ root.addView(animation, ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)
+ animation.playAnimation()
+ }
+ }
+
+ @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.studentDocumentSharingColor)
+ ViewStyler.setStatusBarDark(this, color)
+ color
+ }
+ }
+}
diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/shareextension/ShareExtensionRouter.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/shareextension/ShareExtensionRouter.kt
new file mode 100644
index 0000000000..edd10fd52e
--- /dev/null
+++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/shareextension/ShareExtensionRouter.kt
@@ -0,0 +1,10 @@
+package com.instructure.pandautils.features.shareextension
+
+import android.content.Context
+import android.content.Intent
+import java.util.*
+
+interface ShareExtensionRouter {
+
+ fun routeToProgressScreen(context: Context, workerId: UUID): Intent
+}
\ No newline at end of file
diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/shareextension/ShareExtensionViewModel.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/shareextension/ShareExtensionViewModel.kt
new file mode 100644
index 0000000000..0f4b89a045
--- /dev/null
+++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/shareextension/ShareExtensionViewModel.kt
@@ -0,0 +1,139 @@
+/*
+ * Copyright (C) 2022 - present Instructure, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.instructure.pandautils.features.shareextension
+
+import android.content.Intent
+import android.content.res.Resources
+import android.net.Uri
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.ViewModel
+import androidx.work.WorkInfo
+import com.instructure.canvasapi2.models.Assignment
+import com.instructure.canvasapi2.models.CanvasContext
+import com.instructure.canvasapi2.utils.ApiPrefs
+import com.instructure.pandautils.R
+import com.instructure.pandautils.features.file.upload.FileUploadDialogFragment
+import com.instructure.pandautils.features.file.upload.FileUploadType
+import com.instructure.pandautils.mvvm.Event
+import dagger.hilt.android.lifecycle.HiltViewModel
+import java.util.*
+import javax.inject.Inject
+
+@HiltViewModel
+class ShareExtensionViewModel @Inject constructor(
+ private val apiPrefs: ApiPrefs,
+ private val resources: Resources
+) : ViewModel() {
+
+ var uris: ArrayList? = null
+ var uploadType = FileUploadType.USER
+
+ val events: LiveData>
+ get() = _events
+ private val _events = MutableLiveData>()
+
+ fun checkIfLoggedIn(): Boolean {
+ return apiPrefs.getValidToken().isNotEmpty()
+ }
+
+ fun parseIntentType(intent: Intent) {
+ val action = intent.action
+ val type = intent.type
+
+ if (type == null) {
+ _events.postValue(Event(ShareExtensionAction.ShowToast(resources.getString(R.string.uploadingFromSourceFailed))))
+ return
+ }
+ when (action) {
+ Intent.ACTION_SEND -> {
+ val uri = intent.getParcelableExtra(Intent.EXTRA_STREAM)
+ uri?.let {
+ uris = arrayListOf(it)
+ } ?: _events.postValue(Event(ShareExtensionAction.ShowToast(resources.getString(R.string.errorOccurred))))
+ }
+ Intent.ACTION_SEND_MULTIPLE -> {
+ uris = intent.getParcelableArrayListExtra(Intent.EXTRA_STREAM)
+ }
+ }
+ }
+
+ fun showUploadDialog(course: CanvasContext?, assignment: Assignment?, uploadType: FileUploadType) {
+ this.uploadType = uploadType
+ uris?.let {
+ when (uploadType) {
+ FileUploadType.USER -> _events.postValue(Event(ShareExtensionAction.ShowMyFilesUploadDialog(it, this::uploadDialogCallback)))
+ FileUploadType.ASSIGNMENT -> {
+ when {
+ course == null -> {
+ _events.postValue(Event(ShareExtensionAction.ShowToast(resources.getString(R.string.noCourseSelected))))
+ }
+ assignment == null -> {
+ _events.postValue(Event(ShareExtensionAction.ShowToast(resources.getString(R.string.noAssignmentSelected))))
+ }
+ else -> {
+ _events.postValue(Event(ShareExtensionAction.ShowAssignmentUploadDialog(course, assignment, it, uploadType, this::uploadDialogCallback)))
+ }
+ }
+ }
+ else -> _events.postValue(Event(ShareExtensionAction.ShowToast(resources.getString(R.string.notSupported))))
+ }
+ } ?: _events.postValue(Event(ShareExtensionAction.ShowToast(resources.getString(R.string.errorOccurred))))
+ }
+
+ fun finish() {
+ _events.postValue(Event(ShareExtensionAction.Finish))
+ }
+
+ fun showSuccessDialog(fileUploadType: FileUploadType) {
+ _events.postValue(Event(ShareExtensionAction.ShowSuccessDialog(fileUploadType)))
+ }
+
+ fun showErrorDialog(fileUploadType: FileUploadType) {
+ _events.postValue(Event(ShareExtensionAction.ShowErrorDialog(fileUploadType)))
+ }
+
+ fun showConfetti() {
+ _events.postValue(Event(ShareExtensionAction.ShowConfetti))
+ }
+
+ private fun uploadDialogCallback(event: Int) {
+ when (event) {
+ FileUploadDialogFragment.EVENT_DIALOG_CANCELED -> finish()
+ }
+ }
+
+ private fun showProgressDialog(uuid: UUID) {
+ _events.postValue(Event(ShareExtensionAction.ShowProgressDialog(uuid)))
+ }
+
+ fun workerCallback(uuid: UUID) {
+ showProgressDialog(uuid)
+ }
+
+}
+
+sealed class ShareExtensionAction {
+ data class ShowAssignmentUploadDialog(val course: CanvasContext, val assignment: Assignment, val fileUris: ArrayList, val uploadType: FileUploadType, val dialogCallback: (Int) -> Unit) : ShareExtensionAction()
+ data class ShowMyFilesUploadDialog(val fileUris: ArrayList, val dialogCallback: (Int) -> Unit) : ShareExtensionAction()
+ data class ShowProgressDialog(val uuid: UUID) : ShareExtensionAction()
+ data class ShowSuccessDialog(val fileUploadType: FileUploadType) : ShareExtensionAction()
+ data class ShowErrorDialog(val fileUploadType: FileUploadType) : ShareExtensionAction()
+ object Finish : ShareExtensionAction()
+ object ShowConfetti : ShareExtensionAction()
+ data class ShowToast(val toast: String) : ShareExtensionAction()
+}
\ No newline at end of file
diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/shareextension/progress/ShareExtensionProgressDialogFragment.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/shareextension/progress/ShareExtensionProgressDialogFragment.kt
new file mode 100644
index 0000000000..f6e2933dff
--- /dev/null
+++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/shareextension/progress/ShareExtensionProgressDialogFragment.kt
@@ -0,0 +1,148 @@
+/*
+ * Copyright (C) 2022 - present Instructure, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.instructure.pandautils.features.shareextension.progress
+
+import android.app.Dialog
+import android.content.DialogInterface
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.view.WindowManager
+import androidx.appcompat.app.AlertDialog
+import androidx.fragment.app.DialogFragment
+import androidx.fragment.app.activityViewModels
+import androidx.fragment.app.viewModels
+import com.instructure.pandautils.R
+import com.instructure.pandautils.databinding.FragmentShareExtensionProgressDialogBinding
+import com.instructure.pandautils.features.shareextension.ShareExtensionViewModel
+import com.instructure.pandautils.utils.NullableSerializableArg
+import com.instructure.pandautils.utils.ThemePrefs
+import dagger.hilt.android.AndroidEntryPoint
+import java.util.*
+
+@AndroidEntryPoint
+class ShareExtensionProgressDialogFragment : DialogFragment() {
+
+ private val viewModel: ShareExtensionProgressDialogViewModel by viewModels()
+
+ private val shareExtensionViewModel: ShareExtensionViewModel by activityViewModels()
+
+ private var uuid: UUID? by NullableSerializableArg(KEY_UUID)
+
+ private lateinit var binding: FragmentShareExtensionProgressDialogBinding
+
+ override fun onCreateView(
+ inflater: LayoutInflater, container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View {
+ binding.lifecycleOwner = this
+ binding.viewModel = viewModel
+
+ return binding.root
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+
+ uuid?.let {
+ viewModel.setUUID(it)
+ }
+
+ viewModel.events.observe(viewLifecycleOwner) {
+ it.getContentIfNotHandled()?.let {
+ handleAction(it)
+ }
+ }
+ }
+
+ private fun handleAction(action: ShareExtensionProgressAction) {
+ when (action) {
+ is ShareExtensionProgressAction.ShowSuccessDialog -> {
+ dismiss()
+ shareExtensionViewModel.showSuccessDialog(action.fileUploadType)
+ }
+ is ShareExtensionProgressAction.Close -> {
+ dismiss()
+ shareExtensionViewModel.finish()
+ }
+ is ShareExtensionProgressAction.CancelUpload -> {
+ cancelClicked(action.title, action.message)
+ }
+ is ShareExtensionProgressAction.ShowErrorDialog -> {
+ dismiss()
+ shareExtensionViewModel.showErrorDialog(action.fileUploadType)
+ }
+ }
+ }
+
+ override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
+ binding = FragmentShareExtensionProgressDialogBinding.inflate(layoutInflater, null, false)
+
+ val dialog = AlertDialog.Builder(requireContext())
+ .setView(binding.root)
+ .setNegativeButton(R.string.utils_cancel, null)
+ .setCancelable(false)
+ .create()
+
+ dialog.setCanceledOnTouchOutside(false)
+
+ dialog.setOnShowListener {
+ val negative = dialog.getButton(DialogInterface.BUTTON_NEGATIVE)
+ negative.setTextColor(ThemePrefs.buttonColor)
+ negative.setOnClickListener {
+ viewModel.cancelClicked()
+ }
+ }
+
+ dialog.window?.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND)
+
+ return dialog
+ }
+
+ override fun onCancel(dialog: DialogInterface) {
+ super.onCancel(dialog)
+ requireActivity().onBackPressed()
+ }
+
+ private fun cancelClicked(title: String, message: String) {
+ AlertDialog.Builder(requireContext())
+ .setTitle(title)
+ .setMessage(message)
+ .setNegativeButton(R.string.no) { _, _ -> }
+ .setPositiveButton(R.string.yes) { _, _ ->
+ uuid?.let {
+ viewModel.cancelUpload(it)
+ }
+ viewModel.onCloseClicked()
+ }.show()
+ }
+
+ companion object {
+ const val TAG = "ShareExtensionProgressDialogFragment"
+ const val KEY_UUID = "UUID"
+ fun newInstance(uuid: UUID): ShareExtensionProgressDialogFragment {
+ return ShareExtensionProgressDialogFragment().apply {
+ arguments = Bundle().apply {
+ putSerializable(KEY_UUID, uuid)
+ }
+ this.uuid = uuid
+ }
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/shareextension/progress/ShareExtensionProgressDialogViewModel.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/shareextension/progress/ShareExtensionProgressDialogViewModel.kt
new file mode 100644
index 0000000000..5631f458b9
--- /dev/null
+++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/shareextension/progress/ShareExtensionProgressDialogViewModel.kt
@@ -0,0 +1,178 @@
+package com.instructure.pandautils.features.shareextension.progress
+
+import android.content.res.Resources
+import androidx.annotation.DrawableRes
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.Observer
+import androidx.lifecycle.ViewModel
+import androidx.work.Data
+import androidx.work.WorkInfo
+import androidx.work.WorkManager
+import androidx.work.hasKeyWithValueOfType
+import com.instructure.canvasapi2.models.postmodels.FileSubmitObject
+import com.instructure.pandautils.BR
+import com.instructure.pandautils.R
+import com.instructure.pandautils.features.file.upload.FileUploadType
+import com.instructure.pandautils.features.file.upload.worker.FileUploadWorker
+import com.instructure.pandautils.features.shareextension.progress.itemviewmodels.FileProgressItemViewModel
+import com.instructure.pandautils.mvvm.Event
+import com.instructure.pandautils.mvvm.ViewState
+import com.instructure.pandautils.utils.fromJson
+import com.instructure.pandautils.utils.humanReadableByteCount
+import dagger.hilt.android.lifecycle.HiltViewModel
+import java.util.*
+import javax.inject.Inject
+
+@HiltViewModel
+class ShareExtensionProgressDialogViewModel @Inject constructor(
+ private val workManager: WorkManager,
+ private val resources: Resources
+) : ViewModel() {
+
+ val state: LiveData
+ get() = _state
+ private val _state = MutableLiveData()
+
+ val data: LiveData
+ get() = _data
+ private val _data = MutableLiveData()
+
+ val events: LiveData>
+ get() = _events
+ private val _events = MutableLiveData>()
+
+ private var viewData: ShareExtensionProgressViewData? = null
+
+ private var itemViewData: List = emptyList()
+
+ private var workerId: UUID? = null
+ private var fileUploadType = FileUploadType.USER
+
+ private val observer = Observer {
+ when (it.state) {
+ WorkInfo.State.SUCCEEDED -> {
+ _events.postValue(Event((ShareExtensionProgressAction.ShowSuccessDialog(fileUploadType))))
+ }
+ WorkInfo.State.RUNNING -> {
+ updateViewData(it.progress)
+ }
+ WorkInfo.State.FAILED -> {
+ _events.postValue(Event(ShareExtensionProgressAction.ShowErrorDialog(fileUploadType)))
+ }
+ }
+ }
+
+ fun setUUID(uuid: UUID) {
+ this.workerId = uuid
+ _state.postValue(ViewState.Loading)
+ workManager.getWorkInfoByIdLiveData(uuid).observeForever(observer)
+ }
+
+ override fun onCleared() {
+ super.onCleared()
+ workerId?.let {
+ workManager.getWorkInfoByIdLiveData(it).removeObserver(observer)
+ }
+ }
+
+ private fun updateViewData(progress: Data) {
+ if (allDataPresent(progress)) {
+ _state.postValue(ViewState.Success)
+
+ val maxSize = progress.getLong(FileUploadWorker.PROGRESS_DATA_FULL_SIZE, 1L)
+ val currentSize = progress.getLong(FileUploadWorker.PROGRESS_DATA_UPLOADED_SIZE, 0L)
+ val assignmentName =
+ if (progress.hasKeyWithValueOfType(FileUploadWorker.PROGRESS_DATA_ASSIGNMENT_NAME)) {
+ progress.getString(FileUploadWorker.PROGRESS_DATA_ASSIGNMENT_NAME)
+ } else null
+ fileUploadType = if (assignmentName.isNullOrEmpty()) FileUploadType.USER else FileUploadType.ASSIGNMENT
+
+ val uploadedMap = progress.getStringArray(FileUploadWorker.PROGRESS_DATA_UPLOADED_FILES).orEmpty()
+ .map { it.fromJson() }
+ .associateBy { it.name }
+
+ if (viewData == null) {
+ itemViewData =
+ progress.getStringArray(FileUploadWorker.PROGRESS_DATA_FILES_TO_UPLOAD).orEmpty().toList()
+ .map { it.fromJson() }
+ .map {
+ FileProgressViewData(
+ it.name,
+ it.size.humanReadableByteCount(),
+ getIconDrawableRes(it.contentType),
+ uploadedMap.containsKey(it.name)
+ )
+ }
+
+ viewData = ShareExtensionProgressViewData(
+ items = itemViewData.map { FileProgressItemViewModel(it) },
+ dialogTitle = if (assignmentName.isNullOrEmpty()) resources.getString(R.string.fileUpload) else resources.getString(
+ R.string.submission
+ ),
+ subtitle = if (assignmentName.isNullOrEmpty()) resources.getString(R.string.fileUploadProgressSubtitle) else resources.getString(
+ R.string.submissionProgressSubtitle,
+ assignmentName
+ ),
+ maxSize = maxSize.humanReadableByteCount(),
+ currentSize = currentSize.humanReadableByteCount(),
+ progressInt = ((currentSize.toDouble() / maxSize.toDouble()) * 100.0).toInt(),
+ percentage = "${String.format("%.1f", currentSize.toDouble() / maxSize.toDouble() * 100.0)}%"
+ )
+ viewData?.let {
+ _data.postValue(it)
+ }
+ } else {
+ viewData?.apply {
+ this.currentSize = currentSize.humanReadableByteCount()
+ this.progressInt = ((currentSize.toDouble() / maxSize.toDouble()) * 100).toInt()
+ this.percentage =
+ "${String.format("%.1f", currentSize.toDouble() / maxSize.toDouble() * 100.0)}%"
+ uploadedMap.forEach { uploadedEntry ->
+ this.items.find { itemViewModel ->
+ itemViewModel.data.name == uploadedEntry.key
+ }.apply {
+ this?.data?.uploaded = true
+ this?.data?.notifyPropertyChanged(BR.uploaded)
+ }
+ }
+ notifyPropertyChanged(BR.currentSize)
+ notifyPropertyChanged(BR.progressInt)
+ notifyPropertyChanged(BR.percentage)
+ }
+ }
+
+ }
+ }
+
+ private fun allDataPresent(progress: Data): Boolean {
+ return progress.hasKeyWithValueOfType(FileUploadWorker.PROGRESS_DATA_FULL_SIZE) && progress.hasKeyWithValueOfType>(
+ FileUploadWorker.PROGRESS_DATA_FILES_TO_UPLOAD
+ )
+ }
+
+ fun onCloseClicked() {
+ _events.postValue(Event(ShareExtensionProgressAction.Close))
+ }
+
+ @DrawableRes
+ private fun getIconDrawableRes(contentType: String): Int {
+ return when {
+ contentType.contains("image") -> R.drawable.ic_image
+ contentType.contains("video") -> R.drawable.ic_media
+ contentType.contains("pdf") -> R.drawable.ic_pdf
+ else -> R.drawable.ic_attachment
+ }
+ }
+
+ fun cancelUpload(workerId: UUID) {
+ workManager.cancelWorkById(workerId)
+ }
+
+ fun cancelClicked() {
+ _events.postValue(Event(ShareExtensionProgressAction.CancelUpload(
+ title = if (fileUploadType == FileUploadType.ASSIGNMENT) resources.getString(R.string.cancelSubmissionDialogTitle) else resources.getString(R.string.cancelFileUploadTitle),
+ message = if (fileUploadType == FileUploadType.ASSIGNMENT) resources.getString(R.string.cancelSubmissionDialogMessage) else resources.getString(R.string.cancelFileUploadMessage)
+ )))
+ }
+}
\ No newline at end of file
diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/shareextension/progress/ShareExtensionProgressViewData.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/shareextension/progress/ShareExtensionProgressViewData.kt
new file mode 100644
index 0000000000..735cdd3f4c
--- /dev/null
+++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/shareextension/progress/ShareExtensionProgressViewData.kt
@@ -0,0 +1,31 @@
+package com.instructure.pandautils.features.shareextension.progress
+
+import androidx.annotation.DrawableRes
+import androidx.databinding.BaseObservable
+import androidx.databinding.Bindable
+import com.instructure.pandautils.features.file.upload.FileUploadType
+import com.instructure.pandautils.features.shareextension.progress.itemviewmodels.FileProgressItemViewModel
+
+data class ShareExtensionProgressViewData(
+ val items: List,
+ val dialogTitle: String,
+ val subtitle: String?,
+ val maxSize: String,
+ @get:Bindable var progressInt: Int,
+ @get:Bindable var percentage: String,
+ @get:Bindable var currentSize: String
+) : BaseObservable()
+
+data class FileProgressViewData(
+ val name: String,
+ val size: String,
+ @DrawableRes val icon: Int,
+ @get:Bindable var uploaded: Boolean
+) : BaseObservable()
+
+sealed class ShareExtensionProgressAction {
+ object Close : ShareExtensionProgressAction()
+ data class CancelUpload(val title: String, val message: String) : ShareExtensionProgressAction()
+ data class ShowSuccessDialog(val fileUploadType: FileUploadType) : ShareExtensionProgressAction()
+ data class ShowErrorDialog(val fileUploadType: FileUploadType) : ShareExtensionProgressAction()
+}
\ No newline at end of file
diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/shareextension/progress/itemviewmodels/FileProgressItemViewModel.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/shareextension/progress/itemviewmodels/FileProgressItemViewModel.kt
new file mode 100644
index 0000000000..688c39da59
--- /dev/null
+++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/shareextension/progress/itemviewmodels/FileProgressItemViewModel.kt
@@ -0,0 +1,11 @@
+package com.instructure.pandautils.features.shareextension.progress.itemviewmodels
+
+import com.instructure.pandautils.R
+import com.instructure.pandautils.features.shareextension.progress.FileProgressViewData
+import com.instructure.pandautils.mvvm.ItemViewModel
+
+class FileProgressItemViewModel(
+ val data: FileProgressViewData
+) : ItemViewModel {
+ override val layoutId: Int = R.layout.item_file_progress
+}
\ No newline at end of file
diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/shareextension/status/ShareExtensionStatusDialogFragment.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/shareextension/status/ShareExtensionStatusDialogFragment.kt
new file mode 100644
index 0000000000..45cc479313
--- /dev/null
+++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/shareextension/status/ShareExtensionStatusDialogFragment.kt
@@ -0,0 +1,123 @@
+/*
+ * 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