diff --git a/InstrumentationTests/.gitignore b/InstrumentationTests/.gitignore
deleted file mode 100644
index b5faaad07e..0000000000
--- a/InstrumentationTests/.gitignore
+++ /dev/null
@@ -1,46 +0,0 @@
-# built application files #
-build/
-*.apk
-*.ap_
-
-# files for the dex VM #
-*.dex
-
-# Java class files #
-*.class
-
-# generated files #
-bin/
-gen/
-target
-
-# Local configuration file #
-local.properties
-
-# Windows thumbnail db #
-Thumbs.db
-
-# OSX files #
-.DS_Store
-
-# Eclipse project files #
-.classpath
-.project
-
-# Android Studio and Intellij #
-.idea/
-*.iml
-*.iws
-
-# Package Files #
-*.jar
-*.war
-*.ear
-
-# Gradle Files #
-.gradle
-gradle.properties
-release_build/.gradle
-
-# Project Files #
-.gitconfig
diff --git a/InstrumentationTests/app/.gitignore b/InstrumentationTests/app/.gitignore
deleted file mode 100644
index b5faaad07e..0000000000
--- a/InstrumentationTests/app/.gitignore
+++ /dev/null
@@ -1,46 +0,0 @@
-# built application files #
-build/
-*.apk
-*.ap_
-
-# files for the dex VM #
-*.dex
-
-# Java class files #
-*.class
-
-# generated files #
-bin/
-gen/
-target
-
-# Local configuration file #
-local.properties
-
-# Windows thumbnail db #
-Thumbs.db
-
-# OSX files #
-.DS_Store
-
-# Eclipse project files #
-.classpath
-.project
-
-# Android Studio and Intellij #
-.idea/
-*.iml
-*.iws
-
-# Package Files #
-*.jar
-*.war
-*.ear
-
-# Gradle Files #
-.gradle
-gradle.properties
-release_build/.gradle
-
-# Project Files #
-.gitconfig
diff --git a/InstrumentationTests/app/build.gradle b/InstrumentationTests/app/build.gradle
deleted file mode 100644
index 7b90a045bc..0000000000
--- a/InstrumentationTests/app/build.gradle
+++ /dev/null
@@ -1,82 +0,0 @@
-apply plugin: 'com.android.application'
-/*
- * Copyright (C) 2017 - 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.
- */
-
-repositories {
- jcenter()
- maven { url "https://jitpack.io" }
-}
-
-buildscript {
- repositories {
- jcenter()
- }
- dependencies {
- classpath "com.android.tools.build:gradle:$GLOBAL_GRADLE_TOOLS_VERSION"
- }
-}
-
-android {
- compileSdkVersion rootProject.ext.compileSdkVersion
- buildToolsVersion rootProject.ext.buildToolsVersion
- defaultConfig {
- applicationId "instructure.instrumentationtests"
- minSdkVersion rootProject.ext.minSdkVersion
- targetSdkVersion rootProject.ext.targetSdkVersion
- versionCode rootProject.ext.versionCode
- versionName rootProject.ext.versionName
- testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
- }
- buildTypes {
- release {
- minifyEnabled false
- proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
- }
- }
-
- lintOptions {
- abortOnError false
- }
-
- dexOptions {
- preDexLibraries = false
- javaMaxHeapSize '2g'
- }
-
-}
-
-dependencies {
- compile fileTree(dir: 'libs', include: ['*.jar'])
- androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
- exclude group: 'com.android.support', module: 'support-annotations'
- })
- compile rootProject.ext.supportDependencies.appCompat
- compile 'com.android.support:multidex:1.0.1'
-
- testCompile "org.robolectric:robolectric:3.2.2"
- testCompile 'org.robolectric:shadows-multidex:3.0'
- testCompile "org.robolectric:shadows-play-services:3.0"
- testCompile "org.robolectric:shadows-support-v4:3.0"
-
- compile project(':canvas-api-2')
- compile project(':pandautils')
- compile project(':recyclerview')
- compile project(':blueprint')
- releaseCompile project(path: ':login-api-2', configuration: 'release')
- debugCompile project(path: ':login-api-2', configuration: 'debug')
-
- testCompile 'junit:junit:4.12'
-}
diff --git a/InstrumentationTests/app/proguard-rules.pro b/InstrumentationTests/app/proguard-rules.pro
deleted file mode 100644
index 6e40fe674e..0000000000
--- a/InstrumentationTests/app/proguard-rules.pro
+++ /dev/null
@@ -1,17 +0,0 @@
-# Add project specific ProGuard rules here.
-# By default, the flags in this file are appended to flags specified
-# in /Users/{user}/AndroidSDK/sdk/tools/proguard/proguard-android.txt
-# You can edit the include path and order by changing the proguardFiles
-# directive in build.gradle.
-#
-# For more details, see
-# http://developer.android.com/guide/developing/tools/proguard.html
-
-# Add any project specific keep options here:
-
-# If your project uses WebView with JS, uncomment the following
-# and specify the fully qualified class name to the JavaScript interface
-# class:
-#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
-# public *;
-#}
diff --git a/InstrumentationTests/app/src/androidTest/java/instructure/instrumentationtests/ExampleInstrumentedTest.java b/InstrumentationTests/app/src/androidTest/java/instructure/instrumentationtests/ExampleInstrumentedTest.java
deleted file mode 100644
index 8e57e6e3b9..0000000000
--- a/InstrumentationTests/app/src/androidTest/java/instructure/instrumentationtests/ExampleInstrumentedTest.java
+++ /dev/null
@@ -1,42 +0,0 @@
-/*
- * Copyright (C) 2017 - 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 instructure.instrumentationtests;
-
-import android.content.Context;
-import android.support.test.InstrumentationRegistry;
-import android.support.test.runner.AndroidJUnit4;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-import static org.junit.Assert.*;
-
-/**
- * Instrumentation test, which will execute on an Android device.
- *
- * @see Testing documentation
- */
-@RunWith(AndroidJUnit4.class)
-public class ExampleInstrumentedTest {
- @Test
- public void useAppContext() throws Exception {
- // Context of the app under test.
- Context appContext = InstrumentationRegistry.getTargetContext();
-
- assertEquals("instructure.instrumentationtests", appContext.getPackageName());
- }
-}
diff --git a/InstrumentationTests/app/src/main/AndroidManifest.xml b/InstrumentationTests/app/src/main/AndroidManifest.xml
deleted file mode 100644
index 2a4e5d3414..0000000000
--- a/InstrumentationTests/app/src/main/AndroidManifest.xml
+++ /dev/null
@@ -1,30 +0,0 @@
-
-
-
-
-
-
-
-
-
diff --git a/InstrumentationTests/app/src/main/java/instructure/instrumentationtests/AppManager.java b/InstrumentationTests/app/src/main/java/instructure/instrumentationtests/AppManager.java
deleted file mode 100644
index bef79b3c10..0000000000
--- a/InstrumentationTests/app/src/main/java/instructure/instrumentationtests/AppManager.java
+++ /dev/null
@@ -1,31 +0,0 @@
-/*
- * Copyright (C) 2017 - 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 instructure.instrumentationtests;
-
-import android.content.Context;
-import android.support.multidex.MultiDex;
-import android.support.multidex.MultiDexApplication;
-
-public class AppManager extends MultiDexApplication {
-
- @Override
- protected void attachBaseContext(Context base) {
- super.attachBaseContext(base);
- MultiDex.install(this);
- }
-
-}
diff --git a/InstrumentationTests/app/src/main/res/mipmap-hdpi/ic_launcher.png b/InstrumentationTests/app/src/main/res/mipmap-hdpi/ic_launcher.png
deleted file mode 100644
index cde69bccce..0000000000
Binary files a/InstrumentationTests/app/src/main/res/mipmap-hdpi/ic_launcher.png and /dev/null differ
diff --git a/InstrumentationTests/app/src/main/res/mipmap-mdpi/ic_launcher.png b/InstrumentationTests/app/src/main/res/mipmap-mdpi/ic_launcher.png
deleted file mode 100644
index c133a0cbd3..0000000000
Binary files a/InstrumentationTests/app/src/main/res/mipmap-mdpi/ic_launcher.png and /dev/null differ
diff --git a/InstrumentationTests/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/InstrumentationTests/app/src/main/res/mipmap-xhdpi/ic_launcher.png
deleted file mode 100644
index bfa42f0e7b..0000000000
Binary files a/InstrumentationTests/app/src/main/res/mipmap-xhdpi/ic_launcher.png and /dev/null differ
diff --git a/InstrumentationTests/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/InstrumentationTests/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
deleted file mode 100644
index 324e72cdd7..0000000000
Binary files a/InstrumentationTests/app/src/main/res/mipmap-xxhdpi/ic_launcher.png and /dev/null differ
diff --git a/InstrumentationTests/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/InstrumentationTests/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
deleted file mode 100644
index aee44e1384..0000000000
Binary files a/InstrumentationTests/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png and /dev/null differ
diff --git a/InstrumentationTests/app/src/main/res/values/colors.xml b/InstrumentationTests/app/src/main/res/values/colors.xml
deleted file mode 100644
index 1bd523e366..0000000000
--- a/InstrumentationTests/app/src/main/res/values/colors.xml
+++ /dev/null
@@ -1,22 +0,0 @@
-
-
-
-
- #3F51B5
- #303F9F
- #FF4081
-
diff --git a/InstrumentationTests/app/src/main/res/values/strings.xml b/InstrumentationTests/app/src/main/res/values/strings.xml
deleted file mode 100644
index 8449b18c9e..0000000000
--- a/InstrumentationTests/app/src/main/res/values/strings.xml
+++ /dev/null
@@ -1,19 +0,0 @@
-
-
-
- InstrumentationTests
-
diff --git a/InstrumentationTests/app/src/main/res/values/styles.xml b/InstrumentationTests/app/src/main/res/values/styles.xml
deleted file mode 100644
index 47ce9a26ce..0000000000
--- a/InstrumentationTests/app/src/main/res/values/styles.xml
+++ /dev/null
@@ -1,27 +0,0 @@
-
-
-
-
-
-
-
-
diff --git a/InstrumentationTests/app/src/test/java/instructure/instrumentationtests/SampleTest.java b/InstrumentationTests/app/src/test/java/instructure/instrumentationtests/SampleTest.java
deleted file mode 100644
index 7a0d21bd33..0000000000
--- a/InstrumentationTests/app/src/test/java/instructure/instrumentationtests/SampleTest.java
+++ /dev/null
@@ -1,48 +0,0 @@
-/*
- * Copyright (C) 2017 - 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 instructure.instrumentationtests;
-
-import com.instructure.canvasapi2.utils.DateHelper;
-
-import org.junit.Test;
-
-import java.util.Calendar;
-import java.util.GregorianCalendar;
-
-import instructure.instrumentationtests.robo.RoboTestCase;
-
-public class SampleTest extends RoboTestCase {
-
- @Test
- public void dateTest() {
- GregorianCalendar now = new GregorianCalendar();
- GregorianCalendar later = new GregorianCalendar();
- later.add(Calendar.DAY_OF_WEEK, 1);
-
- assertTrue(DateHelper.compareDays(now, later) == -1);
-
- assertTrue(DateHelper.compareDays(now, now) == 0);
-
- assertTrue(DateHelper.compareDays(later, now) == 1);
- }
-
- @Test
- public void resourceTest() {
- String logIn = context().getResources().getString(R.string.login);
- assertEquals(logIn, "Login");
- }
-}
diff --git a/InstrumentationTests/app/src/test/java/instructure/instrumentationtests/robo/RoboAppManager.java b/InstrumentationTests/app/src/test/java/instructure/instrumentationtests/robo/RoboAppManager.java
deleted file mode 100644
index 0a1d6570eb..0000000000
--- a/InstrumentationTests/app/src/test/java/instructure/instrumentationtests/robo/RoboAppManager.java
+++ /dev/null
@@ -1,32 +0,0 @@
-/*
- * Copyright (C) 2017 - 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 instructure.instrumentationtests.robo;
-
-import org.robolectric.shadows.gms.ShadowGooglePlayServicesUtil;
-
-import instructure.instrumentationtests.AppManager;
-
-public class RoboAppManager extends AppManager {
-
- @Override
- public void onCreate() {
- // See issue: https://github.com/robolectric/robolectric/issues/1995
- // For now we just return that Play Services is disabled.
- ShadowGooglePlayServicesUtil.setIsGooglePlayServicesAvailable(3);//3 == Disabled
- super.onCreate();
- }
-}
diff --git a/InstrumentationTests/app/src/test/java/instructure/instrumentationtests/robo/RoboTestCase.java b/InstrumentationTests/app/src/test/java/instructure/instrumentationtests/robo/RoboTestCase.java
deleted file mode 100644
index 645f67f840..0000000000
--- a/InstrumentationTests/app/src/test/java/instructure/instrumentationtests/robo/RoboTestCase.java
+++ /dev/null
@@ -1,55 +0,0 @@
-/*
- * Copyright (C) 2017 - 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 instructure.instrumentationtests.robo;
-
-import android.content.Context;
-import android.support.annotation.NonNull;
-
-import junit.framework.TestCase;
-
-import org.junit.Before;
-import org.junit.runner.RunWith;
-import org.robolectric.RuntimeEnvironment;
-import org.robolectric.annotation.Config;
-import org.robolectric.shadows.multidex.ShadowMultiDex;
-
-import instructure.instrumentationtests.BuildConfig;
-
-@RunWith(RoboTestRunner.class)
-@Config(constants = BuildConfig.class, shadows = ShadowMultiDex.class, sdk = RoboTestRunner.DEFAULT_SDK)
-public abstract class RoboTestCase extends TestCase {
-
- private RoboAppManager mApplication;
-
- @Override
- @Before
- public void setUp() throws Exception {
- super.setUp();
- }
-
- protected @NonNull RoboAppManager application() {
- if (mApplication != null) {
- return mApplication;
- }
- mApplication = (RoboAppManager) RuntimeEnvironment.application;
- return mApplication;
- }
-
- protected @NonNull Context context() {
- return application().getApplicationContext();
- }
-}
diff --git a/InstrumentationTests/app/src/test/java/instructure/instrumentationtests/robo/RoboTestRunner.java b/InstrumentationTests/app/src/test/java/instructure/instrumentationtests/robo/RoboTestRunner.java
deleted file mode 100644
index b0d6769968..0000000000
--- a/InstrumentationTests/app/src/test/java/instructure/instrumentationtests/robo/RoboTestRunner.java
+++ /dev/null
@@ -1,114 +0,0 @@
-/*
-Copyright 2016 Kickstarter, PBC
-
- 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 instructure.instrumentationtests.robo;
-
-import org.junit.runners.model.InitializationError;
-import org.robolectric.RobolectricTestRunner;
-import org.robolectric.annotation.Config;
-import org.robolectric.manifest.AndroidManifest;
-import org.robolectric.res.FileFsFile;
-import org.robolectric.util.Logger;
-import org.robolectric.util.ReflectionHelpers;
-
-public class RoboTestRunner extends RobolectricTestRunner {
-
- public static final int DEFAULT_SDK = 21;
- private static final String BUILD_OUTPUT = "build/intermediates";
-
- public RoboTestRunner(final Class> testClass) throws InitializationError {
- super(testClass);
- }
-
- @Override
- protected AndroidManifest getAppManifest(final Config config) {
- if (config.constants() == Void.class) {
- Logger.error("Field 'constants' not specified in @Config annotation");
- Logger.error("This is required when using RobolectricGradleTestRunner!");
- throw new RuntimeException("No 'constants' field in @Config annotation!");
- }
-
- final String type = getType(config);
- final String flavor = getFlavor(config);
- final String packageName = getPackageName(config);
-
- final FileFsFile res;
- final FileFsFile assets;
- final FileFsFile manifest;
-
- // res/merged added in Android Gradle plugin 1.3-beta1
- if (FileFsFile.from(BUILD_OUTPUT, "res", "merged").exists()) {
- res = FileFsFile.from(BUILD_OUTPUT, "res", "merged", flavor, type);
- } else if (FileFsFile.from(BUILD_OUTPUT, "res").exists()) {
- res = FileFsFile.from(BUILD_OUTPUT, "res", flavor, type);
- } else {
- res = FileFsFile.from(BUILD_OUTPUT, "bundles", flavor, type, "res");
- }
-
- if (FileFsFile.from(BUILD_OUTPUT, "assets").exists()) {
- assets = FileFsFile.from(BUILD_OUTPUT, "assets", flavor, type);
- } else {
- assets = FileFsFile.from(BUILD_OUTPUT, "bundles", flavor, type, "assets");
- }
-
- if (FileFsFile.from(BUILD_OUTPUT, "manifests").exists()) {
- manifest = FileFsFile.from(BUILD_OUTPUT, "manifests", "full", flavor, type, "AndroidManifest.xml");
- } else {
- manifest = FileFsFile.from(BUILD_OUTPUT, "bundles", flavor, type, "AndroidManifest.xml");
- }
-
- Logger.debug("Robolectric assets directory: " + assets.getPath());
- Logger.debug(" Robolectric res directory: " + res.getPath());
- Logger.debug(" Robolectric manifest path: " + manifest.getPath());
- Logger.debug(" Robolectric package name: " + packageName);
-
- return new AndroidManifest(manifest, res, assets) {
- @Override
- public String getRClassName() throws Exception {
- return instructure.instrumentationtests.R.class.getName();
- }
- };
- }
-
- private static String getType(final Config config) {
- try {
- return ReflectionHelpers.getStaticField(config.constants(), "BUILD_TYPE");
- } catch (Throwable e) {
- return null;
- }
- }
-
- private static String getFlavor(final Config config) {
- try {
- return ReflectionHelpers.getStaticField(config.constants(), "FLAVOR");
- } catch (Throwable e) {
- return null;
- }
- }
-
- private static String getPackageName(final Config config) {
- try {
- final String packageName = config.packageName();
- if (packageName != null && !packageName.isEmpty()) {
- return packageName;
- } else {
- return ReflectionHelpers.getStaticField(config.constants(), "APPLICATION_ID");
- }
- } catch (Throwable e) {
- return null;
- }
- }
-}
diff --git a/InstrumentationTests/app/src/test/resources/robolectric.properties b/InstrumentationTests/app/src/test/resources/robolectric.properties
deleted file mode 100644
index 0028dd332a..0000000000
--- a/InstrumentationTests/app/src/test/resources/robolectric.properties
+++ /dev/null
@@ -1,17 +0,0 @@
-#
-# Copyright (C) 2017 - 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.
-#
-
-application=instructure.instrumentationtests.robo.RoboAppManager
\ No newline at end of file
diff --git a/InstrumentationTests/build.gradle b/InstrumentationTests/build.gradle
deleted file mode 100644
index ffd49bb3c7..0000000000
--- a/InstrumentationTests/build.gradle
+++ /dev/null
@@ -1,66 +0,0 @@
-/*
- * Copyright (C) 2017 - 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.
- *
- */
-buildscript {
- /* Global constants */
- apply from: '../gradle/global.gradle'
-
- repositories {
- jcenter()
- }
- dependencies {
- classpath "com.android.tools.build:gradle:$GLOBAL_GRADLE_TOOLS_VERSION"
- classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$GLOBAL_KOTLIN_VERSION"
-
- // NOTE: Do not place your application dependencies here; they belong
- // in the individual module build.gradle files
- }
-}
-
-allprojects {
- repositories {
- jcenter()
- }
-}
-
-task clean(type: Delete) {
- delete rootProject.buildDir
-}
-
-ext {
- compileSdkVersion = GLOBAL_COMPILE_SDK
- buildToolsVersion = GLOBAL_BUILD_TOOLS_VERSION
-
- targetSdkVersion = GLOBAL_TARGET_SDK
- minSdkVersion = 17
-
- versionCode = 1
- versionName = '1.0d'
-
- supportLibraryVersion = GLOBAL_SUPPORT_LIBRARY_VERSION
- googlePlayServicesVersion = GLOBAL_PLAY_SERVICES_VERSION
-
- supportDependencies = [
- design : "com.android.support:design:$supportLibraryVersion",
- recyclerView : "com.android.support:recyclerview-v7:$supportLibraryVersion",
- appCompat : "com.android.support:appcompat-v7:$supportLibraryVersion",
- supportAnnotation: "com.android.support:support-annotations:$supportLibraryVersion",
- cardView: "com.android.support:cardview-v7:$supportLibraryVersion",
- gpsWearable: "com.google.android.gms:play-services-wearable:$googlePlayServicesVersion",
- supportLibV13: "com.android.support:support-v13:$supportLibraryVersion",
- percent: "com.android.support:percent:$supportLibraryVersion",
- ]
-}
diff --git a/InstrumentationTests/gradle b/InstrumentationTests/gradle
deleted file mode 120000
index a2df95ce1c..0000000000
--- a/InstrumentationTests/gradle
+++ /dev/null
@@ -1 +0,0 @@
-../gradle/gradle
\ No newline at end of file
diff --git a/InstrumentationTests/gradlew b/InstrumentationTests/gradlew
deleted file mode 120000
index 4936b5d3b2..0000000000
--- a/InstrumentationTests/gradlew
+++ /dev/null
@@ -1 +0,0 @@
-../gradle/gradlew
\ No newline at end of file
diff --git a/InstrumentationTests/gradlew.bat b/InstrumentationTests/gradlew.bat
deleted file mode 100644
index aec99730b4..0000000000
--- a/InstrumentationTests/gradlew.bat
+++ /dev/null
@@ -1,90 +0,0 @@
-@if "%DEBUG%" == "" @echo off
-@rem ##########################################################################
-@rem
-@rem Gradle startup script for Windows
-@rem
-@rem ##########################################################################
-
-@rem Set local scope for the variables with windows NT shell
-if "%OS%"=="Windows_NT" setlocal
-
-@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
-set DEFAULT_JVM_OPTS=
-
-set DIRNAME=%~dp0
-if "%DIRNAME%" == "" set DIRNAME=.
-set APP_BASE_NAME=%~n0
-set APP_HOME=%DIRNAME%
-
-@rem Find java.exe
-if defined JAVA_HOME goto findJavaFromJavaHome
-
-set JAVA_EXE=java.exe
-%JAVA_EXE% -version >NUL 2>&1
-if "%ERRORLEVEL%" == "0" goto init
-
-echo.
-echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
-echo.
-echo Please set the JAVA_HOME variable in your environment to match the
-echo location of your Java installation.
-
-goto fail
-
-:findJavaFromJavaHome
-set JAVA_HOME=%JAVA_HOME:"=%
-set JAVA_EXE=%JAVA_HOME%/bin/java.exe
-
-if exist "%JAVA_EXE%" goto init
-
-echo.
-echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
-echo.
-echo Please set the JAVA_HOME variable in your environment to match the
-echo location of your Java installation.
-
-goto fail
-
-:init
-@rem Get command-line arguments, handling Windowz variants
-
-if not "%OS%" == "Windows_NT" goto win9xME_args
-if "%@eval[2+2]" == "4" goto 4NT_args
-
-:win9xME_args
-@rem Slurp the command line arguments.
-set CMD_LINE_ARGS=
-set _SKIP=2
-
-:win9xME_args_slurp
-if "x%~1" == "x" goto execute
-
-set CMD_LINE_ARGS=%*
-goto execute
-
-:4NT_args
-@rem Get arguments from the 4NT Shell from JP Software
-set CMD_LINE_ARGS=%$
-
-:execute
-@rem Setup the command line
-
-set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
-
-@rem Execute Gradle
-"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
-
-:end
-@rem End local scope for the variables with windows NT shell
-if "%ERRORLEVEL%"=="0" goto mainEnd
-
-:fail
-rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
-rem the _cmd.exe /c_ return code!
-if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
-exit /b 1
-
-:mainEnd
-if "%OS%"=="Windows_NT" endlocal
-
-:omega
diff --git a/InstrumentationTests/settings.gradle b/InstrumentationTests/settings.gradle
deleted file mode 100644
index 3f8cda030c..0000000000
--- a/InstrumentationTests/settings.gradle
+++ /dev/null
@@ -1,16 +0,0 @@
-include ':app'
-include ':pandautils'
-include ':login-api-2'
-include ':recyclerview'
-include ':canvas-api-2'
-include ':canvas-api'
-include ':blueprint'
-include ':espresso'
-
-project(':canvas-api').projectDir = new File(rootProject.projectDir, '/../canvas-api')
-project(':pandautils').projectDir = new File(rootProject.projectDir, '/../pandautils')
-project(':login-api-2').projectDir = new File(rootProject.projectDir, '/../login-api-2')
-project(':recyclerview').projectDir = new File(rootProject.projectDir, '/../recyclerview')
-project(':canvas-api-2').projectDir = new File(rootProject.projectDir, '/../canvas-api-2/canvasapi')
-project(':blueprint').projectDir = new File(rootProject.projectDir, '/../blueprint')
-project(':espresso').projectDir = new File(rootProject.projectDir, '/../espresso')
diff --git a/README.md b/README.md
index 0f9832fe42..9e2c4c61d3 100644
--- a/README.md
+++ b/README.md
@@ -8,9 +8,9 @@ The open source code provided by the Android Team at Instructure.
First, install the Flutter SDK using the instructions found [here](https://flutter.dev/docs/get-started/install).
-Next, run `./open_source.sh` once. You may now use Gradle to build the apps. Prior to building the Student app for the first time, navigate to `libs/flutter_student_embed` and run the command `flutter pub get`.
+Next, run `./open_source.sh` once. You may now use Gradle to build the apps.
-### Student and Teacher
+### Student, Teacher and native Parent
1. Open `apps/build.gradle` in Android Studio
```
@@ -20,7 +20,7 @@ Android Studio > Import Project > canvas-android/apps/build.gradle
2. Select the app from the list of configurations (`student` or `teacher`)
3. Tap 'Run' (`^R`) to run the app
-### Parent
+### Flutter Parent
1. Open `canvas-android/apps/flutter_parent` in Android Studio.
2. Make sure the `main.dart` configuration is selected
@@ -34,7 +34,7 @@ Parent | (in apps/flutter_parent) `flutter pub get; flutter build apk` | [![Par
## Running tests
-To run unit tests for Student and Teacher
+To run unit tests for Student, Teacher and native Parent
1. Open the Build Variants window and set the variant to `qaDebug` for the app that you wish to test.
2. You can run the tests by tapping on the play button next to the test case or the test class.
@@ -58,19 +58,19 @@ App | Description
Module | Description
--- | ---
-BluePrint | An MVP Architecture that depends on PandaRecyclerView.
-Canvas-Api | *Deprecated* - Canvas for Android Api used to talk to Canvas LMS. (deprecated)
-Canvas-Api-2 | Canvas for Android Api used to talk to the Canvas LMS and is testable.
-dataseedingapi| gRPC wrapper for Canvas that enables creating data to test the apps
-Espresso | The UI testing library built on Espresso.
-SoSeedyCLI | CLI for using data seeding API manually
-SoSeedyGRPC | gRPC server for using data seeding with iOS from Xcode
-Foosball | A Foosball Application created and used interally to boost fun by over 120%.
-Login-Api | *Deprecated* - The Library used to making logging in and getting a token relatively easy. (deprecated)
-Login-Api-2 | The libarary used to make logging in and getting a token relative easy and is testable.
-PandaUtils | The core library for new features in the Student and Teacher apps.
-PandaRecyclerView | A fancy RecyclerView library that supports expanding and collapsing, pagination, and stuff like that.
-Rceditor | A wrapper for rich content editing used in Canvas Teacher.
+annotations | A wrapper for the PSPDFKit library and logic for annotation handling and converting in PDF documents.
+blueprint | An MVP Architecture that depends on PandaRecyclerView. (deprecated)
+buildSrc | Library for common gradle dependencies and gradle transformers that are used by the project.
+canvas-api-2 | Canvas for Android Api used to talk to the Canvas LMS and is testable.
+dataseedingapi | gRPC wrapper for Canvas that enables creating data to test the apps.
+DocumentScanner | A wrapper for document scanning features.
+espresso | The UI testing library built on Espresso.
+interactions | Interactions for navigation used in the apps.
+login-api-2 | The libarary used to make logging in and getting a token relative easy and is testable.
+pandares | Collection of resources used in our apps.
+pandautils | The core library for the apps. All the common code is implemented here that is reused by the 3 apps.
+rceditor | A wrapper for rich content editing used in our apps.
+recyclerview | A fancy RecyclerView library that supports expanding and collapsing, pagination, and stuff like that. (deprecated)
#### Our applications are licensed under the GPLv3 License.
diff --git a/android-vault b/android-vault
index e9ea50424a..15fababdb6 160000
--- a/android-vault
+++ b/android-vault
@@ -1 +1 @@
-Subproject commit e9ea50424a27d20bf744ccd5235750c0bdec9c2c
+Subproject commit 15fababdb67c36b68ad528108af12e7583724bf2
diff --git a/apps/flutter_parent/lib/l10n/app_localizations.dart b/apps/flutter_parent/lib/l10n/app_localizations.dart
index b7f03e6be3..a99c173e18 100644
--- a/apps/flutter_parent/lib/l10n/app_localizations.dart
+++ b/apps/flutter_parent/lib/l10n/app_localizations.dart
@@ -900,10 +900,10 @@ class AppLocalizations {
desc: 'Title for alerts when there is a course announcement',
);
- String get institutionAnnouncement => Intl.message(
- 'Institution Announcement',
- desc: 'Title for alerts when there is an institution announcement',
- );
+ String get globalAnnouncement => Intl.message(
+ 'Global Announcement',
+ desc: 'Title for alerts when there is a global announcement',
+ );
String assignmentGradeAboveThreshold(String threshold) => Intl.message(
'Assignment Grade Above $threshold',
@@ -1033,7 +1033,7 @@ class AppLocalizations {
String get courseAnnouncements => Intl.message('Course Announcements');
- String get institutionAnnouncements => Intl.message('Institution Announcements');
+ String get globalAnnouncements => Intl.message('Global Announcements');
String get never => Intl.message('Never',
desc: 'Indication that tells the user they will not receive alert notifications of a specific kind');
@@ -1619,8 +1619,8 @@ class AppLocalizations {
String get errorLoadingAnnouncement => Intl.message('There was an error loading this announcement',
desc: 'Message shown when an announcement detail screen fails to load');
- String get institutionAnnouncementTitle =>
- Intl.message('Institution Announcement', desc: 'Title text shown for institution level announcements');
+ String get globalAnnouncementTitle =>
+ Intl.message('Global Announcement', desc: 'Title text shown for institution level announcements');
String get genericNetworkError => Intl.message('Network error');
diff --git a/apps/flutter_parent/lib/models/assignment.dart b/apps/flutter_parent/lib/models/assignment.dart
index 3947713bdf..db39f7be6d 100644
--- a/apps/flutter_parent/lib/models/assignment.dart
+++ b/apps/flutter_parent/lib/models/assignment.dart
@@ -119,6 +119,9 @@ abstract class Assignment implements Built {
@BuiltValueField(wireName: 'submission_types')
BuiltList? get submissionTypes;
+ @BuiltValueField(wireName: 'hide_in_gradebook')
+ bool? get isHiddenInGradeBook;
+
static void _initializeBuilder(AssignmentBuilder b) => b
..pointsPossible = 0.0
..useRubricForGrading = false
diff --git a/apps/flutter_parent/lib/models/assignment.g.dart b/apps/flutter_parent/lib/models/assignment.g.dart
index 4b4ce37fd5..dd5c864e4c 100644
--- a/apps/flutter_parent/lib/models/assignment.g.dart
+++ b/apps/flutter_parent/lib/models/assignment.g.dart
@@ -248,6 +248,11 @@ class _$AssignmentSerializer implements StructuredSerializer {
..add(serializers.serialize(value,
specifiedType: const FullType(
BuiltList, const [const FullType(SubmissionTypes)])));
+ value = object.isHiddenInGradeBook;
+
+ result
+ ..add('hide_in_gradebook')
+ ..add(serializers.serialize(value, specifiedType: const FullType(bool)));
return result;
}
@@ -382,6 +387,10 @@ class _$AssignmentSerializer implements StructuredSerializer {
BuiltList, const [const FullType(SubmissionTypes)]))!
as BuiltList);
break;
+ case 'hide_in_gradebook':
+ result.isHiddenInGradeBook = serializers.deserialize(value,
+ specifiedType: const FullType(bool)) as bool?;
+ break;
}
}
@@ -523,6 +532,8 @@ class _$Assignment extends Assignment {
final bool isStudioEnabled;
@override
final BuiltList? submissionTypes;
+ @override
+ final bool? isHiddenInGradeBook;
factory _$Assignment([void Function(AssignmentBuilder)? updates]) =>
(new AssignmentBuilder()..update(updates))._build();
@@ -556,7 +567,8 @@ class _$Assignment extends Assignment {
required this.moderatedGrading,
required this.anonymousGrading,
required this.isStudioEnabled,
- this.submissionTypes})
+ this.submissionTypes,
+ this.isHiddenInGradeBook})
: super._() {
BuiltValueNullFieldError.checkNotNull(id, r'Assignment', 'id');
BuiltValueNullFieldError.checkNotNull(
@@ -626,7 +638,8 @@ class _$Assignment extends Assignment {
moderatedGrading == other.moderatedGrading &&
anonymousGrading == other.anonymousGrading &&
isStudioEnabled == other.isStudioEnabled &&
- submissionTypes == other.submissionTypes;
+ submissionTypes == other.submissionTypes &&
+ isHiddenInGradeBook == other.isHiddenInGradeBook;
}
@override
@@ -661,6 +674,7 @@ class _$Assignment extends Assignment {
_$hash = $jc(_$hash, anonymousGrading.hashCode);
_$hash = $jc(_$hash, isStudioEnabled.hashCode);
_$hash = $jc(_$hash, submissionTypes.hashCode);
+ _$hash = $jc(_$hash, isHiddenInGradeBook.hashCode);
_$hash = $jf(_$hash);
return _$hash;
}
@@ -696,7 +710,8 @@ class _$Assignment extends Assignment {
..add('moderatedGrading', moderatedGrading)
..add('anonymousGrading', anonymousGrading)
..add('isStudioEnabled', isStudioEnabled)
- ..add('submissionTypes', submissionTypes))
+ ..add('submissionTypes', submissionTypes)
+ ..add('isHiddenInGradeBook', isHiddenInGradeBook))
.toString();
}
}
@@ -838,6 +853,11 @@ class AssignmentBuilder implements Builder {
set submissionTypes(ListBuilder? submissionTypes) =>
_$this._submissionTypes = submissionTypes;
+ bool? _isHiddenInGradeBook;
+ bool? get isHiddenInGradeBook => _$this._isHiddenInGradeBook;
+ set isHiddenInGradeBook(bool? isHiddenInGradeBook) =>
+ _$this._isHiddenInGradeBook = isHiddenInGradeBook;
+
AssignmentBuilder() {
Assignment._initializeBuilder(this);
}
@@ -874,6 +894,7 @@ class AssignmentBuilder implements Builder {
_anonymousGrading = $v.anonymousGrading;
_isStudioEnabled = $v.isStudioEnabled;
_submissionTypes = $v.submissionTypes?.toBuilder();
+ _isHiddenInGradeBook = $v.isHiddenInGradeBook;
_$v = null;
}
return this;
@@ -936,7 +957,8 @@ class AssignmentBuilder implements Builder {
moderatedGrading: BuiltValueNullFieldError.checkNotNull(moderatedGrading, r'Assignment', 'moderatedGrading'),
anonymousGrading: BuiltValueNullFieldError.checkNotNull(anonymousGrading, r'Assignment', 'anonymousGrading'),
isStudioEnabled: BuiltValueNullFieldError.checkNotNull(isStudioEnabled, r'Assignment', 'isStudioEnabled'),
- submissionTypes: _submissionTypes?.build());
+ submissionTypes: _submissionTypes?.build(),
+ isHiddenInGradeBook: isHiddenInGradeBook);
} catch (_) {
late String _$failedField;
try {
diff --git a/apps/flutter_parent/lib/models/help_link.dart b/apps/flutter_parent/lib/models/help_link.dart
index 467ed5b648..af8241783a 100644
--- a/apps/flutter_parent/lib/models/help_link.dart
+++ b/apps/flutter_parent/lib/models/help_link.dart
@@ -28,18 +28,18 @@ abstract class HelpLink implements Built {
factory HelpLink([void Function(HelpLinkBuilder) updates]) = _$HelpLink;
- String get id;
+ String? get id;
String get type;
@BuiltValueField(wireName: 'available_to')
BuiltList get availableTo;
- String get url;
+ String? get url;
- String get text;
+ String? get text;
- String get subtext;
+ String? get subtext;
}
class AvailableTo extends EnumClass {
diff --git a/apps/flutter_parent/lib/models/help_link.g.dart b/apps/flutter_parent/lib/models/help_link.g.dart
index a596b2cb39..e9bd162ddf 100644
--- a/apps/flutter_parent/lib/models/help_link.g.dart
+++ b/apps/flutter_parent/lib/models/help_link.g.dart
@@ -55,22 +55,38 @@ class _$HelpLinkSerializer implements StructuredSerializer {
Iterable serialize(Serializers serializers, HelpLink object,
{FullType specifiedType = FullType.unspecified}) {
final result = [
- 'id',
- serializers.serialize(object.id, specifiedType: const FullType(String)),
'type',
serializers.serialize(object.type, specifiedType: const FullType(String)),
'available_to',
serializers.serialize(object.availableTo,
specifiedType:
const FullType(BuiltList, const [const FullType(AvailableTo)])),
- 'url',
- serializers.serialize(object.url, specifiedType: const FullType(String)),
- 'text',
- serializers.serialize(object.text, specifiedType: const FullType(String)),
- 'subtext',
- serializers.serialize(object.subtext,
- specifiedType: const FullType(String)),
];
+ Object? value;
+ value = object.id;
+
+ result
+ ..add('id')
+ ..add(
+ serializers.serialize(value, specifiedType: const FullType(String)));
+ value = object.url;
+
+ result
+ ..add('url')
+ ..add(
+ serializers.serialize(value, specifiedType: const FullType(String)));
+ value = object.text;
+
+ result
+ ..add('text')
+ ..add(
+ serializers.serialize(value, specifiedType: const FullType(String)));
+ value = object.subtext;
+
+ result
+ ..add('subtext')
+ ..add(
+ serializers.serialize(value, specifiedType: const FullType(String)));
return result;
}
@@ -88,7 +104,7 @@ class _$HelpLinkSerializer implements StructuredSerializer {
switch (key) {
case 'id':
result.id = serializers.deserialize(value,
- specifiedType: const FullType(String))! as String;
+ specifiedType: const FullType(String)) as String?;
break;
case 'type':
result.type = serializers.deserialize(value,
@@ -102,15 +118,15 @@ class _$HelpLinkSerializer implements StructuredSerializer {
break;
case 'url':
result.url = serializers.deserialize(value,
- specifiedType: const FullType(String))! as String;
+ specifiedType: const FullType(String)) as String?;
break;
case 'text':
result.text = serializers.deserialize(value,
- specifiedType: const FullType(String))! as String;
+ specifiedType: const FullType(String)) as String?;
break;
case 'subtext':
result.subtext = serializers.deserialize(value,
- specifiedType: const FullType(String))! as String;
+ specifiedType: const FullType(String)) as String?;
break;
}
}
@@ -138,36 +154,32 @@ class _$AvailableToSerializer implements PrimitiveSerializer {
class _$HelpLink extends HelpLink {
@override
- final String id;
+ final String? id;
@override
final String type;
@override
final BuiltList availableTo;
@override
- final String url;
+ final String? url;
@override
- final String text;
+ final String? text;
@override
- final String subtext;
+ final String? subtext;
factory _$HelpLink([void Function(HelpLinkBuilder)? updates]) =>
(new HelpLinkBuilder()..update(updates))._build();
_$HelpLink._(
- {required this.id,
+ {this.id,
required this.type,
required this.availableTo,
- required this.url,
- required this.text,
- required this.subtext})
+ this.url,
+ this.text,
+ this.subtext})
: super._() {
- BuiltValueNullFieldError.checkNotNull(id, r'HelpLink', 'id');
BuiltValueNullFieldError.checkNotNull(type, r'HelpLink', 'type');
BuiltValueNullFieldError.checkNotNull(
availableTo, r'HelpLink', 'availableTo');
- BuiltValueNullFieldError.checkNotNull(url, r'HelpLink', 'url');
- BuiltValueNullFieldError.checkNotNull(text, r'HelpLink', 'text');
- BuiltValueNullFieldError.checkNotNull(subtext, r'HelpLink', 'subtext');
}
@override
@@ -279,16 +291,13 @@ class HelpLinkBuilder implements Builder {
try {
_$result = _$v ??
new _$HelpLink._(
- id: BuiltValueNullFieldError.checkNotNull(id, r'HelpLink', 'id'),
+ id: id,
type: BuiltValueNullFieldError.checkNotNull(
type, r'HelpLink', 'type'),
availableTo: availableTo.build(),
- url: BuiltValueNullFieldError.checkNotNull(
- url, r'HelpLink', 'url'),
- text: BuiltValueNullFieldError.checkNotNull(
- text, r'HelpLink', 'text'),
- subtext: BuiltValueNullFieldError.checkNotNull(
- subtext, r'HelpLink', 'subtext'));
+ url: url,
+ text: text,
+ subtext: subtext);
} catch (_) {
late String _$failedField;
try {
diff --git a/apps/flutter_parent/lib/network/utils/dio_config.dart b/apps/flutter_parent/lib/network/utils/dio_config.dart
index eaa8e90f1c..7265d0b9fb 100644
--- a/apps/flutter_parent/lib/network/utils/dio_config.dart
+++ b/apps/flutter_parent/lib/network/utils/dio_config.dart
@@ -22,7 +22,6 @@ import 'package:flutter/widgets.dart';
import 'package:flutter_parent/network/utils/api_prefs.dart';
import 'package:flutter_parent/network/utils/authentication_interceptor.dart';
import 'package:flutter_parent/utils/debug_flags.dart';
-import 'package:path_provider/path_provider.dart';
import 'private_consts.dart';
@@ -153,8 +152,15 @@ class DioConfig {
String? overrideToken = null,
Map? extraHeaders = null,
PageSize pageSize = PageSize.none,
+ bool disableFileVerifiers = true,
}) {
Map? extraParams = ApiPrefs.isMasquerading() ? {'as_user_id': ApiPrefs.getUser()?.id} : null;
+
+ if (disableFileVerifiers) {
+ extraParams ??= {};
+ extraParams['no_verifiers'] = 1;
+ }
+
return DioConfig(
baseUrl: includeApiPath ? ApiPrefs.getApiUrl() : '${ApiPrefs.getDomain()}/',
baseHeaders: ApiPrefs.getHeaderMap(
@@ -236,6 +242,7 @@ Dio canvasDio({
String? overrideToken = null,
Map? extraHeaders = null,
PageSize pageSize = PageSize.none,
+ bool disableFileVerifiers = true,
}) {
return DioConfig.canvas(
forceRefresh: forceRefresh,
@@ -243,7 +250,8 @@ Dio canvasDio({
overrideToken: overrideToken,
extraHeaders: extraHeaders,
pageSize: pageSize,
- includeApiPath: includeApiPath)
+ includeApiPath: includeApiPath,
+ disableFileVerifiers: disableFileVerifiers)
.dio;
}
diff --git a/apps/flutter_parent/lib/screens/alert_thresholds/alert_thresholds_extensions.dart b/apps/flutter_parent/lib/screens/alert_thresholds/alert_thresholds_extensions.dart
index 6fb99e039f..5d84a00c8d 100644
--- a/apps/flutter_parent/lib/screens/alert_thresholds/alert_thresholds_extensions.dart
+++ b/apps/flutter_parent/lib/screens/alert_thresholds/alert_thresholds_extensions.dart
@@ -40,7 +40,7 @@ extension GetTitleFromAlert on AlertType {
title = L10n(context).courseAnnouncements;
break;
case AlertType.institutionAnnouncement:
- title = L10n(context).institutionAnnouncements;
+ title = L10n(context).globalAnnouncements;
break;
default:
title = L10n(context).unexpectedError;
diff --git a/apps/flutter_parent/lib/screens/alerts/alerts_screen.dart b/apps/flutter_parent/lib/screens/alerts/alerts_screen.dart
index d9ae32d731..d8ff0af4b1 100644
--- a/apps/flutter_parent/lib/screens/alerts/alerts_screen.dart
+++ b/apps/flutter_parent/lib/screens/alerts/alerts_screen.dart
@@ -224,7 +224,7 @@ class __AlertsListState extends State<_AlertsList> {
String title = '';
switch (alert.alertType) {
case AlertType.institutionAnnouncement:
- title = l10n.institutionAnnouncement;
+ title = l10n.globalAnnouncement;
break;
case AlertType.courseAnnouncement:
title = l10n.courseAnnouncement;
diff --git a/apps/flutter_parent/lib/screens/announcements/announcement_details_screen.dart b/apps/flutter_parent/lib/screens/announcements/announcement_details_screen.dart
index 71a9c73bea..300018f004 100644
--- a/apps/flutter_parent/lib/screens/announcements/announcement_details_screen.dart
+++ b/apps/flutter_parent/lib/screens/announcements/announcement_details_screen.dart
@@ -46,7 +46,7 @@ class _AnnouncementDetailScreenState extends State {
widget.announcementId,
widget.announcementType,
widget.courseId,
- L10n(context).institutionAnnouncementTitle,
+ L10n(context).globalAnnouncementTitle,
forceRefresh,
);
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 d51733c091..3e1148424b 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
@@ -84,7 +84,7 @@ class CourseDetailsModel extends BaseModel {
final groupFuture = _interactor()
.loadAssignmentGroups(courseId, student?.id, _nextGradingPeriod?.id, forceRefresh: forceRefresh).then((groups) async {
// Remove unpublished assignments to match web
- return groups?.map((group) => (group.toBuilder()..assignments.removeWhere((assignment) => !assignment.published)).build()).toList();
+ return groups?.map((group) => (group.toBuilder()..assignments.removeWhere((assignment) => !assignment.published || assignment.isHiddenInGradeBook == true)).build()).toList();
});
final gradingPeriodsFuture =
diff --git a/apps/flutter_parent/lib/screens/help/help_screen.dart b/apps/flutter_parent/lib/screens/help/help_screen.dart
index 4e468a5720..4d2ca0edf1 100644
--- a/apps/flutter_parent/lib/screens/help/help_screen.dart
+++ b/apps/flutter_parent/lib/screens/help/help_screen.dart
@@ -65,8 +65,8 @@ class _HelpScreenState extends State {
List _generateLinks(List? links) {
List helpLinks = List.from(links?.map(
(l) => ListTile(
- title: Text(l.text, style: Theme.of(context).textTheme.titleMedium),
- subtitle: Text(l.subtext, style: Theme.of(context).textTheme.bodySmall),
+ title: Text(l.text ?? '', style: Theme.of(context).textTheme.titleMedium),
+ subtitle: Text(l.subtext ?? '', style: Theme.of(context).textTheme.bodySmall),
onTap: () => _linkClick(l),
),
) ?? []);
@@ -84,7 +84,7 @@ class _HelpScreenState extends State {
}
void _linkClick(HelpLink link) {
- String url = link.url;
+ String url = link.url ?? '';
if (url[0] == '#') {
// Internal link
if (url.contains('#create_ticket')) {
@@ -93,24 +93,24 @@ class _HelpScreenState extends State {
// Custom for Android
_showShareLove();
}
- } else if (link.id.contains('submit_feature_idea')) {
+ } else if (link.id?.contains('submit_feature_idea') == true) {
_showRequestFeature();
- } else if (link.url.startsWith('tel:+')) {
+ } else if (url.startsWith('tel:+')) {
// Support phone links: https://community.canvaslms.com/docs/DOC-12664-4214610054
- locator().launchPhone(link.url);
- } else if (link.url.startsWith('mailto:')) {
+ locator().launchPhone(url);
+ } else if (url.startsWith('mailto:')) {
// Support mailto links: https://community.canvaslms.com/docs/DOC-12664-4214610054
- locator().launchEmail(link.url);
- } else if (link.url.contains('cases.canvaslms.com/liveagentchat')) {
+ locator().launchEmail(url);
+ } else if (url.contains('cases.canvaslms.com/liveagentchat')) {
// Chat with Canvas Support - Doesn't seem work properly with WebViews, so we kick it out
// to the external browser
- locator().launch(link.url);
- } else if (link.id.contains('search_the_canvas_guides')) {
+ locator().launch(url);
+ } else if (link.id?.contains('search_the_canvas_guides') == true) {
// Send them to the mobile Canvas guides
_showSearch();
} else {
// External url
- locator().launch(link.url);
+ locator().launch(url);
}
}
diff --git a/apps/flutter_parent/lib/screens/help/help_screen_interactor.dart b/apps/flutter_parent/lib/screens/help/help_screen_interactor.dart
index cc143cd67d..3fdc29789b 100644
--- a/apps/flutter_parent/lib/screens/help/help_screen_interactor.dart
+++ b/apps/flutter_parent/lib/screens/help/help_screen_interactor.dart
@@ -34,6 +34,7 @@ class HelpScreenInteractor {
link.availableTo.contains(AvailableTo.user));
List filterObserverLinks(BuiltList list) => list
+ .where((link) => link.url != null && link.text != null)
.where((link) =>
link.availableTo.contains(AvailableTo.observer) ||
link.availableTo.contains(AvailableTo.user))
diff --git a/apps/flutter_parent/pubspec.yaml b/apps/flutter_parent/pubspec.yaml
index 9f1ace5fff..2c30a63c85 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.11.0+51
+version: 3.12.0+52
module:
androidX: true
diff --git a/apps/flutter_parent/test/network/dio_config_test.dart b/apps/flutter_parent/test/network/dio_config_test.dart
index c0a7a3591b..9b2591e15b 100644
--- a/apps/flutter_parent/test/network/dio_config_test.dart
+++ b/apps/flutter_parent/test/network/dio_config_test.dart
@@ -79,7 +79,14 @@ void main() {
var dio = await canvasDio(pageSize: PageSize(perPageSize));
final options = dio.options;
- expect(options.queryParameters, {'per_page': perPageSize});
+ expect(options.queryParameters['per_page'], 1);
+ });
+
+ test('sets no verifiers param by default', () async {
+ var dio = await canvasDio();
+ final options = dio.options;
+
+ expect(options.queryParameters['no_verifiers'], 1);
});
test('sets as_user_id param when masquerading', () async {
diff --git a/apps/flutter_parent/test/screens/alert_thresholds/alert_thresholds_screen_test.dart b/apps/flutter_parent/test/screens/alert_thresholds/alert_thresholds_screen_test.dart
index 141cd83e3e..476b541cec 100644
--- a/apps/flutter_parent/test/screens/alert_thresholds/alert_thresholds_screen_test.dart
+++ b/apps/flutter_parent/test/screens/alert_thresholds/alert_thresholds_screen_test.dart
@@ -99,7 +99,7 @@ void main() {
expect(_percentageThresholdFinder(AppLocalizations().assignmentGradeBelow), findsOneWidget);
expect(_percentageThresholdFinder(AppLocalizations().assignmentGradeAbove), findsOneWidget);
expect(_switchThresholdFinder(AppLocalizations().courseAnnouncements), findsOneWidget);
- expect(_switchThresholdFinder(AppLocalizations().institutionAnnouncements), findsOneWidget);
+ expect(_switchThresholdFinder(AppLocalizations().globalAnnouncements), findsOneWidget);
});
testWidgetsWithAccessibilityChecks('shows delete option if student can be deleted', (tester) async {
diff --git a/apps/flutter_parent/test/screens/alerts/alerts_screen_test.dart b/apps/flutter_parent/test/screens/alerts/alerts_screen_test.dart
index fed81ebfc1..0a0bd2ac27 100644
--- a/apps/flutter_parent/test/screens/alerts/alerts_screen_test.dart
+++ b/apps/flutter_parent/test/screens/alerts/alerts_screen_test.dart
@@ -181,7 +181,7 @@ void main() {
await tester.pumpWidget(_testableWidget());
await tester.pumpAndSettle();
- final title = find.text(AppLocalizations().institutionAnnouncement);
+ final title = find.text(AppLocalizations().globalAnnouncement);
expect(title, findsOneWidget);
expect((tester.widget(title) as Text).style!.color, ParentColors.ash);
expect(find.text(alerts.first.title), findsOneWidget);
diff --git a/apps/flutter_parent/test/screens/announcements/announcement_details_screen_test.dart b/apps/flutter_parent/test/screens/announcements/announcement_details_screen_test.dart
index 17289b1b87..2031a16b32 100644
--- a/apps/flutter_parent/test/screens/announcements/announcement_details_screen_test.dart
+++ b/apps/flutter_parent/test/screens/announcements/announcement_details_screen_test.dart
@@ -93,7 +93,7 @@ void main() {
final response = AnnouncementViewState(courseName, announcementSubject, announcementMessage, postedAt, null);
when(interactor.getAnnouncement(
- announcementId, AnnouncementType.COURSE, courseId, AppLocalizations().institutionAnnouncementTitle, any))
+ announcementId, AnnouncementType.COURSE, courseId, AppLocalizations().globalAnnouncementTitle, any))
.thenAnswer((_) => Future.value(response));
await tester.pumpWidget(_testableWidget(announcementId, AnnouncementType.COURSE, courseId));
@@ -119,7 +119,7 @@ void main() {
final response = AnnouncementViewState(courseName, announcementSubject, announcementMessage, postedAt, null);
when(interactor.getAnnouncement(
- announcementId, AnnouncementType.COURSE, courseId, AppLocalizations().institutionAnnouncementTitle, any))
+ announcementId, AnnouncementType.COURSE, courseId, AppLocalizations().globalAnnouncementTitle, any))
.thenAnswer((_) => Future.value(response));
await tester.pumpWidget(_testableWidget(announcementId, AnnouncementType.COURSE, courseId));
@@ -145,7 +145,7 @@ void main() {
final response =
AnnouncementViewState(courseName, announcementSubject, announcementMessage, postedAt, attachment);
when(interactor.getAnnouncement(
- announcementId, AnnouncementType.COURSE, courseId, AppLocalizations().institutionAnnouncementTitle, any))
+ announcementId, AnnouncementType.COURSE, courseId, AppLocalizations().globalAnnouncementTitle, any))
.thenAnswer((_) => Future.value(response));
await tester.pumpWidget(_testableWidget(announcementId, AnnouncementType.COURSE, courseId));
@@ -167,7 +167,7 @@ void main() {
final announcementMessage = 'hodor';
final announcementSubject = 'hodor subject';
final postedAt = DateTime.now();
- final toolbarTitle = AppLocalizations().institutionAnnouncementTitle;
+ final toolbarTitle = AppLocalizations().globalAnnouncementTitle;
final response = AnnouncementViewState(toolbarTitle, announcementSubject, announcementMessage, postedAt, null);
when(interactor.getAnnouncement(announcementId, AnnouncementType.INSTITUTION, courseId, toolbarTitle, any))
diff --git a/apps/flutter_parent/test/screens/help/help_screen_interactor_test.dart b/apps/flutter_parent/test/screens/help/help_screen_interactor_test.dart
index 00178d1d4a..2b827e1103 100644
--- a/apps/flutter_parent/test/screens/help/help_screen_interactor_test.dart
+++ b/apps/flutter_parent/test/screens/help/help_screen_interactor_test.dart
@@ -96,6 +96,21 @@ void main() {
observerLinks);
});
+ test('filterObserverLinks only returns links that has text and url', () async {
+ var validLinks = [
+ createHelpLink(availableTo: [AvailableTo.observer]),
+ createHelpLink(availableTo: [AvailableTo.user]),
+ ];
+
+ var invalidLinks = [
+ createNullableHelpLink(url: 'url', availableTo: [AvailableTo.observer]),
+ createNullableHelpLink(text: 'text', availableTo: [AvailableTo.observer]),
+ ];
+
+ expect(HelpScreenInteractor().filterObserverLinks(BuiltList.from([...validLinks, ...invalidLinks])),
+ validLinks);
+ });
+
test('custom list is returned if there are any custom lists', () async {
var api = MockHelpLinksApi();
var customLinks = [
@@ -144,3 +159,11 @@ HelpLink createHelpLink({String? id, String? text, String? url, List? availableTo}) => HelpLink((b) => b
+ ..id = id
+ ..type = ''
+ ..availableTo = ListBuilder(availableTo != null ? availableTo : [])
+ ..url = url
+ ..text = text
+ ..subtext = 'subtext');
\ No newline at end of file
diff --git a/apps/parent/build.gradle b/apps/parent/build.gradle
index ce64c3d77b..79e052b28f 100644
--- a/apps/parent/build.gradle
+++ b/apps/parent/build.gradle
@@ -22,6 +22,7 @@ plugins {
id 'kotlin-kapt'
id 'com.google.firebase.crashlytics'
id 'dagger.hilt.android.plugin'
+ id 'org.jetbrains.kotlin.plugin.compose'
}
configurations {
@@ -39,8 +40,8 @@ android {
applicationId "com.instructure.parentapp"
minSdkVersion Versions.MIN_SDK
targetSdkVersion Versions.TARGET_SDK
- versionCode 50
- versionName "3.10.0"
+ versionCode 52
+ versionName "3.12.0"
buildConfigField "boolean", "IS_TESTING", "false"
testInstrumentationRunner 'com.instructure.parentapp.ui.espresso.ParentHiltTestRunner'
@@ -149,9 +150,6 @@ android {
enableAggregatingTask = false
enableExperimentalClasspathAggregation = true
}
- composeOptions {
- kotlinCompilerExtensionVersion = Versions.KOTLIN_COMPOSE_COMPILER_VERSION
- }
testOptions.animationsDisabled = true
}
@@ -212,7 +210,7 @@ dependencies {
implementation Libs.ANDROIDX_BROWSER
implementation Libs.ANDROIDX_CARDVIEW
implementation Libs.ANDROIDX_CONSTRAINT_LAYOUT
- implementation Libs.ANDROIDX_DESIGN
+ implementation Libs.MATERIAL_DESIGN
implementation Libs.ANDROIDX_RECYCLERVIEW
implementation Libs.ANDROIDX_PALETTE
implementation Libs.PLAY_IN_APP_UPDATES
diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/compose/alerts/details/AnnouncementDetailsScreenTest.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/compose/alerts/details/AnnouncementDetailsScreenTest.kt
new file mode 100644
index 0000000000..18dd4f5798
--- /dev/null
+++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/compose/alerts/details/AnnouncementDetailsScreenTest.kt
@@ -0,0 +1,183 @@
+/*
+ * Copyright (C) 2024 - 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.parentapp.ui.compose.alerts.details
+
+import android.graphics.Color
+import androidx.compose.ui.test.assertHasClickAction
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.onNodeWithText
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.platform.app.InstrumentationRegistry
+import com.instructure.canvasapi2.models.Attachment
+import com.instructure.canvasapi2.utils.DateHelper
+import com.instructure.pandares.R
+import com.instructure.parentapp.features.alerts.details.AnnouncementDetailsScreen
+import com.instructure.parentapp.features.alerts.details.AnnouncementDetailsUiState
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import java.time.Instant
+import java.util.Date
+
+@RunWith(AndroidJUnit4::class)
+class AnnouncementDetailsScreenTest {
+
+ @get:Rule
+ val composeTestRule = createComposeRule()
+
+ @Test
+ fun assertContent() {
+ val uiState = AnnouncementDetailsUiState(
+ studentColor = 1,
+ pageTitle = "Course Name",
+ announcementTitle = "Alert Title",
+ message = "Alert Message",
+ postedDate = Date.from(
+ Instant.parse("2024-01-03T00:00:00Z")
+ ),
+ attachment = Attachment(
+ id = 1,
+ filename = "attachment_file_name",
+ size = 100,
+ displayName = "File Name",
+ thumbnailUrl = "thumbnail_url"
+ )
+ )
+ composeTestRule.setContent {
+ AnnouncementDetailsScreen(
+ uiState = uiState,
+ navigationActionClick = {},
+ actionHandler = {}
+ )
+ }
+
+ composeTestRule.onNodeWithText("Course Name").assertIsDisplayed()
+ composeTestRule.onNodeWithText("Alert Title").assertIsDisplayed()
+ composeTestRule.onNodeWithText("attachment_file_name").assertIsDisplayed()
+ composeTestRule.onNodeWithTag("attachment").assertIsDisplayed().assertHasClickAction()
+ val dateString = DateHelper.getDateAtTimeString(
+ InstrumentationRegistry.getInstrumentation().targetContext,
+ R.string.alertDateTime,
+ uiState.postedDate
+ )
+ dateString?.let {
+ composeTestRule.onNodeWithText(it).assertIsDisplayed()
+ }
+ }
+
+ @Test
+ fun assertSnackbar() {
+ val uiState = AnnouncementDetailsUiState(
+ showErrorSnack = true,
+ studentColor = 1,
+ pageTitle = "Course Name",
+ announcementTitle = "Alert Title",
+ message = "Alert Message",
+ postedDate = Date.from(
+ Instant.parse("2024-01-03T00:00:00Z")
+ ),
+ attachment = Attachment(
+ id = 1,
+ filename = "attachment_file_name",
+ size = 100,
+ displayName = "File Name",
+ thumbnailUrl = "thumbnail_url"
+ )
+ )
+ composeTestRule.setContent {
+ AnnouncementDetailsScreen(
+ uiState = uiState,
+ navigationActionClick = {},
+ actionHandler = {}
+ )
+ }
+
+ composeTestRule.onNodeWithText("There was an error loading this announcement")
+ .assertIsDisplayed()
+ composeTestRule.onNodeWithText("Course Name").assertIsDisplayed()
+ composeTestRule.onNodeWithText("Alert Title").assertIsDisplayed()
+ composeTestRule.onNodeWithText("attachment_file_name").assertIsDisplayed()
+ val dateString = DateHelper.getDateAtTimeString(
+ InstrumentationRegistry.getInstrumentation().targetContext,
+ R.string.alertDateTime,
+ uiState.postedDate
+ )
+ dateString?.let {
+ composeTestRule.onNodeWithText(it).assertIsDisplayed()
+ }
+ }
+
+ @Test
+ fun assertError() {
+ composeTestRule.setContent {
+ AnnouncementDetailsScreen(
+ uiState = AnnouncementDetailsUiState(
+ isLoading = false,
+ isError = true,
+ isRefreshing = false,
+ studentColor = Color.BLUE,
+ ),
+ navigationActionClick = {},
+ actionHandler = {}
+ )
+ }
+
+
+ composeTestRule.onNodeWithText("There was an error loading this announcement")
+ .assertIsDisplayed()
+ composeTestRule.onNodeWithText("Retry").assertHasClickAction().assertIsDisplayed()
+ }
+
+ @Test
+ fun assertLoading() {
+ composeTestRule.setContent {
+ AnnouncementDetailsScreen(
+ uiState = AnnouncementDetailsUiState(
+ isLoading = true,
+ isError = false,
+ isRefreshing = false,
+ studentColor = Color.BLUE,
+ ),
+ navigationActionClick = {},
+ actionHandler = {}
+ )
+ }
+
+ composeTestRule.onNodeWithTag("loading").assertIsDisplayed()
+ }
+
+ @Test
+ fun assertRefreshing() {
+ composeTestRule.setContent {
+ AnnouncementDetailsScreen(
+ uiState = AnnouncementDetailsUiState(
+ isLoading = false,
+ isError = false,
+ isRefreshing = true,
+ studentColor = Color.BLUE,
+ ),
+ navigationActionClick = {},
+ actionHandler = {}
+ )
+ }
+
+ composeTestRule.onNodeWithTag("pullRefreshIndicator").assertIsDisplayed()
+ }
+}
diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/compose/alerts/list/AlertsListItemTest.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/compose/alerts/list/AlertsListItemTest.kt
index 954c889f0a..e812d46ab9 100644
--- a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/compose/alerts/list/AlertsListItemTest.kt
+++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/compose/alerts/list/AlertsListItemTest.kt
@@ -46,6 +46,7 @@ class AlertsListItemTest {
composeTestRule.setContent {
AlertsListItem(alert = AlertsItemUiState(
alertId = 1L,
+ contextId = 10L,
title = "Assignment Missing title",
alertType = AlertType.ASSIGNMENT_MISSING,
date = Date(),
@@ -70,6 +71,7 @@ class AlertsListItemTest {
composeTestRule.setContent {
AlertsListItem(alert = AlertsItemUiState(
alertId = 1L,
+ contextId = 10L,
title = "Assignment Grade High title",
alertType = AlertType.ASSIGNMENT_GRADE_HIGH,
date = Date(),
@@ -92,6 +94,7 @@ class AlertsListItemTest {
composeTestRule.setContent {
AlertsListItem(alert = AlertsItemUiState(
alertId = 1L,
+ contextId = 10L,
title = "Assignment Grade Low title",
alertType = AlertType.ASSIGNMENT_GRADE_LOW,
date = Date(),
@@ -114,6 +117,7 @@ class AlertsListItemTest {
composeTestRule.setContent {
AlertsListItem(alert = AlertsItemUiState(
alertId = 1L,
+ contextId = 10L,
title = "Course Grade High title",
alertType = AlertType.COURSE_GRADE_HIGH,
date = Date(),
@@ -136,6 +140,7 @@ class AlertsListItemTest {
composeTestRule.setContent {
AlertsListItem(alert = AlertsItemUiState(
alertId = 1L,
+ contextId = 10L,
title = "Course Grade Low title",
alertType = AlertType.COURSE_GRADE_LOW,
date = Date(),
@@ -158,6 +163,7 @@ class AlertsListItemTest {
composeTestRule.setContent {
AlertsListItem(alert = AlertsItemUiState(
alertId = 1L,
+ contextId = 10L,
title = "Course Announcement title",
alertType = AlertType.COURSE_ANNOUNCEMENT,
date = Date(),
@@ -180,7 +186,8 @@ class AlertsListItemTest {
composeTestRule.setContent {
AlertsListItem(alert = AlertsItemUiState(
alertId = 1L,
- title = "Institution Announcement title",
+ contextId = 10L,
+ title = "Global Announcement title",
alertType = AlertType.INSTITUTION_ANNOUNCEMENT,
date = Date(),
observerAlertThreshold = null,
@@ -190,8 +197,8 @@ class AlertsListItemTest {
), userColor = Color.BLUE, actionHandler = {})
}
- composeTestRule.onNodeWithText("Institution Announcement").assertIsDisplayed()
- composeTestRule.onNodeWithText("Institution Announcement title").assertIsDisplayed()
+ composeTestRule.onNodeWithText("Global Announcement").assertIsDisplayed()
+ composeTestRule.onNodeWithText("Global Announcement title").assertIsDisplayed()
composeTestRule.onNode(hasDrawable(R.drawable.ic_info)).assertIsDisplayed()
composeTestRule.onNodeWithText(parseDate(Date())).assertIsDisplayed()
composeTestRule.onNodeWithTag("alertItem").assertHasClickAction()
@@ -202,6 +209,7 @@ class AlertsListItemTest {
composeTestRule.setContent {
AlertsListItem(alert = AlertsItemUiState(
alertId = 1L,
+ contextId = 10L,
title = "Locked for User title",
alertType = AlertType.ASSIGNMENT_MISSING,
date = Date(),
@@ -224,7 +232,8 @@ class AlertsListItemTest {
composeTestRule.setContent {
AlertsListItem(alert = AlertsItemUiState(
alertId = 1L,
- title = "Institution Announcement title",
+ contextId = 10L,
+ title = "Global Announcement title",
alertType = AlertType.INSTITUTION_ANNOUNCEMENT,
date = Date(),
observerAlertThreshold = null,
@@ -234,8 +243,8 @@ class AlertsListItemTest {
), userColor = Color.BLUE, actionHandler = {})
}
- composeTestRule.onNodeWithText("Institution Announcement").assertIsDisplayed()
- composeTestRule.onNodeWithText("Institution Announcement title").assertIsDisplayed()
+ composeTestRule.onNodeWithText("Global Announcement").assertIsDisplayed()
+ composeTestRule.onNodeWithText("Global Announcement title").assertIsDisplayed()
composeTestRule.onNode(hasDrawable(R.drawable.ic_info)).assertIsDisplayed()
composeTestRule.onNodeWithText(parseDate(Date())).assertIsDisplayed()
composeTestRule.onNodeWithTag("alertItem").assertHasClickAction()
diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/compose/alerts/list/AlertsScreenTest.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/compose/alerts/list/AlertsScreenTest.kt
index b98af51b5f..0f7c763fbc 100644
--- a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/compose/alerts/list/AlertsScreenTest.kt
+++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/compose/alerts/list/AlertsScreenTest.kt
@@ -114,6 +114,7 @@ class AlertsScreenTest {
val items = listOf(
AlertsItemUiState(
alertId = 1,
+ contextId = 10,
title = "Alert 1",
alertType = AlertType.ASSIGNMENT_GRADE_LOW,
date = Date.from(Instant.parse("2023-09-15T09:02:00Z")),
@@ -124,6 +125,7 @@ class AlertsScreenTest {
),
AlertsItemUiState(
alertId = 2,
+ contextId = 20,
title = "Alert 2",
alertType = AlertType.ASSIGNMENT_GRADE_HIGH,
date = Date.from(Instant.parse("2023-09-16T09:02:00Z")),
@@ -134,6 +136,7 @@ class AlertsScreenTest {
),
AlertsItemUiState(
alertId = 3,
+ contextId = 30,
title = "Alert 3",
alertType = AlertType.COURSE_GRADE_LOW,
date = Date.from(Instant.parse("2023-09-17T09:02:00Z")),
@@ -144,6 +147,7 @@ class AlertsScreenTest {
),
AlertsItemUiState(
alertId = 4,
+ contextId = 40,
title = "Alert 4",
alertType = AlertType.COURSE_GRADE_HIGH,
date = Date.from(Instant.parse("2023-09-18T09:02:00Z")),
@@ -154,6 +158,7 @@ class AlertsScreenTest {
),
AlertsItemUiState(
alertId = 5,
+ contextId = 50,
title = "Alert 5",
alertType = AlertType.ASSIGNMENT_MISSING,
date = Date.from(Instant.parse("2023-09-19T09:02:00Z")),
@@ -164,6 +169,7 @@ class AlertsScreenTest {
),
AlertsItemUiState(
alertId = 6,
+ contextId = 60,
title = "Alert 6",
alertType = AlertType.COURSE_ANNOUNCEMENT,
date = Date.from(Instant.parse("2023-09-20T09:02:00Z")),
@@ -174,6 +180,7 @@ class AlertsScreenTest {
),
AlertsItemUiState(
alertId = 7,
+ contextId = 70,
title = "Alert 7",
alertType = AlertType.INSTITUTION_ANNOUNCEMENT,
date = Date.from(Instant.parse("2023-09-21T09:02:00Z")),
@@ -216,7 +223,7 @@ class AlertsScreenTest {
AlertType.COURSE_GRADE_HIGH -> "Course Grade Above $threshold"
AlertType.ASSIGNMENT_MISSING -> "Assignment missing"
AlertType.COURSE_ANNOUNCEMENT -> "Course Announcement"
- AlertType.INSTITUTION_ANNOUNCEMENT -> "Institution Announcement"
+ AlertType.INSTITUTION_ANNOUNCEMENT -> "Global Announcement"
else -> "Unknown"
}
}
diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/AlertsInteractionTest.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/AlertsInteractionTest.kt
index 7a2d099067..1f91a23e04 100644
--- a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/AlertsInteractionTest.kt
+++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/AlertsInteractionTest.kt
@@ -18,11 +18,18 @@ import androidx.compose.ui.platform.ComposeView
import androidx.test.espresso.matcher.ViewMatchers
import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckResultUtils
import com.google.android.apps.common.testing.accessibility.framework.checks.SpeakableTextPresentCheck
+import com.instructure.canvas.espresso.common.pages.AssignmentDetailsPage
import com.instructure.canvas.espresso.mockCanvas.MockCanvas
+import com.instructure.canvas.espresso.mockCanvas.addAssignmentsToGroups
+import com.instructure.canvas.espresso.mockCanvas.addCoursePermissions
+import com.instructure.canvas.espresso.mockCanvas.addDiscussionTopicToCourse
import com.instructure.canvas.espresso.mockCanvas.addObserverAlert
import com.instructure.canvas.espresso.mockCanvas.init
import com.instructure.canvasapi2.models.AlertType
import com.instructure.canvasapi2.models.AlertWorkflowState
+import com.instructure.canvasapi2.models.CanvasContext
+import com.instructure.canvasapi2.models.CanvasContextPermission
+import com.instructure.espresso.ModuleItemInteractions
import com.instructure.parentapp.utils.ParentComposeTest
import com.instructure.parentapp.utils.tokenLogin
import dagger.hilt.android.testing.HiltAndroidTest
@@ -32,6 +39,7 @@ import java.util.Date
@HiltAndroidTest
class AlertsInteractionTest : ParentComposeTest() {
+ private val assignmentDetailsPage = AssignmentDetailsPage(ModuleItemInteractions())
@Test
fun dismissAlert() {
@@ -112,13 +120,15 @@ class AlertsInteractionTest : ParentComposeTest() {
}
@Test
- fun openAlert() {
+ fun openAssignmentAlert() {
val data = initData()
goToAlerts(data)
val student = data.students.first()
val observer = data.parents.first()
val course = data.courses.values.first()
+ data.addAssignmentsToGroups(course)
+ val assignment = data.assignments.values.first { it.submission != null }
val alert = data.addObserverAlert(
observer,
@@ -127,6 +137,79 @@ class AlertsInteractionTest : ParentComposeTest() {
AlertType.ASSIGNMENT_MISSING,
AlertWorkflowState.UNREAD,
Date(),
+ "https://${data.domain}/courses/${course.id}/assignments/${assignment.id}",
+ false
+ )
+
+ alertsPage.refresh()
+
+ composeTestRule.waitForIdle()
+ alertsPage.assertAlertItemDisplayed(alert.title)
+ alertsPage.assertAlertUnread(alert.title)
+ alertsPage.clickOnAlert(alert.title)
+
+ assignmentDetailsPage.assertPageObjects()
+ assignmentDetailsPage.assertStatusSubmitted()
+ }
+
+ @Test
+ fun openCourseAlert() {
+ val data = MockCanvas.init(studentCount = 1, parentCount = 1, courseCount = 1, teacherCount = 1)
+ goToAlerts(data)
+
+ val student = data.students.first()
+ val observer = data.parents.first()
+ val teacher = data.teachers.first()
+ val course = data.courses.values.first()
+
+ data.addCoursePermissions(
+ course.id,
+ CanvasContextPermission(canCreateAnnouncement = true)
+ )
+
+ val announcement = data.addDiscussionTopicToCourse(
+ course = course,
+ user = teacher,
+ isAnnouncement = true
+ )
+
+ val alert = data.addObserverAlert(
+ observer,
+ student,
+ course,
+ AlertType.COURSE_ANNOUNCEMENT,
+ AlertWorkflowState.UNREAD,
+ Date(),
+ "https://${data.domain}/courses/${course.id}/discussion_topics/${announcement.id}",
+ false
+ )
+
+ alertsPage.refresh()
+
+ composeTestRule.waitForIdle()
+ alertsPage.assertAlertItemDisplayed(alert.title)
+ alertsPage.assertAlertUnread(alert.title)
+ alertsPage.clickOnAlert(alert.title)
+
+ announcementDetailsPage.assertCourseAnnouncementDetailsDisplayed(course, announcement)
+ }
+
+ @Test
+ fun openGlobalAlert() {
+ val data = MockCanvas.init(studentCount = 1, parentCount = 1, teacherCount = 1, courseCount = 1, accountNotificationCount = 1)
+ goToAlerts(data)
+
+ val student = data.students.first()
+ val observer = data.parents.first()
+ val announcement = data.accountNotifications.values.first()
+
+ val alert = data.addObserverAlert(
+ observer,
+ student,
+ CanvasContext.getGenericContext(CanvasContext.Type.USER, announcement.id),
+ AlertType.INSTITUTION_ANNOUNCEMENT,
+ AlertWorkflowState.UNREAD,
+ Date(),
null,
false
)
@@ -138,7 +221,7 @@ class AlertsInteractionTest : ParentComposeTest() {
alertsPage.assertAlertUnread(alert.title)
alertsPage.clickOnAlert(alert.title)
- //TODO check that we route to the correct screen when ready
+ announcementDetailsPage.assertGlobalAnnouncementDetailsDisplayed(announcement)
}
private fun initData(): MockCanvas {
diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/AssignmentDetailsInteractionTest.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/AssignmentDetailsInteractionTest.kt
new file mode 100644
index 0000000000..3ebdb61d0b
--- /dev/null
+++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/AssignmentDetailsInteractionTest.kt
@@ -0,0 +1,466 @@
+/*
+ * Copyright (C) 2024 - 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.parentapp.ui.interaction
+
+import androidx.compose.ui.platform.ComposeView
+import androidx.test.espresso.matcher.ViewMatchers
+import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckResultUtils
+import com.google.android.apps.common.testing.accessibility.framework.checks.SpeakableTextPresentCheck
+import com.instructure.canvas.espresso.checkToastText
+import com.instructure.canvas.espresso.common.pages.AssignmentDetailsPage
+import com.instructure.canvas.espresso.mockCanvas.MockCanvas
+import com.instructure.canvas.espresso.mockCanvas.addAssignment
+import com.instructure.canvas.espresso.mockCanvas.addAssignmentsToGroups
+import com.instructure.canvas.espresso.mockCanvas.addObserverAlert
+import com.instructure.canvas.espresso.mockCanvas.addSubmissionForAssignment
+import com.instructure.canvas.espresso.mockCanvas.init
+import com.instructure.canvasapi2.models.AlertType
+import com.instructure.canvasapi2.models.AlertWorkflowState
+import com.instructure.canvasapi2.models.Assignment
+import com.instructure.canvasapi2.models.CourseSettings
+import com.instructure.canvasapi2.utils.toApiString
+import com.instructure.espresso.ModuleItemInteractions
+import com.instructure.parentapp.R
+import com.instructure.parentapp.utils.ParentComposeTest
+import com.instructure.parentapp.utils.tokenLogin
+import dagger.hilt.android.testing.HiltAndroidTest
+import org.hamcrest.Matchers
+import org.junit.Test
+import java.util.Calendar
+import java.util.Date
+
+@HiltAndroidTest
+class AssignmentDetailsInteractionTest : ParentComposeTest() {
+ override fun displaysPageObjects() = Unit
+
+ private val assignmentDetailsPage = AssignmentDetailsPage(ModuleItemInteractions())
+
+ @Test
+ fun testSubmissionStatus_Missing() {
+ val data = setupData()
+ val successfulAssignment = data.assignments.values.first { it.submission == null && it.dueDate == null }
+ gotoAssignment(data, successfulAssignment)
+
+ assignmentDetailsPage.assertPageObjects()
+ assignmentDetailsPage.assertStatusNotSubmitted()
+ assignmentDetailsPage.assertDisplaysDate("No Due Date")
+ }
+
+ @Test
+ fun testSubmissionStatus_NotSubmitted() {
+ val data = setupData()
+ val successfulAssignment = data.assignments.values.first { it.submission == null && it.dueDate == null }
+ gotoAssignment(data, successfulAssignment)
+
+ assignmentDetailsPage.assertPageObjects()
+ assignmentDetailsPage.assertStatusNotSubmitted()
+ assignmentDetailsPage.assertDisplaysDate("No Due Date")
+ }
+
+ @Test
+ fun testDisplayToolbarTitles() {
+ val data = setupData()
+ val course = data.courses.values.first()
+ val assignment = data.assignments.values.first()
+ gotoAssignment(data, assignment)
+
+ assignmentDetailsPage.assertDisplayToolbarTitle()
+ assignmentDetailsPage.assertDisplayToolbarSubtitle(course.name)
+
+ }
+
+ @Test
+ fun testDisplayDueDate() {
+ val data = setupData()
+ val calendar = Calendar.getInstance().apply { set(2023, 0, 31, 23, 59, 0) }
+ val expectedDueDate = "January 31, 2023 11:59 PM"
+ val course = data.courses.values.first()
+ val assignmentWithNoDueDate = data.addAssignment(course.id, name = "Test Assignment", dueAt = calendar.time.toApiString())
+
+ gotoAssignment(data, assignmentWithNoDueDate)
+
+ assignmentDetailsPage.assertDisplaysDate(expectedDueDate)
+ }
+
+ @Test
+ fun testNavigating_viewAssignmentDetails() {
+ // Test clicking on the Assignment item in the Assignment List to load the Assignment Details Page
+ val data = setupData()
+ val assignmentList = data.assignments
+ val assignmentWithSubmissionEntry = assignmentList.filter {it.value.submission != null}
+ val assignmentWithSubmission = assignmentWithSubmissionEntry.entries.first().value
+
+ gotoAssignment(data, assignmentWithSubmission)
+
+ assignmentDetailsPage.assertPageObjects()
+ assignmentDetailsPage.assertAssignmentDetails(assignmentWithSubmission)
+ }
+
+ @Test
+ fun testLetterGradeAssignmentWithoutQuantitativeRestriction() {
+ val data = setupData()
+ val assignment = addAssignment(data, Assignment.GradingType.LETTER_GRADE, "B", 90.0, 100)
+ gotoAssignment(data, assignment)
+
+ assignmentDetailsPage.assertGradeDisplayed("B")
+ assignmentDetailsPage.assertOutOfTextDisplayed("Out of 100 pts")
+ assignmentDetailsPage.assertScoreDisplayed("90")
+ }
+
+ @Test
+ fun testGpaScaleAssignmentWithoutQuantitativeRestriction() {
+ val data = setupData()
+ val assignment = addAssignment(MockCanvas.data, Assignment.GradingType.GPA_SCALE, "3.7", 90.0, 100)
+ gotoAssignment(data, assignment)
+
+ assignmentDetailsPage.assertGradeDisplayed("3.7")
+ assignmentDetailsPage.assertOutOfTextDisplayed("Out of 100 pts")
+ assignmentDetailsPage.assertScoreDisplayed("90")
+ }
+
+ @Test
+ fun testPointsAssignmentWithoutQuantitativeRestriction() {
+ val data = setupData()
+ val assignment = addAssignment(MockCanvas.data, Assignment.GradingType.POINTS, "90", 90.0, 100)
+ gotoAssignment(data, assignment)
+
+ assignmentDetailsPage.assertGradeNotDisplayed()
+ assignmentDetailsPage.assertOutOfTextDisplayed("Out of 100 pts")
+ assignmentDetailsPage.assertScoreDisplayed("90")
+ }
+
+ @Test
+ fun testPointsAssignmentExcusedWithoutQuantitativeRestriction() {
+ val data = setupData()
+ val assignment = addAssignment(MockCanvas.data, Assignment.GradingType.POINTS, null, 90.0, 100, excused = true)
+ gotoAssignment(data, assignment)
+
+ assignmentDetailsPage.assertGradeDisplayed("EX")
+ assignmentDetailsPage.assertOutOfTextDisplayed("Out of 100 pts")
+ assignmentDetailsPage.assertScoreNotDisplayed()
+ }
+
+ @Test
+ fun testPercentageAssignmentWithoutQuantitativeRestriction() {
+ val data = setupData()
+ val assignment = addAssignment(MockCanvas.data, Assignment.GradingType.PERCENT, "90%", 90.0, 100)
+ gotoAssignment(data, assignment)
+
+ assignmentDetailsPage.assertGradeDisplayed("90%")
+ assignmentDetailsPage.assertOutOfTextDisplayed("Out of 100 pts")
+ assignmentDetailsPage.assertScoreDisplayed("90")
+ }
+
+ @Test
+ fun testPassFailAssignmentWithoutQuantitativeRestriction() {
+ val data = setupData()
+ val assignment = addAssignment(MockCanvas.data, Assignment.GradingType.PASS_FAIL, "complete", 0.0, 0)
+ gotoAssignment(data, assignment)
+
+ assignmentDetailsPage.assertGradeDisplayed("Complete")
+ assignmentDetailsPage.assertOutOfTextDisplayed("Out of 0 pts")
+ assignmentDetailsPage.assertScoreNotDisplayed()
+ }
+
+ @Test
+ fun testLetterGradeAssignmentWithQuantitativeRestriction() {
+ val data = setupData(restrictQuantitativeData = true)
+ val assignment = addAssignment(data, Assignment.GradingType.LETTER_GRADE, "B", 90.0, 100)
+ gotoAssignment(data, assignment)
+
+ assignmentDetailsPage.assertGradeDisplayed("B")
+ assignmentDetailsPage.assertOutOfTextNotDisplayed()
+ assignmentDetailsPage.assertScoreNotDisplayed()
+ }
+
+ @Test
+ fun testGpaScaleAssignmentWithQuantitativeRestriction() {
+ val data = setupData(restrictQuantitativeData = true)
+ val assignment = addAssignment(MockCanvas.data, Assignment.GradingType.GPA_SCALE, "3.7", 90.0, 100)
+ gotoAssignment(data, assignment)
+
+ assignmentDetailsPage.assertGradeDisplayed("3.7")
+ assignmentDetailsPage.assertOutOfTextNotDisplayed()
+ assignmentDetailsPage.assertScoreNotDisplayed()
+ }
+
+ @Test
+ fun testPointsAssignmentWithQuantitativeRestriction() {
+ val data = setupData(restrictQuantitativeData = true)
+ val assignment = addAssignment(MockCanvas.data, Assignment.GradingType.POINTS, "65", 65.0, 100)
+ gotoAssignment(data, assignment)
+
+ assignmentDetailsPage.assertGradeDisplayed("D")
+ assignmentDetailsPage.assertOutOfTextNotDisplayed()
+ assignmentDetailsPage.assertScoreNotDisplayed()
+ }
+
+ @Test
+ fun testPointsAssignmentExcusedWithQuantitativeRestriction() {
+ val data = setupData(restrictQuantitativeData = true)
+ val assignment = addAssignment(MockCanvas.data, Assignment.GradingType.POINTS, null, 90.0, 100, excused = true)
+ gotoAssignment(data, assignment)
+
+ assignmentDetailsPage.assertGradeDisplayed("EX")
+ assignmentDetailsPage.assertOutOfTextNotDisplayed()
+ assignmentDetailsPage.assertScoreNotDisplayed()
+ }
+
+ @Test
+ fun testPercentageAssignmentWithQuantitativeRestriction() {
+ val data = setupData(restrictQuantitativeData = true)
+ val assignment = addAssignment(MockCanvas.data, Assignment.GradingType.PERCENT, "70%", 70.0, 100)
+ gotoAssignment(data, assignment)
+
+ assignmentDetailsPage.assertGradeDisplayed("C")
+ assignmentDetailsPage.assertOutOfTextNotDisplayed()
+ assignmentDetailsPage.assertScoreNotDisplayed()
+ }
+
+ @Test
+ fun testPassFailAssignmentWithQuantitativeRestriction() {
+ val data = setupData(restrictQuantitativeData = true)
+ val assignment = addAssignment(MockCanvas.data, Assignment.GradingType.PASS_FAIL, "complete", 0.0, 0)
+ gotoAssignment(data, assignment)
+
+ assignmentDetailsPage.assertGradeDisplayed("Complete")
+ assignmentDetailsPage.assertOutOfTextNotDisplayed()
+ assignmentDetailsPage.assertScoreNotDisplayed()
+ }
+
+ @Test
+ fun testReminderSectionIsNotVisibleWhenThereIsNoFutureDueDate() {
+ val data = setupData()
+ val course = data.courses.values.first()
+ val assignment = data.addAssignment(course.id, name = "Test Assignment", dueAt = Calendar.getInstance().apply {
+ add(Calendar.DAY_OF_MONTH, -1)
+ }.time.toApiString())
+ gotoAssignment(data, assignment)
+
+ assignmentDetailsPage.assertReminderSectionNotDisplayed()
+ }
+
+ @Test
+ fun testReminderSectionIsVisibleWhenThereIsFutureDueDate() {
+ val data = setupData()
+ val course = data.courses.values.first()
+ val assignment = data.addAssignment(course.id, name = "Test Assignment", dueAt = Calendar.getInstance().apply {
+ add(Calendar.DAY_OF_MONTH, 1)
+ }.time.toApiString())
+ gotoAssignment(data, assignment)
+
+ assignmentDetailsPage.assertReminderSectionDisplayed()
+ }
+
+ @Test
+ fun testAddReminder() {
+ val data = setupData()
+ val course = data.courses.values.first()
+ val assignment = data.addAssignment(course.id, name = "Test Assignment", dueAt = Calendar.getInstance().apply {
+ add(Calendar.DAY_OF_MONTH, 1)
+ }.time.toApiString())
+ gotoAssignment(data, assignment)
+
+ assignmentDetailsPage.clickAddReminder()
+ assignmentDetailsPage.selectTimeOption("1 Hour Before")
+
+ assignmentDetailsPage.assertReminderDisplayedWithText("1 Hour Before")
+ }
+
+ @Test
+ fun testRemoveReminder() {
+ val data = setupData()
+ val course = data.courses.values.first()
+ val assignment = data.addAssignment(course.id, name = "Test Assignment", dueAt = Calendar.getInstance().apply {
+ add(Calendar.DAY_OF_MONTH, 1)
+ }.time.toApiString())
+ gotoAssignment(data, assignment)
+
+ assignmentDetailsPage.clickAddReminder()
+ assignmentDetailsPage.selectTimeOption("1 Hour Before")
+
+ assignmentDetailsPage.assertReminderDisplayedWithText("1 Hour Before")
+
+ assignmentDetailsPage.removeReminderWithText("1 Hour Before")
+
+ assignmentDetailsPage.assertReminderNotDisplayedWithText("1 Hour Before")
+ }
+
+ @Test
+ fun testAddCustomReminder() {
+ val data = setupData()
+ val course = data.courses.values.first()
+ val assignment = data.addAssignment(course.id, name = "Test Assignment", dueAt = Calendar.getInstance().apply {
+ add(Calendar.DAY_OF_MONTH, 1)
+ }.time.toApiString())
+ gotoAssignment(data, assignment)
+
+ assignmentDetailsPage.clickAddReminder()
+ assignmentDetailsPage.clickCustom()
+ assignmentDetailsPage.assertDoneButtonIsDisabled()
+ assignmentDetailsPage.fillQuantity("15")
+ assignmentDetailsPage.assertDoneButtonIsDisabled()
+ assignmentDetailsPage.clickHoursBefore()
+ assignmentDetailsPage.clickDone()
+
+ assignmentDetailsPage.assertReminderDisplayedWithText("15 Hours Before")
+ }
+
+ @Test
+ fun testAddReminderInPastShowsError() {
+ val data = setupData()
+ val course = data.courses.values.first()
+ val assignment = data.addAssignment(course.id, name = "Test Assignment", dueAt = Calendar.getInstance().apply {
+ add(Calendar.MINUTE, 30)
+ }.time.toApiString())
+ gotoAssignment(data, assignment)
+
+ assignmentDetailsPage.clickAddReminder()
+ assignmentDetailsPage.selectTimeOption("1 Hour Before")
+
+ checkToastText(R.string.reminderInPast, activityRule.activity)
+ }
+
+ @Test
+ fun testAddReminderForTheSameTimeShowsError() {
+ val data = setupData()
+ val course = data.courses.values.first()
+ val assignment = data.addAssignment(course.id, name = "Test Assignment", dueAt = Calendar.getInstance().apply {
+ add(Calendar.DAY_OF_MONTH, 1)
+ }.time.toApiString())
+ gotoAssignment(data, assignment)
+
+ assignmentDetailsPage.clickAddReminder()
+ assignmentDetailsPage.selectTimeOption("1 Hour Before")
+ assignmentDetailsPage.clickAddReminder()
+ assignmentDetailsPage.selectTimeOption("1 Hour Before")
+
+ checkToastText(R.string.reminderAlreadySet, activityRule.activity)
+ }
+
+ @Test
+ fun testAddReminderForTheSameTimeWithDifferentMeasureOfTimeShowsError() {
+ val data = setupData()
+ val course = data.courses.values.first()
+ val assignment = data.addAssignment(course.id, name = "Test Assignment", dueAt = Calendar.getInstance().apply {
+ add(Calendar.DAY_OF_MONTH, 10)
+ }.time.toApiString())
+ gotoAssignment(data, assignment)
+
+ assignmentDetailsPage.clickAddReminder()
+ assignmentDetailsPage.selectTimeOption("1 Week Before")
+ assignmentDetailsPage.clickAddReminder()
+
+ assignmentDetailsPage.clickCustom()
+ assignmentDetailsPage.fillQuantity("7")
+ assignmentDetailsPage.clickDaysBefore()
+ assignmentDetailsPage.clickDone()
+
+ checkToastText(R.string.reminderAlreadySet, activityRule.activity)
+ }
+
+ private fun setupData(restrictQuantitativeData: Boolean = false): MockCanvas {
+ val data = MockCanvas.init(
+ parentCount = 1,
+ studentCount = 1,
+ courseCount = 1
+ )
+
+ val course = data.courses.values.first()
+
+ val gradingScheme = listOf(
+ listOf("A", 0.9),
+ listOf("B", 0.8),
+ listOf("C", 0.7),
+ listOf("D", 0.6),
+ listOf("F", 0.0)
+ )
+
+ data.courseSettings[course.id] = CourseSettings(restrictQuantitativeData = restrictQuantitativeData)
+
+ val newCourse = course
+ .copy(settings = CourseSettings(restrictQuantitativeData = restrictQuantitativeData),
+ gradingSchemeRaw = gradingScheme)
+ data.courses[course.id] = newCourse
+
+ data.addAssignmentsToGroups(newCourse)
+
+ return data
+ }
+
+ private fun gotoAssignment(data: MockCanvas, assignment: Assignment) {
+ val student = data.students.first()
+ val observer = data.parents.first()
+ val course = data.courses.values.first()
+
+ tokenLogin(data.domain, data.tokenFor(observer)!!, observer)
+ composeTestRule.waitForIdle()
+
+ dashboardPage.clickAlerts()
+ composeTestRule.waitForIdle()
+
+ val alert = data.addObserverAlert(
+ observer,
+ student,
+ course,
+ AlertType.ASSIGNMENT_MISSING,
+ AlertWorkflowState.UNREAD,
+ Date(),
+ "https://${data.domain}/courses/${course.id}/assignments/${assignment.id}",
+ false
+ )
+
+ alertsPage.refresh()
+ composeTestRule.waitForIdle()
+
+ alertsPage.clickOnAlert(alert.title)
+ composeTestRule.waitForIdle()
+ }
+
+ private fun addAssignment(data: MockCanvas, gradingType: Assignment.GradingType, grade: String?, score: Double?, maxScore: Int, excused: Boolean = false): Assignment {
+ val course = data.courses.values.first()
+ val student = data.students.first()
+
+ val assignment = data.addAssignment(
+ courseId = course.id,
+ submissionTypeList = listOf(Assignment.SubmissionType.ONLINE_TEXT_ENTRY),
+ gradingType = Assignment.gradingTypeToAPIString(gradingType) ?: "",
+ pointsPossible = maxScore,
+ )
+
+ data.addSubmissionForAssignment(assignment.id, student.id, Assignment.SubmissionType.ONLINE_TEXT_ENTRY.apiString, grade = grade, score = score, excused = excused)
+
+ return assignment
+ }
+
+ override fun enableAndConfigureAccessibilityChecks() {
+ extraAccessibilitySupressions = Matchers.allOf(
+ AccessibilityCheckResultUtils.matchesCheck(
+ SpeakableTextPresentCheck::class.java
+ ),
+ AccessibilityCheckResultUtils.matchesViews(
+ ViewMatchers.withParent(
+ ViewMatchers.withClassName(
+ Matchers.equalTo(ComposeView::class.java.name)
+ )
+ )
+ )
+ )
+
+ super.enableAndConfigureAccessibilityChecks()
+ }
+
+}
\ No newline at end of file
diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/ParentInboxComposeInteractionTest.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/ParentInboxComposeInteractionTest.kt
index 2f318e190e..8311fcdfaa 100644
--- a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/ParentInboxComposeInteractionTest.kt
+++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/ParentInboxComposeInteractionTest.kt
@@ -6,6 +6,7 @@ import com.google.android.apps.common.testing.accessibility.framework.Accessibil
import com.google.android.apps.common.testing.accessibility.framework.checks.SpeakableTextPresentCheck
import com.instructure.canvas.espresso.common.interaction.InboxComposeInteractionTest
import com.instructure.canvas.espresso.common.pages.InboxPage
+import com.instructure.canvas.espresso.common.pages.compose.InboxComposePage
import com.instructure.canvas.espresso.mockCanvas.MockCanvas
import com.instructure.canvas.espresso.mockCanvas.addCoursePermissions
import com.instructure.canvas.espresso.mockCanvas.addRecipientsToCourse
@@ -13,14 +14,18 @@ import com.instructure.canvas.espresso.mockCanvas.init
import com.instructure.canvasapi2.models.CanvasContextPermission
import com.instructure.canvasapi2.models.Conversation
import com.instructure.canvasapi2.models.Course
+import com.instructure.canvasapi2.models.Recipient
import com.instructure.canvasapi2.models.User
+import com.instructure.canvasapi2.type.EnrollmentType
import com.instructure.parentapp.BuildConfig
import com.instructure.parentapp.features.login.LoginActivity
import com.instructure.parentapp.ui.pages.DashboardPage
+import com.instructure.parentapp.ui.pages.ParentInboxCoursePickerPage
import com.instructure.parentapp.utils.ParentActivityTestRule
import com.instructure.parentapp.utils.tokenLogin
import dagger.hilt.android.testing.HiltAndroidTest
import org.hamcrest.Matchers
+import org.junit.Test
@HiltAndroidTest
class ParentInboxComposeInteractionTest: InboxComposeInteractionTest() {
@@ -30,6 +35,38 @@ class ParentInboxComposeInteractionTest: InboxComposeInteractionTest() {
private val dashboardPage = DashboardPage()
private val inboxPage = InboxPage()
+ private val inboxComposePage = InboxComposePage(composeTestRule)
+ private val inboxCoursePickerPage = ParentInboxCoursePickerPage(composeTestRule)
+
+ @Test
+ fun testParentComposeDefaultValues() {
+ val data = initData(canSendToAll = true)
+ data.recipientGroups[getFirstCourse().id] = listOf(
+ Recipient(
+ stringId = getTeachers().first().id.toString(),
+ name = getTeachers().first().name,
+ commonCourses = hashMapOf(
+ getFirstCourse().id.toString() to arrayOf(EnrollmentType.TEACHERENROLLMENT.rawValue())
+ )
+ )
+ )
+ val parent = data.parents.first()
+ val token = data.tokenFor(parent)!!
+ tokenLogin(data.domain, token, parent)
+
+ dashboardPage.openNavigationDrawer()
+ dashboardPage.clickInbox()
+
+ inboxPage.pressNewMessageButton()
+
+ inboxCoursePickerPage.selectCourseWithUser(getFirstCourse().name, observedUserName = "for ${getObservedStudent().shortName ?: getObservedStudent().name}")
+
+ composeTestRule.waitUntil { !inboxComposePage.isRecipientsLoading() }
+
+ inboxComposePage.assertContextSelected(getFirstCourse().name)
+ inboxComposePage.assertSubjectText(getFirstCourse().name)
+ inboxComposePage.assertRecipientSelected(getTeachers().first().name)
+ }
override fun goToInboxCompose(data: MockCanvas) {
val parent = data.parents.first()
@@ -40,6 +77,11 @@ class ParentInboxComposeInteractionTest: InboxComposeInteractionTest() {
dashboardPage.clickInbox()
inboxPage.pressNewMessageButton()
+
+ inboxCoursePickerPage.selectCourseWithUser(getFirstCourse().name, observedUserName = "for ${getObservedStudent().shortName ?: getObservedStudent().name}")
+
+ composeTestRule.waitUntil { !inboxComposePage.isRecipientsLoading() }
+ inboxComposePage.removeAllRecipients()
}
override fun initData(canSendToAll: Boolean, sendMessages: Boolean): MockCanvas {
@@ -81,6 +123,8 @@ class ParentInboxComposeInteractionTest: InboxComposeInteractionTest() {
super.enableAndConfigureAccessibilityChecks()
}
+ private fun getObservedStudent(): User = MockCanvas.data.students[0]
+
override fun getLoggedInUser(): User = MockCanvas.data.parents[0]
override fun getTeachers(): List { return MockCanvas.data.teachers }
@@ -88,4 +132,6 @@ class ParentInboxComposeInteractionTest: InboxComposeInteractionTest() {
override fun getFirstCourse(): Course { return MockCanvas.data.courses.values.first() }
override fun getSentConversation(): Conversation? { return MockCanvas.data.sentConversation }
+
+ override fun selectContext() = Unit
}
\ No newline at end of file
diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/AlertSettingsPage.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/AlertSettingsPage.kt
index e4c334ffe3..b573deae61 100644
--- a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/AlertSettingsPage.kt
+++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/AlertSettingsPage.kt
@@ -101,7 +101,7 @@ class AlertSettingsPage(private val composeTestRule: ComposeTestRule) : BasePage
AlertType.ASSIGNMENT_MISSING -> "Assignment missing"
AlertType.ASSIGNMENT_GRADE_HIGH -> "Assignment grade above"
AlertType.ASSIGNMENT_GRADE_LOW -> "Assignment grade below"
- AlertType.INSTITUTION_ANNOUNCEMENT -> "Institution Announcements"
+ AlertType.INSTITUTION_ANNOUNCEMENT -> "Global Announcements"
}
}
}
\ No newline at end of file
diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/AnnouncementDetailsPage.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/AnnouncementDetailsPage.kt
new file mode 100644
index 0000000000..e148fe2763
--- /dev/null
+++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/AnnouncementDetailsPage.kt
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2024 - 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.parentapp.ui.pages
+
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.junit4.ComposeTestRule
+import androidx.compose.ui.test.onNodeWithText
+import androidx.test.platform.app.InstrumentationRegistry
+import com.instructure.canvasapi2.models.AccountNotification
+import com.instructure.canvasapi2.models.Course
+import com.instructure.canvasapi2.models.DiscussionTopicHeader
+import com.instructure.canvasapi2.utils.DateHelper
+import com.instructure.pandares.R
+
+class AnnouncementDetailsPage(private val composeTestRule: ComposeTestRule) {
+
+ fun assertCourseAnnouncementDetailsDisplayed(
+ course: Course,
+ discussionTopicHeader: DiscussionTopicHeader
+ ) {
+ composeTestRule.onNodeWithText(course.name).assertIsDisplayed()
+ composeTestRule.onNodeWithText(discussionTopicHeader.title.orEmpty()).assertIsDisplayed()
+ val dateString = DateHelper.getDateAtTimeString(
+ InstrumentationRegistry.getInstrumentation().targetContext,
+ R.string.alertDateTime,
+ discussionTopicHeader.postedDate
+ )
+ dateString?.let {
+ composeTestRule.onNodeWithText(it).assertIsDisplayed()
+ }
+ }
+
+ fun assertGlobalAnnouncementDetailsDisplayed(accountNotification: AccountNotification) {
+ composeTestRule.onNodeWithText("Global Announcement").assertIsDisplayed()
+ composeTestRule.onNodeWithText(accountNotification.subject).assertIsDisplayed()
+ val dateString = DateHelper.getDateAtTimeString(
+ InstrumentationRegistry.getInstrumentation().targetContext,
+ R.string.alertDateTime,
+ accountNotification.startDate
+ )
+ dateString?.let {
+ composeTestRule.onNodeWithText(it).assertIsDisplayed()
+ }
+ }
+}
diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/ParentInboxCoursePickerPage.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/ParentInboxCoursePickerPage.kt
new file mode 100644
index 0000000000..23ae71588f
--- /dev/null
+++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/ParentInboxCoursePickerPage.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2024 - 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.parentapp.ui.pages
+
+import androidx.compose.ui.test.hasAnyChild
+import androidx.compose.ui.test.hasText
+import androidx.compose.ui.test.junit4.ComposeTestRule
+import androidx.compose.ui.test.performClick
+
+class ParentInboxCoursePickerPage(private val composeTestRule: ComposeTestRule) {
+ fun selectCourseWithUser(courseName: String, observedUserName: String) {
+ composeTestRule.onNode(hasAnyChild(hasText(courseName)).and(hasAnyChild(hasText(observedUserName)))).performClick()
+ }
+}
\ No newline at end of file
diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/utils/ParentComposeTest.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/utils/ParentComposeTest.kt
index 873e9bf5b7..3f75c06699 100644
--- a/apps/parent/src/androidTest/java/com/instructure/parentapp/utils/ParentComposeTest.kt
+++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/utils/ParentComposeTest.kt
@@ -22,6 +22,7 @@ import com.instructure.parentapp.features.login.LoginActivity
import com.instructure.parentapp.ui.pages.AddStudentPage
import com.instructure.parentapp.ui.pages.AlertSettingsPage
import com.instructure.parentapp.ui.pages.AlertsPage
+import com.instructure.parentapp.ui.pages.AnnouncementDetailsPage
import com.instructure.parentapp.ui.pages.CourseDetailsPage
import com.instructure.parentapp.ui.pages.CoursesPage
import com.instructure.parentapp.ui.pages.ManageStudentsPage
@@ -45,6 +46,7 @@ abstract class ParentComposeTest : ParentTest() {
protected val coursesPage = CoursesPage(composeTestRule)
protected val notAParentPage = NotAParentPage(composeTestRule)
protected val courseDetailsPage = CourseDetailsPage(composeTestRule)
+ protected val announcementDetailsPage = AnnouncementDetailsPage(composeTestRule)
override fun displaysPageObjects() = Unit
}
diff --git a/apps/parent/src/main/AndroidManifest.xml b/apps/parent/src/main/AndroidManifest.xml
index f890d3d570..fd8079950a 100644
--- a/apps/parent/src/main/AndroidManifest.xml
+++ b/apps/parent/src/main/AndroidManifest.xml
@@ -27,6 +27,8 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/apps/parent/src/main/java/com/instructure/parentapp/di/ApplicationModule.kt b/apps/parent/src/main/java/com/instructure/parentapp/di/ApplicationModule.kt
index 7d0f341cf6..9d71d341d3 100644
--- a/apps/parent/src/main/java/com/instructure/parentapp/di/ApplicationModule.kt
+++ b/apps/parent/src/main/java/com/instructure/parentapp/di/ApplicationModule.kt
@@ -17,7 +17,6 @@
package com.instructure.parentapp.di
-import com.instructure.canvasapi2.utils.Analytics
import com.instructure.canvasapi2.utils.ApiPrefs
import com.instructure.loginapi.login.util.PreviousUsersUtils
import com.instructure.loginapi.login.util.QRLogin
@@ -45,11 +44,6 @@ class ApplicationModule {
return QRLogin
}
- @Provides
- fun provideAnalytics(): Analytics {
- return Analytics
- }
-
@Provides
fun providePreviousUsersUtils(): PreviousUsersUtils {
return PreviousUsersUtils
diff --git a/apps/parent/src/main/java/com/instructure/parentapp/di/feature/AssignmentDetailsModule.kt b/apps/parent/src/main/java/com/instructure/parentapp/di/feature/AssignmentDetailsModule.kt
new file mode 100644
index 0000000000..85eac76a94
--- /dev/null
+++ b/apps/parent/src/main/java/com/instructure/parentapp/di/feature/AssignmentDetailsModule.kt
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2024 - 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.parentapp.di.feature
+
+import com.instructure.canvasapi2.apis.AssignmentAPI
+import com.instructure.canvasapi2.apis.CourseAPI
+import com.instructure.canvasapi2.apis.QuizAPI
+import com.instructure.canvasapi2.apis.SubmissionAPI
+import com.instructure.canvasapi2.utils.ApiPrefs
+import com.instructure.pandautils.features.assignments.details.AssignmentDetailsBehaviour
+import com.instructure.pandautils.features.assignments.details.AssignmentDetailsColorProvider
+import com.instructure.pandautils.features.assignments.details.AssignmentDetailsRepository
+import com.instructure.pandautils.features.assignments.details.AssignmentDetailsRouter
+import com.instructure.pandautils.features.assignments.details.AssignmentDetailsSubmissionHandler
+import com.instructure.pandautils.receivers.alarm.AlarmReceiverNotificationHandler
+import com.instructure.pandautils.room.appdatabase.daos.ReminderDao
+import com.instructure.pandautils.utils.ColorKeeper
+import com.instructure.parentapp.features.assignment.details.ParentAssignmentDetailsBehaviour
+import com.instructure.parentapp.features.assignment.details.ParentAssignmentDetailsColorProvider
+import com.instructure.parentapp.features.assignment.details.ParentAssignmentDetailsRepository
+import com.instructure.parentapp.features.assignment.details.ParentAssignmentDetailsRouter
+import com.instructure.parentapp.features.assignment.details.ParentAssignmentDetailsSubmissionHandler
+import com.instructure.parentapp.features.assignment.details.receiver.ParentAlarmReceiverNotificationHandler
+import com.instructure.parentapp.util.ParentPrefs
+import com.instructure.parentapp.util.navigation.Navigation
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.android.components.FragmentComponent
+import dagger.hilt.android.components.ViewModelComponent
+import dagger.hilt.components.SingletonComponent
+
+@Module
+@InstallIn(FragmentComponent::class)
+class AssignmentDetailsFragmentModule {
+ @Provides
+ fun provideAssignmentDetailsRouter(navigation: Navigation): AssignmentDetailsRouter {
+ return ParentAssignmentDetailsRouter(navigation)
+ }
+
+ @Provides
+ fun provideAssignmentDetailsBehaviour(parentPrefs: ParentPrefs, apiPrefs: ApiPrefs): AssignmentDetailsBehaviour {
+ return ParentAssignmentDetailsBehaviour(parentPrefs, apiPrefs)
+ }
+}
+
+@Module
+@InstallIn(ViewModelComponent::class)
+class AssignmentDetailsModule {
+ @Provides
+ fun provideAssignmentDetailsRepository(
+ coursesApi: CourseAPI.CoursesInterface,
+ assignmentApi: AssignmentAPI.AssignmentInterface,
+ quizApi: QuizAPI.QuizInterface,
+ submissionApi: SubmissionAPI.SubmissionInterface,
+ reminderDao: ReminderDao
+ ): AssignmentDetailsRepository {
+ return ParentAssignmentDetailsRepository(coursesApi, assignmentApi, quizApi, submissionApi, reminderDao)
+ }
+
+ @Provides
+ fun provideAssignmentDetailsSubmissionHandler(): AssignmentDetailsSubmissionHandler {
+ return ParentAssignmentDetailsSubmissionHandler()
+ }
+
+ @Provides
+ fun provideAssignmentDetailsColorProvider(parentPrefs: ParentPrefs, colorKeeper: ColorKeeper): AssignmentDetailsColorProvider {
+ return ParentAssignmentDetailsColorProvider(parentPrefs, colorKeeper)
+ }
+}
+
+@Module
+@InstallIn(SingletonComponent::class)
+class AssignmentDetailsSingletonModule {
+ @Provides
+ fun provideAssignmentDetailsNotificationHandler(): AlarmReceiverNotificationHandler {
+ return ParentAlarmReceiverNotificationHandler()
+ }
+}
\ No newline at end of file
diff --git a/apps/parent/src/main/java/com/instructure/parentapp/di/feature/DashboardModule.kt b/apps/parent/src/main/java/com/instructure/parentapp/di/feature/DashboardModule.kt
index 4c575ef2a6..0f6d7e3a73 100644
--- a/apps/parent/src/main/java/com/instructure/parentapp/di/feature/DashboardModule.kt
+++ b/apps/parent/src/main/java/com/instructure/parentapp/di/feature/DashboardModule.kt
@@ -18,6 +18,7 @@
package com.instructure.parentapp.di.feature
import com.instructure.canvasapi2.apis.EnrollmentAPI
+import com.instructure.canvasapi2.apis.LaunchDefinitionsAPI
import com.instructure.canvasapi2.apis.UnreadCountAPI
import com.instructure.parentapp.features.dashboard.AlertCountUpdater
import com.instructure.parentapp.features.dashboard.AlertCountUpdaterImpl
@@ -40,9 +41,10 @@ class DashboardModule {
@Provides
fun provideDashboardRepository(
enrollmentApi: EnrollmentAPI.EnrollmentInterface,
- unreadCountsApi: UnreadCountAPI.UnreadCountsInterface
+ unreadCountsApi: UnreadCountAPI.UnreadCountsInterface,
+ launchDefinitionsApi: LaunchDefinitionsAPI.LaunchDefinitionsInterface
): DashboardRepository {
- return DashboardRepository(enrollmentApi, unreadCountsApi)
+ return DashboardRepository(enrollmentApi, unreadCountsApi, launchDefinitionsApi)
}
}
diff --git a/apps/parent/src/main/java/com/instructure/parentapp/di/feature/InboxModule.kt b/apps/parent/src/main/java/com/instructure/parentapp/di/feature/InboxModule.kt
index abdbd4b128..9f75b73e2b 100644
--- a/apps/parent/src/main/java/com/instructure/parentapp/di/feature/InboxModule.kt
+++ b/apps/parent/src/main/java/com/instructure/parentapp/di/feature/InboxModule.kt
@@ -19,6 +19,7 @@ package com.instructure.parentapp.di.feature
import androidx.fragment.app.FragmentActivity
import com.instructure.canvasapi2.apis.CourseAPI
+import com.instructure.canvasapi2.apis.EnrollmentAPI
import com.instructure.canvasapi2.apis.GroupAPI
import com.instructure.canvasapi2.apis.InboxApi
import com.instructure.canvasapi2.apis.ProgressAPI
@@ -27,6 +28,7 @@ import com.instructure.pandautils.features.inbox.compose.InboxComposeRepository
import com.instructure.pandautils.features.inbox.list.InboxRepository
import com.instructure.pandautils.features.inbox.list.InboxRouter
import com.instructure.parentapp.features.inbox.compose.ParentInboxComposeRepository
+import com.instructure.parentapp.features.inbox.coursepicker.ParentInboxCoursePickerRepository
import com.instructure.parentapp.features.inbox.list.ParentInboxRepository
import com.instructure.parentapp.features.inbox.list.ParentInboxRouter
import com.instructure.parentapp.util.navigation.Navigation
@@ -68,4 +70,12 @@ class InboxModule {
): InboxComposeRepository {
return ParentInboxComposeRepository(courseAPI, recipientAPI, inboxAPI)
}
+
+ @Provides
+ fun provideInboxCoursePickerRepository(
+ courseAPI: CourseAPI.CoursesInterface,
+ enrollmentAPI: EnrollmentAPI.EnrollmentInterface,
+ ): ParentInboxCoursePickerRepository {
+ return ParentInboxCoursePickerRepository(courseAPI, enrollmentAPI)
+ }
}
diff --git a/apps/parent/src/main/java/com/instructure/parentapp/di/feature/LtiLaunchModule.kt b/apps/parent/src/main/java/com/instructure/parentapp/di/feature/LtiLaunchModule.kt
new file mode 100644
index 0000000000..d250896b30
--- /dev/null
+++ b/apps/parent/src/main/java/com/instructure/parentapp/di/feature/LtiLaunchModule.kt
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2024 - 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.parentapp.di.feature
+
+import com.instructure.canvasapi2.apis.LaunchDefinitionsAPI
+import com.instructure.parentapp.features.lti.LtiLaunchRepository
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.android.components.ViewModelComponent
+
+@Module
+@InstallIn(ViewModelComponent::class)
+class LtiLaunchModule {
+
+ @Provides
+ fun provideLtiLaunchRepository(launchDefinitionsInterface: LaunchDefinitionsAPI.LaunchDefinitionsInterface): LtiLaunchRepository {
+ return LtiLaunchRepository(launchDefinitionsInterface)
+ }
+}
\ No newline at end of file
diff --git a/apps/parent/src/main/java/com/instructure/parentapp/features/addstudent/AddStudentViewModel.kt b/apps/parent/src/main/java/com/instructure/parentapp/features/addstudent/AddStudentViewModel.kt
index 2e01d1fbb4..59acb7b5e2 100644
--- a/apps/parent/src/main/java/com/instructure/parentapp/features/addstudent/AddStudentViewModel.kt
+++ b/apps/parent/src/main/java/com/instructure/parentapp/features/addstudent/AddStudentViewModel.kt
@@ -19,7 +19,8 @@ package com.instructure.parentapp.features.addstudent
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.google.firebase.crashlytics.FirebaseCrashlytics
-import com.instructure.pandautils.utils.color
+import com.instructure.pandautils.utils.ColorKeeper
+import com.instructure.pandautils.utils.studentColor
import com.instructure.parentapp.features.dashboard.SelectedStudentHolder
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableSharedFlow
@@ -40,7 +41,7 @@ class AddStudentViewModel @Inject constructor(
private val _uiState =
MutableStateFlow(
AddStudentUiState(
- color = selectedStudentHolder.selectedStudentState.value.color,
+ color = selectedStudentHolder.selectedStudentState.value.studentColor,
actionHandler = this::handleAction
)
)
@@ -53,7 +54,7 @@ class AddStudentViewModel @Inject constructor(
viewModelScope.launch {
selectedStudentHolder.selectedStudentChangedFlow.collectLatest { user ->
_uiState.value = _uiState.value.copy(
- color = user.color
+ color = user.studentColor
)
}
}
diff --git a/apps/parent/src/main/java/com/instructure/parentapp/features/alerts/details/AnnouncementDetailsFragment.kt b/apps/parent/src/main/java/com/instructure/parentapp/features/alerts/details/AnnouncementDetailsFragment.kt
new file mode 100644
index 0000000000..a4aae98ae4
--- /dev/null
+++ b/apps/parent/src/main/java/com/instructure/parentapp/features/alerts/details/AnnouncementDetailsFragment.kt
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2024 - 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.parentapp.features.alerts.details
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.platform.ComposeView
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.viewModels
+import androidx.navigation.fragment.findNavController
+import com.instructure.pandautils.utils.ViewStyler
+import dagger.hilt.android.AndroidEntryPoint
+
+@AndroidEntryPoint
+class AnnouncementDetailsFragment : Fragment() {
+
+ private val viewModel: AnnouncementDetailsViewModel by viewModels()
+
+ override fun onCreateView(
+ inflater: LayoutInflater, container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View {
+ return ComposeView(requireActivity()).apply {
+ setContent {
+ val uiState by viewModel.uiState.collectAsState()
+ ViewStyler.setStatusBarDark(requireActivity(), uiState.studentColor)
+ AnnouncementDetailsScreen(
+ uiState,
+ viewModel::handleAction,
+ navigationActionClick = {
+ findNavController().popBackStack()
+ }
+ )
+ }
+ }
+ }
+
+ companion object {
+ const val COURSE_ID = "course-id"
+ const val ANNOUNCEMENT_ID = "announcement-id"
+ }
+}
diff --git a/apps/parent/src/main/java/com/instructure/parentapp/features/alerts/details/AnnouncementDetailsRepository.kt b/apps/parent/src/main/java/com/instructure/parentapp/features/alerts/details/AnnouncementDetailsRepository.kt
new file mode 100644
index 0000000000..2c9fbdcf33
--- /dev/null
+++ b/apps/parent/src/main/java/com/instructure/parentapp/features/alerts/details/AnnouncementDetailsRepository.kt
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2024 - 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.parentapp.features.alerts.details
+
+import com.instructure.canvasapi2.apis.AccountNotificationAPI
+import com.instructure.canvasapi2.apis.AnnouncementAPI
+import com.instructure.canvasapi2.apis.CourseAPI
+import com.instructure.canvasapi2.builders.RestParams
+import com.instructure.canvasapi2.models.AccountNotification
+import com.instructure.canvasapi2.models.Course
+import com.instructure.canvasapi2.models.DiscussionTopicHeader
+import javax.inject.Inject
+
+class AnnouncementDetailsRepository @Inject constructor(
+ private val announcementApi: AnnouncementAPI.AnnouncementInterface,
+ private val courseApi: CourseAPI.CoursesInterface,
+ private val accountNotificationApi: AccountNotificationAPI.AccountNotificationInterface
+) {
+ suspend fun getCourse(
+ courseId: Long,
+ forceNetwork: Boolean
+ ): Course? {
+ val restParams = RestParams(isForceReadFromNetwork = forceNetwork)
+ return courseApi.getCourse(courseId, restParams).dataOrNull
+ }
+
+ suspend fun getCourseAnnouncement(
+ courseId: Long,
+ announcementId: Long,
+ forceNetwork: Boolean
+ ): DiscussionTopicHeader {
+ val restParams = RestParams(isForceReadFromNetwork = forceNetwork)
+ return announcementApi.getCourseAnnouncement(
+ courseId,
+ announcementId,
+ restParams
+ ).dataOrThrow
+ }
+
+ suspend fun getGlobalAnnouncement(
+ announcementId: Long,
+ forceNetwork: Boolean
+ ): AccountNotification {
+ val restParams = RestParams(isForceReadFromNetwork = forceNetwork)
+ return accountNotificationApi.getAccountNotification(announcementId, restParams).dataOrThrow
+ }
+}
diff --git a/apps/parent/src/main/java/com/instructure/parentapp/features/alerts/details/AnnouncementDetailsScreen.kt b/apps/parent/src/main/java/com/instructure/parentapp/features/alerts/details/AnnouncementDetailsScreen.kt
new file mode 100644
index 0000000000..65516031fb
--- /dev/null
+++ b/apps/parent/src/main/java/com/instructure/parentapp/features/alerts/details/AnnouncementDetailsScreen.kt
@@ -0,0 +1,289 @@
+/*
+ * Copyright (C) 2024 - 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 .
+ *
+ */
+@file:OptIn(ExperimentalMaterialApi::class)
+
+package com.instructure.parentapp.features.alerts.details
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.Divider
+import androidx.compose.material.ExperimentalMaterialApi
+import androidx.compose.material.Scaffold
+import androidx.compose.material.SnackbarHost
+import androidx.compose.material.SnackbarHostState
+import androidx.compose.material.SnackbarResult
+import androidx.compose.material.Text
+import androidx.compose.material.pullrefresh.PullRefreshIndicator
+import androidx.compose.material.pullrefresh.pullRefresh
+import androidx.compose.material.pullrefresh.rememberPullRefreshState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.res.colorResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import com.instructure.canvasapi2.models.Attachment
+import com.instructure.canvasapi2.utils.ContextKeeper
+import com.instructure.canvasapi2.utils.DateHelper
+import com.instructure.pandares.R
+import com.instructure.pandautils.compose.CanvasTheme
+import com.instructure.pandautils.compose.composables.CanvasThemedAppBar
+import com.instructure.pandautils.compose.composables.ComposeCanvasWebViewWrapper
+import com.instructure.pandautils.compose.composables.ErrorContent
+import com.instructure.pandautils.compose.composables.Loading
+import com.instructure.pandautils.features.inbox.utils.AttachmentCard
+import com.instructure.pandautils.features.inbox.utils.AttachmentCardItem
+import com.instructure.pandautils.features.inbox.utils.AttachmentStatus
+import com.jakewharton.threetenabp.AndroidThreeTen
+import java.util.Date
+
+@Composable
+fun AnnouncementDetailsScreen(
+ uiState: AnnouncementDetailsUiState,
+ actionHandler: (AnnouncementDetailsAction) -> Unit,
+ navigationActionClick: () -> Unit,
+ modifier: Modifier = Modifier
+) {
+ CanvasTheme {
+ val snackbarHostState = remember { SnackbarHostState() }
+ val errorMessage = stringResource(id = R.string.errorLoadingAnnouncement)
+ LaunchedEffect(uiState.showErrorSnack) {
+ if (uiState.showErrorSnack) {
+ val result = snackbarHostState.showSnackbar(errorMessage)
+ if (result == SnackbarResult.Dismissed) {
+ actionHandler(AnnouncementDetailsAction.SnackbarDismissed)
+ }
+ }
+ }
+
+ Scaffold(
+ backgroundColor = colorResource(id = R.color.backgroundLightest),
+ topBar = {
+ CanvasThemedAppBar(
+ title = uiState.pageTitle.orEmpty(),
+ backgroundColor = Color(uiState.studentColor),
+ contentColor = colorResource(id = R.color.textLightest),
+ navigationActionClick = {
+ navigationActionClick()
+ }
+ )
+ },
+ snackbarHost = { SnackbarHost(hostState = snackbarHostState) },
+ content = { padding ->
+ val pullRefreshState = rememberPullRefreshState(
+ refreshing = uiState.isRefreshing,
+ onRefresh = {
+ actionHandler(AnnouncementDetailsAction.Refresh)
+ }
+ )
+ Box(modifier = modifier.pullRefresh(pullRefreshState)) {
+ when {
+ uiState.isError -> {
+ ErrorContent(
+ errorMessage = stringResource(id = R.string.errorLoadingAnnouncement),
+ retryClick = {
+ actionHandler(AnnouncementDetailsAction.Refresh)
+ }, modifier = Modifier.fillMaxSize()
+ )
+ }
+
+ uiState.isLoading -> {
+ Loading(
+ modifier = Modifier
+ .fillMaxSize()
+ .testTag("loading"),
+ color = Color(uiState.studentColor)
+ )
+ }
+
+ else -> {
+ AnnouncementDetailsSuccessScreen(
+ uiState,
+ actionHandler,
+ modifier
+ )
+ }
+ }
+ PullRefreshIndicator(
+ refreshing = uiState.isRefreshing,
+ state = pullRefreshState,
+ modifier = Modifier
+ .align(Alignment.TopCenter)
+ .testTag("pullRefreshIndicator"),
+ contentColor = Color(uiState.studentColor)
+ )
+ }
+ })
+ }
+}
+
+@Composable
+private fun AnnouncementDetailsSuccessScreen(
+ uiState: AnnouncementDetailsUiState,
+ actionHandler: (AnnouncementDetailsAction) -> Unit,
+ modifier: Modifier = Modifier
+) {
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .verticalScroll(rememberScrollState())
+ ) {
+ val padding = 16.dp
+ Column(
+ modifier = modifier.padding(vertical = padding)
+ ) {
+ Column(modifier = Modifier.padding(horizontal = padding)) {
+ Text(
+ text = uiState.announcementTitle.orEmpty(),
+ style = TextStyle(
+ color = colorResource(id = R.color.textDarkest),
+ fontSize = 24.sp
+ )
+ )
+ Text(
+ text = DateHelper.getDateAtTimeString(
+ LocalContext.current,
+ R.string.alertDateTime,
+ uiState.postedDate
+ ).orEmpty(),
+ modifier = Modifier.padding(top = 4.dp, bottom = 18.dp),
+ style = TextStyle(
+ color = colorResource(id = R.color.textDarkest),
+ fontSize = 14.sp
+ )
+ )
+ AttachmentsRow(uiState.attachment, actionHandler)
+ }
+ uiState.message?.let { message ->
+ Divider(Modifier.padding(horizontal = padding))
+ Text(
+ modifier = Modifier.padding(
+ top = 18.dp, bottom = 6.dp, start = padding, end = padding
+ ),
+ text = stringResource(R.string.description),
+ style = TextStyle(
+ color = colorResource(id = R.color.textDarkest),
+ fontSize = 12.sp,
+ fontWeight = FontWeight.Medium
+ )
+ )
+ ComposeCanvasWebViewWrapper(
+ modifier = Modifier.padding(horizontal = 6.dp),
+ html = message
+ )
+ }
+ }
+ }
+}
+
+@Composable
+private fun AttachmentsRow(
+ attachment: Attachment?,
+ actionHandler: (AnnouncementDetailsAction) -> Unit
+) {
+ attachment?.let {
+ AttachmentCard(
+ AttachmentCardItem(
+ attachment = it,
+ status = AttachmentStatus.UPLOADED,
+ readOnly = true
+ ),
+ onSelect = { actionHandler(AnnouncementDetailsAction.OpenAttachment(it)) },
+ onRemove = {}
+ )
+ Spacer(modifier = Modifier.height(12.dp))
+ }
+}
+
+@Preview
+@Composable
+private fun AnnouncementDetailsPreview() {
+ ContextKeeper.appContext = LocalContext.current
+ AndroidThreeTen.init(LocalContext.current)
+ AnnouncementDetailsScreen(
+ uiState = AnnouncementDetailsUiState(
+ pageTitle = "Course Name",
+ announcementTitle = "Announcement Title",
+ postedDate = Date(),
+ message = "",
+ attachment = null
+ ),
+ actionHandler = {},
+ navigationActionClick = {}
+ )
+}
+
+@Preview
+@Composable
+private fun AnnouncementDetailsAttachmentPreview() {
+ ContextKeeper.appContext = LocalContext.current
+ AndroidThreeTen.init(LocalContext.current)
+ AnnouncementDetailsScreen(
+ uiState = AnnouncementDetailsUiState(
+ pageTitle = "Course Name",
+ announcementTitle = "Announcement Title",
+ postedDate = Date(),
+ message = "",
+ attachment = Attachment(
+ id = 1,
+ filename = "Attached_document",
+ contentType = "pdf"
+ )
+ ),
+ actionHandler = {},
+ navigationActionClick = {}
+ )
+}
+
+@Preview
+@Composable
+private fun AnnouncementDetailsErrorPreview() {
+ AnnouncementDetailsScreen(
+ uiState = AnnouncementDetailsUiState(
+ isError = true
+ ),
+ actionHandler = {},
+ navigationActionClick = {}
+ )
+}
+
+@Preview
+@Composable
+private fun AnnouncementDetailsLoadingPreview() {
+ AnnouncementDetailsScreen(
+ uiState = AnnouncementDetailsUiState(
+ isLoading = true
+ ),
+ actionHandler = {},
+ navigationActionClick = {}
+ )
+}
diff --git a/apps/parent/src/main/java/com/instructure/parentapp/features/alerts/details/AnnouncementDetailsUiState.kt b/apps/parent/src/main/java/com/instructure/parentapp/features/alerts/details/AnnouncementDetailsUiState.kt
new file mode 100644
index 0000000000..c6995dff00
--- /dev/null
+++ b/apps/parent/src/main/java/com/instructure/parentapp/features/alerts/details/AnnouncementDetailsUiState.kt
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2024 - 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.parentapp.features.alerts.details
+
+import android.graphics.Color
+import androidx.annotation.ColorInt
+import com.instructure.canvasapi2.models.Attachment
+import java.util.Date
+
+data class AnnouncementDetailsUiState(
+ val pageTitle: String? = null,
+ val announcementTitle: String? = null,
+ val postedDate: Date? = null,
+ val message: String? = null,
+ val attachment: Attachment? = null,
+ @ColorInt val studentColor: Int = Color.BLACK,
+ val isLoading: Boolean = false,
+ val isError: Boolean = false,
+ val isRefreshing: Boolean = false,
+ val showErrorSnack: Boolean = false
+)
+
+sealed class AnnouncementDetailsAction {
+ data object Refresh : AnnouncementDetailsAction()
+ data class OpenAttachment(val attachment: Attachment) : AnnouncementDetailsAction()
+ data object SnackbarDismissed :AnnouncementDetailsAction()
+}
diff --git a/apps/parent/src/main/java/com/instructure/parentapp/features/alerts/details/AnnouncementDetailsViewModel.kt b/apps/parent/src/main/java/com/instructure/parentapp/features/alerts/details/AnnouncementDetailsViewModel.kt
new file mode 100644
index 0000000000..db25e319ea
--- /dev/null
+++ b/apps/parent/src/main/java/com/instructure/parentapp/features/alerts/details/AnnouncementDetailsViewModel.kt
@@ -0,0 +1,159 @@
+/*
+ * Copyright (C) 2024 - 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.parentapp.features.alerts.details
+
+import android.content.Context
+import androidx.lifecycle.SavedStateHandle
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.instructure.canvasapi2.utils.weave.catch
+import com.instructure.canvasapi2.utils.weave.tryLaunch
+import com.instructure.pandares.R
+import com.instructure.pandautils.utils.FileDownloader
+import com.instructure.pandautils.utils.studentColor
+import com.instructure.parentapp.util.ParentPrefs
+import dagger.hilt.android.lifecycle.HiltViewModel
+import dagger.hilt.android.qualifiers.ApplicationContext
+import kotlinx.coroutines.async
+import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.update
+import kotlinx.coroutines.launch
+import javax.inject.Inject
+
+@HiltViewModel
+class AnnouncementDetailsViewModel @Inject constructor(
+ @ApplicationContext private val context: Context,
+ savedStateHandle: SavedStateHandle,
+ private val repository: AnnouncementDetailsRepository,
+ private val parentPrefs: ParentPrefs,
+ private val fileDownloader: FileDownloader
+) : ViewModel() {
+
+ private val courseId = savedStateHandle.get(AnnouncementDetailsFragment.COURSE_ID) ?: -1
+ private val announcementId =
+ savedStateHandle.get(AnnouncementDetailsFragment.ANNOUNCEMENT_ID) ?: -1
+
+ private val _uiState = MutableStateFlow(AnnouncementDetailsUiState())
+ val uiState = _uiState.asStateFlow()
+
+ init {
+ _uiState.update {
+ it.copy(isLoading = true)
+ }
+ loadData()
+ }
+
+ fun handleAction(action: AnnouncementDetailsAction) {
+ when (action) {
+ is AnnouncementDetailsAction.Refresh -> {
+ _uiState.update {
+ it.copy(isRefreshing = true)
+ }
+ loadData(true)
+ }
+ is AnnouncementDetailsAction.OpenAttachment -> {
+ viewModelScope.launch {
+ fileDownloader.downloadFileToDevice(action.attachment)
+ }
+ }
+ AnnouncementDetailsAction.SnackbarDismissed -> {
+ _uiState.update {
+ it.copy(showErrorSnack = false)
+ }
+ }
+ }
+ }
+
+ private fun loadData(forceNetwork: Boolean = false) {
+ viewModelScope.tryLaunch {
+ _uiState.update {
+ it.copy(
+ studentColor = parentPrefs.currentStudent.studentColor
+ )
+ }
+ if (courseId == -1L) {
+ fetchGlobalAnnouncement(forceNetwork)
+ } else {
+ fetchCourseAnnouncement(forceNetwork)
+ }
+ } catch {
+ if (uiState.value.pageTitle == null && uiState.value.announcementTitle == null && uiState.value.message == null) {
+ _uiState.update {
+ it.copy(
+ isLoading = false,
+ isRefreshing = false,
+ isError = true
+ )
+ }
+ } else {
+ _uiState.update {
+ it.copy(
+ isLoading = false,
+ isRefreshing = false,
+ showErrorSnack = true
+ )
+ }
+ }
+ }
+ }
+
+ private suspend fun fetchGlobalAnnouncement(forceNetwork: Boolean = false) {
+ val globalAnnouncement = repository.getGlobalAnnouncement(announcementId, forceNetwork)
+ _uiState.update {
+ it.copy(
+ isLoading = false,
+ isRefreshing = false,
+ isError = false,
+ pageTitle = context.getString(R.string.globalAnnouncementPageTitle),
+ announcementTitle = globalAnnouncement.subject,
+ message = globalAnnouncement.message,
+ postedDate = globalAnnouncement.startDate,
+ )
+ }
+ }
+
+ private suspend fun fetchCourseAnnouncement(forceNetwork: Boolean = false) {
+ coroutineScope {
+ val course = async { repository.getCourse(courseId, forceNetwork) }
+ val announcement = async {
+ repository.getCourseAnnouncement(
+ courseId,
+ announcementId,
+ forceNetwork
+ )
+ }
+
+ _uiState.update {
+ with(announcement.await()) {
+ it.copy(
+ isLoading = false,
+ isRefreshing = false,
+ isError = false,
+ pageTitle = course.await()?.name,
+ announcementTitle = title,
+ message = message,
+ postedDate = postedDate,
+ attachment = attachments.getOrNull(0)?.toAttachment()
+ )
+ }
+ }
+ }
+ }
+}
diff --git a/apps/parent/src/main/java/com/instructure/parentapp/features/alerts/list/AlertsFragment.kt b/apps/parent/src/main/java/com/instructure/parentapp/features/alerts/list/AlertsFragment.kt
index 74a2c9bdf0..eec0ce9350 100644
--- a/apps/parent/src/main/java/com/instructure/parentapp/features/alerts/list/AlertsFragment.kt
+++ b/apps/parent/src/main/java/com/instructure/parentapp/features/alerts/list/AlertsFragment.kt
@@ -58,10 +58,14 @@ class AlertsFragment : Fragment() {
private fun handleAction(action: AlertsViewModelAction) {
when (action) {
- is AlertsViewModelAction.Navigate -> {
- navigation.navigate(activity, action.route)
+ is AlertsViewModelAction.NavigateToRoute -> {
+ action.route?.let {
+ navigation.navigate(activity, it)
+ }
+ }
+ is AlertsViewModelAction.NavigateToGlobalAnnouncement -> {
+ navigation.navigate(activity, navigation.globalAnnouncementRoute(action.alertId))
}
-
is AlertsViewModelAction.ShowSnackbar -> {
Snackbar.make(requireView(), action.message, Snackbar.LENGTH_SHORT).apply {
action.action?.let { setAction(it) { action.actionCallback?.invoke() } }
diff --git a/apps/parent/src/main/java/com/instructure/parentapp/features/alerts/list/AlertsRepository.kt b/apps/parent/src/main/java/com/instructure/parentapp/features/alerts/list/AlertsRepository.kt
index 70b932d0cf..10b61c13c6 100644
--- a/apps/parent/src/main/java/com/instructure/parentapp/features/alerts/list/AlertsRepository.kt
+++ b/apps/parent/src/main/java/com/instructure/parentapp/features/alerts/list/AlertsRepository.kt
@@ -31,7 +31,7 @@ class AlertsRepository(
) {
suspend fun getAlertsForStudent(studentId: Long, forceNetwork: Boolean): List {
- val restParams = RestParams(isForceReadFromNetwork = forceNetwork)
+ val restParams = RestParams(isForceReadFromNetwork = forceNetwork, usePerPageQueryParam = true)
val allAlerts = observerApi.getObserverAlerts(studentId, restParams).depaginate {
observerApi.getNextPageObserverAlerts(it, restParams)
}.dataOrThrow.sortedByDescending { it.actionDate }
diff --git a/apps/parent/src/main/java/com/instructure/parentapp/features/alerts/list/AlertsScreen.kt b/apps/parent/src/main/java/com/instructure/parentapp/features/alerts/list/AlertsScreen.kt
index 01739d777d..7c72717b0c 100644
--- a/apps/parent/src/main/java/com/instructure/parentapp/features/alerts/list/AlertsScreen.kt
+++ b/apps/parent/src/main/java/com/instructure/parentapp/features/alerts/list/AlertsScreen.kt
@@ -217,19 +217,10 @@ fun AlertsListItem(
}
}
- fun dateTime(dateTime: Date): String {
- val date = DateHelper.getDayMonthDateString(context, dateTime)
- val time = DateHelper.getFormattedTime(context, dateTime)
-
- return context.getString(R.string.alertDateTime, date, time)
- }
-
Row(modifier = modifier
.fillMaxWidth()
- .clickable(enabled = alert.htmlUrl != null) {
- alert.htmlUrl?.let {
- actionHandler(AlertsAction.Navigate(alert.alertId, it))
- }
+ .clickable {
+ actionHandler(AlertsAction.Navigate(alert.alertId, alert.contextId, alert.htmlUrl, alert.alertType))
}
.padding(8.dp)
.testTag("alertItem"),
@@ -269,7 +260,11 @@ fun AlertsListItem(
)
alert.date?.let {
Text(
- text = dateTime(alert.date),
+ text = DateHelper.getDateAtTimeString(
+ LocalContext.current,
+ com.instructure.pandares.R.string.alertDateTime,
+ it
+ ) ?: "",
style = TextStyle(
color = colorResource(id = R.color.textDark),
fontSize = 12.sp
@@ -300,6 +295,7 @@ fun AlertsScreenPreview() {
alerts = listOf(
AlertsItemUiState(
alertId = 1L,
+ contextId = 1L,
title = "Alert title",
alertType = AlertType.COURSE_ANNOUNCEMENT,
date = Date(),
@@ -310,6 +306,7 @@ fun AlertsScreenPreview() {
),
AlertsItemUiState(
alertId = 2L,
+ contextId = 2L,
title = "Assignment missing",
alertType = AlertType.ASSIGNMENT_MISSING,
date = Date(),
@@ -320,6 +317,7 @@ fun AlertsScreenPreview() {
),
AlertsItemUiState(
alertId = 3L,
+ contextId = 3L,
title = "Course grade low",
alertType = AlertType.COURSE_GRADE_LOW,
date = Date(),
@@ -330,6 +328,7 @@ fun AlertsScreenPreview() {
),
AlertsItemUiState(
alertId = 4L,
+ contextId = 4L,
title = "Course grade high",
alertType = AlertType.COURSE_GRADE_HIGH,
date = Date(),
@@ -340,6 +339,7 @@ fun AlertsScreenPreview() {
),
AlertsItemUiState(
alertId = 5L,
+ contextId = 5L,
title = "Institution announcement",
alertType = AlertType.INSTITUTION_ANNOUNCEMENT,
date = Date(),
@@ -350,6 +350,7 @@ fun AlertsScreenPreview() {
),
AlertsItemUiState(
alertId = 6L,
+ contextId = 6L,
title = "Assignment grade low",
alertType = AlertType.ASSIGNMENT_GRADE_LOW,
date = Date(),
@@ -360,6 +361,7 @@ fun AlertsScreenPreview() {
),
AlertsItemUiState(
alertId = 7L,
+ contextId = 7L,
title = "Assignment grade high",
alertType = AlertType.ASSIGNMENT_GRADE_HIGH,
date = Date(),
@@ -370,6 +372,7 @@ fun AlertsScreenPreview() {
),
AlertsItemUiState(
alertId = 8L,
+ contextId = 8L,
title = "Locked alert",
alertType = AlertType.COURSE_ANNOUNCEMENT,
date = Date(),
@@ -427,6 +430,7 @@ fun AlertsListItemPreview() {
AlertsListItem(
alert = AlertsItemUiState(
alertId = 1L,
+ contextId = 1L,
title = "Alert title",
alertType = AlertType.COURSE_ANNOUNCEMENT,
date = Date(),
diff --git a/apps/parent/src/main/java/com/instructure/parentapp/features/alerts/list/AlertsUiState.kt b/apps/parent/src/main/java/com/instructure/parentapp/features/alerts/list/AlertsUiState.kt
index f9199fcd11..e30494699d 100644
--- a/apps/parent/src/main/java/com/instructure/parentapp/features/alerts/list/AlertsUiState.kt
+++ b/apps/parent/src/main/java/com/instructure/parentapp/features/alerts/list/AlertsUiState.kt
@@ -30,6 +30,7 @@ data class AlertsUiState(
data class AlertsItemUiState(
val alertId: Long,
+ val contextId: Long,
val title: String,
val alertType: AlertType,
val date: Date?,
@@ -40,12 +41,13 @@ data class AlertsItemUiState(
)
sealed class AlertsViewModelAction {
- data class Navigate(val route: String): AlertsViewModelAction()
+ data class NavigateToRoute(val route: String?): AlertsViewModelAction()
+ data class NavigateToGlobalAnnouncement(val alertId: Long): AlertsViewModelAction()
data class ShowSnackbar(val message: Int, val action: Int?, val actionCallback: (() -> Unit)?): AlertsViewModelAction()
}
sealed class AlertsAction {
data object Refresh : AlertsAction()
- data class Navigate(val alertId: Long, val route: String) : AlertsAction()
+ data class Navigate(val alertId: Long, val contextId: Long, val route: String?, val alertType: AlertType) : AlertsAction()
data class DismissAlert(val alertId: Long) : AlertsAction()
}
\ No newline at end of file
diff --git a/apps/parent/src/main/java/com/instructure/parentapp/features/alerts/list/AlertsViewModel.kt b/apps/parent/src/main/java/com/instructure/parentapp/features/alerts/list/AlertsViewModel.kt
index 1dcdf95ed6..87833d9472 100644
--- a/apps/parent/src/main/java/com/instructure/parentapp/features/alerts/list/AlertsViewModel.kt
+++ b/apps/parent/src/main/java/com/instructure/parentapp/features/alerts/list/AlertsViewModel.kt
@@ -20,9 +20,10 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.instructure.canvasapi2.models.Alert
import com.instructure.canvasapi2.models.AlertThreshold
+import com.instructure.canvasapi2.models.AlertType
import com.instructure.canvasapi2.models.AlertWorkflowState
import com.instructure.canvasapi2.models.User
-import com.instructure.pandautils.utils.color
+import com.instructure.pandautils.utils.studentColor
import com.instructure.parentapp.R
import com.instructure.parentapp.features.dashboard.AlertCountUpdater
import com.instructure.parentapp.features.dashboard.SelectedStudentHolder
@@ -58,6 +59,20 @@ class AlertsViewModel @Inject constructor(
studentChanged(it)
}
}
+
+ viewModelScope.launch {
+ selectedStudentHolder.selectedStudentColorChanged.collect {
+ updateColor()
+ }
+ }
+ }
+
+ private fun updateColor() {
+ selectedStudent?.let { student ->
+ _uiState.update {
+ it.copy(studentColor = student.studentColor)
+ }
+ }
}
private suspend fun studentChanged(student: User?) {
@@ -65,7 +80,7 @@ class AlertsViewModel @Inject constructor(
selectedStudent = student
_uiState.update {
it.copy(
- studentColor = student.color,
+ studentColor = student.studentColor,
isLoading = true
)
}
@@ -112,7 +127,14 @@ class AlertsViewModel @Inject constructor(
when (action) {
is AlertsAction.Navigate -> {
viewModelScope.launch {
- _events.send(AlertsViewModelAction.Navigate(action.route))
+ when (action.alertType) {
+ AlertType.INSTITUTION_ANNOUNCEMENT -> {
+ _events.send(AlertsViewModelAction.NavigateToGlobalAnnouncement(action.contextId))
+ }
+ else -> {
+ _events.send(AlertsViewModelAction.NavigateToRoute(action.route))
+ }
+ }
markAlertRead(action.alertId)
alertCountUpdater.updateShouldRefreshAlertCount(true)
}
@@ -191,6 +213,7 @@ class AlertsViewModel @Inject constructor(
private fun createAlertItem(alert: Alert): AlertsItemUiState {
return AlertsItemUiState(
alertId = alert.id,
+ contextId = alert.contextId,
title = alert.title,
alertType = alert.alertType,
date = alert.actionDate,
diff --git a/apps/parent/src/main/java/com/instructure/parentapp/features/alerts/settings/AlertSettingsScreen.kt b/apps/parent/src/main/java/com/instructure/parentapp/features/alerts/settings/AlertSettingsScreen.kt
index c7ccb3f520..95067605a9 100644
--- a/apps/parent/src/main/java/com/instructure/parentapp/features/alerts/settings/AlertSettingsScreen.kt
+++ b/apps/parent/src/main/java/com/instructure/parentapp/features/alerts/settings/AlertSettingsScreen.kt
@@ -99,7 +99,7 @@ fun AlertSettingsScreen(
navIconRes = R.drawable.ic_back_arrow,
navigationActionClick = navigationActionClick,
backgroundColor = Color(uiState.userColor),
- textColor = colorResource(id = R.color.white),
+ textColor = colorResource(id = R.color.textLightest),
actions = {
var showMenu by remember { mutableStateOf(false) }
var showConfirmationDialog by remember { mutableStateOf(false) }
@@ -113,7 +113,10 @@ fun AlertSettingsScreen(
}
}
OverflowMenu(
+ modifier = Modifier
+ .background(color = colorResource(id = R.color.backgroundLightestElevated)),
showMenu = showMenu,
+ iconColor = colorResource(id = R.color.textLightest),
onDismissRequest = { showMenu = !showMenu }) {
DropdownMenuItem(
modifier = Modifier.testTag("deleteMenuItem"),
@@ -123,7 +126,7 @@ fun AlertSettingsScreen(
showConfirmationDialog = true
}
}) {
- Text(text = stringResource(id = R.string.delete))
+ Text(text = stringResource(id = R.string.delete), color = colorResource(id = R.color.textDarkest))
}
}
}
@@ -261,7 +264,7 @@ fun getTitle(alertType: AlertType): Int {
AlertType.COURSE_GRADE_HIGH -> R.string.alertSettingsCourseGradeHigh
AlertType.COURSE_GRADE_LOW -> R.string.alertSettingsCourseGradeLow
AlertType.COURSE_ANNOUNCEMENT -> R.string.alertSettingsCourseAnnouncement
- AlertType.INSTITUTION_ANNOUNCEMENT -> R.string.alertSettingsInstitutionAnnouncement
+ AlertType.INSTITUTION_ANNOUNCEMENT -> R.string.alertSettingsGlobalAnnouncement
}
}
diff --git a/apps/parent/src/main/java/com/instructure/parentapp/features/alerts/settings/AlertSettingsViewModel.kt b/apps/parent/src/main/java/com/instructure/parentapp/features/alerts/settings/AlertSettingsViewModel.kt
index 9f8d14306b..253e725a71 100644
--- a/apps/parent/src/main/java/com/instructure/parentapp/features/alerts/settings/AlertSettingsViewModel.kt
+++ b/apps/parent/src/main/java/com/instructure/parentapp/features/alerts/settings/AlertSettingsViewModel.kt
@@ -23,7 +23,7 @@ import com.google.firebase.crashlytics.FirebaseCrashlytics
import com.instructure.canvasapi2.models.AlertType
import com.instructure.canvasapi2.models.User
import com.instructure.pandautils.utils.Const
-import com.instructure.pandautils.utils.color
+import com.instructure.pandautils.utils.studentColor
import com.instructure.parentapp.R
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.channels.Channel
@@ -50,7 +50,7 @@ class AlertSettingsViewModel @Inject constructor(
avatarUrl = student.avatarUrl.orEmpty(),
studentName = student.shortName ?: student.name,
studentPronouns = student.pronouns,
- userColor = student.color,
+ userColor = student.studentColor,
actionHandler = this::handleAction
)
)
diff --git a/apps/parent/src/main/java/com/instructure/parentapp/features/assignment/details/ParentAssignmentDetailsBehaviour.kt b/apps/parent/src/main/java/com/instructure/parentapp/features/assignment/details/ParentAssignmentDetailsBehaviour.kt
new file mode 100644
index 0000000000..6fc95d507c
--- /dev/null
+++ b/apps/parent/src/main/java/com/instructure/parentapp/features/assignment/details/ParentAssignmentDetailsBehaviour.kt
@@ -0,0 +1,136 @@
+/*
+ * Copyright (C) 2024 - 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.parentapp.features.assignment.details
+
+import android.content.Context
+import android.content.res.ColorStateList
+import androidx.annotation.ColorInt
+import androidx.appcompat.content.res.AppCompatResources
+import androidx.appcompat.widget.Toolbar
+import androidx.constraintlayout.widget.ConstraintLayout
+import androidx.fragment.app.FragmentActivity
+import com.google.android.material.floatingactionbutton.FloatingActionButton
+import com.instructure.canvasapi2.models.Assignment
+import com.instructure.canvasapi2.models.Course
+import com.instructure.canvasapi2.type.EnrollmentType
+import com.instructure.canvasapi2.utils.ApiPrefs
+import com.instructure.interactions.bookmarks.Bookmarker
+import com.instructure.pandautils.binding.setTint
+import com.instructure.pandautils.databinding.FragmentAssignmentDetailsBinding
+import com.instructure.pandautils.features.assignments.details.AssignmentDetailsBehaviour
+import com.instructure.pandautils.features.inbox.utils.InboxComposeOptions
+import com.instructure.pandautils.utils.DP
+import com.instructure.pandautils.utils.ViewStyler
+import com.instructure.pandautils.utils.onClick
+import com.instructure.pandautils.utils.orDefault
+import com.instructure.pandautils.utils.studentColor
+import com.instructure.parentapp.R
+import com.instructure.parentapp.util.ParentPrefs
+import javax.inject.Inject
+
+
+class ParentAssignmentDetailsBehaviour @Inject constructor(
+ private val parentPrefs: ParentPrefs,
+ private val apiPrefs: ApiPrefs,
+): AssignmentDetailsBehaviour() {
+ @ColorInt override val dialogColor: Int = parentPrefs.currentStudent.studentColor
+
+ private var fab: FloatingActionButton? = null
+
+ override fun applyTheme(
+ activity: FragmentActivity,
+ binding: FragmentAssignmentDetailsBinding?,
+ bookmark: Bookmarker,
+ course: Course?,
+ toolbar: Toolbar
+ ) {
+ ViewStyler.themeToolbarColored(activity, toolbar, parentPrefs.currentStudent.studentColor, activity.getColor(R.color.textLightest))
+ ViewStyler.setStatusBarDark(activity, parentPrefs.currentStudent.studentColor)
+ }
+
+ override fun setupAppSpecificViews(
+ activity: FragmentActivity,
+ binding: FragmentAssignmentDetailsBinding?,
+ course: Course,
+ assignment: Assignment?,
+ routeToCompose: ((InboxComposeOptions) -> Unit)?
+ ) {
+ binding?.assignmentDetailsPage?.addView(messageFAB(activity, course, assignment, routeToCompose))
+
+ binding?.scrollView?.setOnScrollChangeListener { _, _, scrollY, _, oldScrollY ->
+ if (binding.scrollView.scrollY == 0) {
+ fab?.show()
+ } else if (binding.scrollView.getChildAt(0).bottom <= (binding.scrollView.height + binding.scrollView.scrollY)) {
+ fab?.hide()
+ } else if (scrollY < oldScrollY) {
+ fab?.show()
+ } else if (scrollY > oldScrollY) {
+ fab?.hide()
+ }
+ }
+ }
+
+ private fun messageFAB(context: Context, course: Course, assignment: Assignment?, routeToCompose: ((InboxComposeOptions) -> Unit)?): FloatingActionButton {
+ return FloatingActionButton(context).apply {
+ setImageDrawable(AppCompatResources.getDrawable(context, R.drawable.ic_chat))
+ contentDescription = context.getString(R.string.sendMessageAboutAssignment)
+ setTint(R.color.textLightest)
+ backgroundTintList = ColorStateList.valueOf(parentPrefs.currentStudent.studentColor)
+ layoutParams = ConstraintLayout.LayoutParams(ConstraintLayout.LayoutParams.WRAP_CONTENT, ConstraintLayout.LayoutParams.WRAP_CONTENT).apply {
+ bottomToBottom = ConstraintLayout.LayoutParams.PARENT_ID
+ endToEnd = ConstraintLayout.LayoutParams.PARENT_ID
+ marginEnd = context.DP(16).toInt()
+ bottomMargin = context.DP(16).toInt()
+ }
+ onClick {
+ routeToCompose?.invoke(getInboxComposeOptions(context, course, assignment))
+ }
+ }
+ .also { fab = it }
+ }
+
+ private fun getInboxComposeOptions(context: Context, course: Course?, assignment: Assignment?): InboxComposeOptions {
+ val courseContextId = course?.contextId.orEmpty()
+ var options = InboxComposeOptions.buildNewMessage()
+ options = options.copy(
+ defaultValues = options.defaultValues.copy(
+ contextCode = courseContextId,
+ contextName = course?.name.orEmpty(),
+ subject = context.getString(
+ R.string.regardingHiddenMessageWithAssignmentPrefix,
+ parentPrefs.currentStudent?.name.orEmpty(),
+ assignment?.name.orEmpty()
+ )
+ ),
+ disabledFields = options.disabledFields.copy(
+ isContextDisabled = true
+ ),
+ autoSelectRecipientsFromRoles = listOf(EnrollmentType.TEACHERENROLLMENT),
+ hiddenBodyMessage = context.getString(
+ R.string.regardingHiddenMessage,
+ parentPrefs.currentStudent?.name.orEmpty(),
+ getContextURL(course?.id.orDefault(), assignment?.id.orDefault())
+ )
+ )
+
+ return options
+ }
+
+ private fun getContextURL(courseId: Long, assignmentId: Long): String {
+ return "${apiPrefs.fullDomain}/courses/$courseId/assignments/$assignmentId"
+ }
+}
\ No newline at end of file
diff --git a/apps/parent/src/main/java/com/instructure/parentapp/features/assignment/details/ParentAssignmentDetailsColorProvider.kt b/apps/parent/src/main/java/com/instructure/parentapp/features/assignment/details/ParentAssignmentDetailsColorProvider.kt
new file mode 100644
index 0000000000..c31c45d368
--- /dev/null
+++ b/apps/parent/src/main/java/com/instructure/parentapp/features/assignment/details/ParentAssignmentDetailsColorProvider.kt
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2024 - 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.parentapp.features.assignment.details
+
+import androidx.annotation.ColorInt
+import com.instructure.canvasapi2.models.Course
+import com.instructure.pandautils.features.assignments.details.AssignmentDetailsColorProvider
+import com.instructure.pandautils.utils.ColorKeeper
+import com.instructure.pandautils.utils.ThemedColor
+import com.instructure.pandautils.utils.studentColor
+import com.instructure.parentapp.util.ParentPrefs
+import javax.inject.Inject
+
+class ParentAssignmentDetailsColorProvider @Inject constructor(
+ private val parentPrefs: ParentPrefs,
+ private val colorKeeper: ColorKeeper
+): AssignmentDetailsColorProvider() {
+ @ColorInt
+ override val submissionAndRubricLabelColor: Int = parentPrefs.currentStudent.studentColor
+
+ override fun getContentColor(course: Course?): ThemedColor {
+ return colorKeeper.getOrGenerateUserColor(parentPrefs.currentStudent)
+ }
+}
\ No newline at end of file
diff --git a/apps/parent/src/main/java/com/instructure/parentapp/features/assignment/details/ParentAssignmentDetailsRepository.kt b/apps/parent/src/main/java/com/instructure/parentapp/features/assignment/details/ParentAssignmentDetailsRepository.kt
new file mode 100644
index 0000000000..02fec3714c
--- /dev/null
+++ b/apps/parent/src/main/java/com/instructure/parentapp/features/assignment/details/ParentAssignmentDetailsRepository.kt
@@ -0,0 +1,106 @@
+/*
+ * Copyright (C) 2024 - 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.parentapp.features.assignment.details
+
+import androidx.lifecycle.LiveData
+import com.instructure.canvasapi2.apis.AssignmentAPI
+import com.instructure.canvasapi2.apis.CourseAPI
+import com.instructure.canvasapi2.apis.QuizAPI
+import com.instructure.canvasapi2.apis.SubmissionAPI
+import com.instructure.canvasapi2.builders.RestParams
+import com.instructure.canvasapi2.models.Assignment
+import com.instructure.canvasapi2.models.Course
+import com.instructure.canvasapi2.models.LTITool
+import com.instructure.canvasapi2.models.Quiz
+import com.instructure.pandautils.features.assignments.details.AssignmentDetailsRepository
+import com.instructure.pandautils.room.appdatabase.daos.ReminderDao
+import com.instructure.pandautils.room.appdatabase.entities.ReminderEntity
+import com.instructure.pandautils.utils.orDefault
+import com.instructure.parentapp.util.ParentPrefs
+
+class ParentAssignmentDetailsRepository(
+ private val coursesApi: CourseAPI.CoursesInterface,
+ private val assignmentApi: AssignmentAPI.AssignmentInterface,
+ private val quizApi: QuizAPI.QuizInterface,
+ private val submissionApi: SubmissionAPI.SubmissionInterface,
+ private val reminderDao: ReminderDao
+): AssignmentDetailsRepository {
+ override suspend fun getCourseWithGrade(courseId: Long, forceNetwork: Boolean): Course {
+ val params = RestParams(isForceReadFromNetwork = forceNetwork)
+ return coursesApi.getCourseWithGrade(courseId, params).dataOrThrow
+ }
+
+ override suspend fun getAssignment(
+ isObserver: Boolean,
+ assignmentId: Long,
+ courseId: Long,
+ forceNetwork: Boolean
+ ): Assignment {
+ val params = RestParams(isForceReadFromNetwork = forceNetwork)
+ return assignmentApi.getAssignmentIncludeObservees(courseId, assignmentId, params).dataOrThrow.toAssignment(ParentPrefs.currentStudent?.id.orDefault())
+ }
+
+ override suspend fun getQuiz(courseId: Long, quizId: Long, forceNetwork: Boolean): Quiz {
+ val params = RestParams(isForceReadFromNetwork = forceNetwork)
+ return quizApi.getQuiz(courseId, quizId, params).dataOrThrow
+ }
+
+ override suspend fun getExternalToolLaunchUrl(
+ courseId: Long,
+ externalToolId: Long,
+ assignmentId: Long,
+ forceNetwork: Boolean
+ ): LTITool? {
+ val params = RestParams(isForceReadFromNetwork = forceNetwork)
+ return assignmentApi.getExternalToolLaunchUrl(courseId, externalToolId, assignmentId, restParams = params).dataOrThrow
+ }
+
+ override suspend fun getLtiFromAuthenticationUrl(url: String, forceNetwork: Boolean): LTITool? {
+ val params = RestParams(isForceReadFromNetwork = forceNetwork)
+ return submissionApi.getLtiFromAuthenticationUrl(url, params).dataOrThrow
+ }
+
+ override fun getRemindersByAssignmentIdLiveData(
+ userId: Long,
+ assignmentId: Long
+ ): LiveData> {
+ return reminderDao.findByAssignmentIdLiveData(userId, assignmentId)
+ }
+
+ override suspend fun deleteReminderById(id: Long) {
+ reminderDao.deleteById(id)
+ }
+
+ override suspend fun addReminder(
+ userId: Long,
+ assignment: Assignment,
+ text: String,
+ time: Long
+ ): Long {
+ return reminderDao.insert(
+ ReminderEntity(
+ userId = userId,
+ assignmentId = assignment.id,
+ htmlUrl = assignment.htmlUrl.orEmpty(),
+ name = assignment.name.orEmpty(),
+ text = text,
+ time = time
+ )
+ )
+ }
+
+ override fun isOnline(): Boolean = true
+}
\ No newline at end of file
diff --git a/apps/parent/src/main/java/com/instructure/parentapp/features/assignment/details/ParentAssignmentDetailsRouter.kt b/apps/parent/src/main/java/com/instructure/parentapp/features/assignment/details/ParentAssignmentDetailsRouter.kt
new file mode 100644
index 0000000000..40c4c8a9ee
--- /dev/null
+++ b/apps/parent/src/main/java/com/instructure/parentapp/features/assignment/details/ParentAssignmentDetailsRouter.kt
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2024 - 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.parentapp.features.assignment.details
+
+import androidx.fragment.app.FragmentActivity
+import com.instructure.pandautils.features.assignments.details.AssignmentDetailsRouter
+import com.instructure.pandautils.features.inbox.utils.InboxComposeOptions
+import com.instructure.parentapp.util.navigation.Navigation
+
+class ParentAssignmentDetailsRouter(
+ private val navigation: Navigation
+): AssignmentDetailsRouter() {
+ override fun navigateToSendMessage(activity: FragmentActivity, options: InboxComposeOptions) {
+ val route = navigation.inboxComposeRoute(options)
+ navigation.navigate(activity, route)
+ }
+}
\ No newline at end of file
diff --git a/apps/parent/src/main/java/com/instructure/parentapp/features/assignment/details/ParentAssignmentDetailsSubmissionHandler.kt b/apps/parent/src/main/java/com/instructure/parentapp/features/assignment/details/ParentAssignmentDetailsSubmissionHandler.kt
new file mode 100644
index 0000000000..a8ecf5345b
--- /dev/null
+++ b/apps/parent/src/main/java/com/instructure/parentapp/features/assignment/details/ParentAssignmentDetailsSubmissionHandler.kt
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2024 - 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.parentapp.features.assignment.details
+
+import android.content.Context
+import android.content.res.Resources
+import android.net.Uri
+import androidx.fragment.app.FragmentActivity
+import androidx.lifecycle.MutableLiveData
+import com.instructure.canvasapi2.models.Assignment
+import com.instructure.canvasapi2.models.Course
+import com.instructure.canvasapi2.models.LTITool
+import com.instructure.pandautils.features.assignments.details.AssignmentDetailsSubmissionHandler
+import com.instructure.pandautils.features.assignments.details.AssignmentDetailsViewData
+import java.io.File
+
+class ParentAssignmentDetailsSubmissionHandler(
+) : AssignmentDetailsSubmissionHandler {
+ override var isUploading: Boolean = false
+ override var lastSubmissionIsDraft: Boolean = false
+ override var lastSubmissionEntry: String? = null
+ override var lastSubmissionAssignmentId: Long? = null
+ override var lastSubmissionSubmissionType: String? = null
+
+ override fun addAssignmentSubmissionObserver(assignmentId: Long, userId: Long, resources: Resources, data: MutableLiveData, refreshAssignment: () -> Unit) = Unit
+
+ override fun removeAssignmentSubmissionObserver() = Unit
+
+ override fun uploadAudioSubmission(context: Context?, course: Course?, assignment: Assignment?, file: File?) = Unit
+
+ override fun getVideoUri(fragment: FragmentActivity): Uri? = null
+
+ override suspend fun getStudioLTITool(assignment: Assignment, courseId: Long?): LTITool? = null
+}
\ No newline at end of file
diff --git a/apps/parent/src/main/java/com/instructure/parentapp/features/assignment/details/receiver/ParentAlarmReceiverNotificationHandler.kt b/apps/parent/src/main/java/com/instructure/parentapp/features/assignment/details/receiver/ParentAlarmReceiverNotificationHandler.kt
new file mode 100644
index 0000000000..0f8f31ae1d
--- /dev/null
+++ b/apps/parent/src/main/java/com/instructure/parentapp/features/assignment/details/receiver/ParentAlarmReceiverNotificationHandler.kt
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2024 - 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.parentapp.features.assignment.details.receiver
+
+import android.app.NotificationChannel
+import android.app.NotificationManager
+import android.app.PendingIntent
+import android.content.Context
+import android.net.Uri
+import androidx.core.app.NotificationCompat
+import com.instructure.pandautils.receivers.alarm.AlarmReceiver
+import com.instructure.pandautils.receivers.alarm.AlarmReceiverNotificationHandler
+import com.instructure.parentapp.R
+import com.instructure.parentapp.features.main.MainActivity
+
+class ParentAlarmReceiverNotificationHandler: AlarmReceiverNotificationHandler {
+ override fun showNotification(context: Context, assignmentId: Long, assignmentPath: String, assignmentName: String, dueIn: String) {
+ val intent = MainActivity.createIntent(context, Uri.parse(assignmentPath))
+
+ val pendingIntent = PendingIntent.getActivity(
+ context, 0, intent,
+ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
+ )
+
+ val builder = NotificationCompat.Builder(context, AlarmReceiver.CHANNEL_ID)
+ .setSmallIcon(R.drawable.ic_notification_canvas_logo)
+ .setContentTitle(context.getString(R.string.reminderNotificationTitle))
+ .setContentText(context.getString(R.string.reminderNotificationDescription, dueIn, assignmentName))
+ .setAutoCancel(true)
+ .setContentIntent(pendingIntent)
+ .setPriority(NotificationCompat.PRIORITY_DEFAULT)
+
+ val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
+ notificationManager.notify(assignmentId.toInt(), builder.build())
+ }
+
+ override fun createNotificationChannel(context: Context) {
+ val channel = NotificationChannel(
+ AlarmReceiver.CHANNEL_ID,
+ context.getString(R.string.reminderNotificationChannelName),
+ NotificationManager.IMPORTANCE_DEFAULT
+ ).apply {
+ description = context.getString(R.string.reminderNotificationChannelDescription)
+ }
+
+ val notificationManager: NotificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
+ notificationManager.createNotificationChannel(channel)
+ }
+}
\ No newline at end of file
diff --git a/apps/parent/src/main/java/com/instructure/parentapp/features/calendar/ParentCalendarFragment.kt b/apps/parent/src/main/java/com/instructure/parentapp/features/calendar/ParentCalendarFragment.kt
index d3c2000ef0..79116692bc 100644
--- a/apps/parent/src/main/java/com/instructure/parentapp/features/calendar/ParentCalendarFragment.kt
+++ b/apps/parent/src/main/java/com/instructure/parentapp/features/calendar/ParentCalendarFragment.kt
@@ -21,7 +21,7 @@ import androidx.lifecycle.flowWithLifecycle
import androidx.lifecycle.lifecycleScope
import com.instructure.pandautils.features.calendar.BaseCalendarFragment
import com.instructure.pandautils.utils.ViewStyler
-import com.instructure.pandautils.utils.color
+import com.instructure.pandautils.utils.studentColor
import com.instructure.parentapp.features.dashboard.SelectedStudentHolder
import com.instructure.parentapp.util.ParentPrefs
import dagger.hilt.android.AndroidEntryPoint
@@ -47,7 +47,8 @@ class ParentCalendarFragment : BaseCalendarFragment() {
}
override fun applyTheme() {
- val color = ParentPrefs.currentStudent.color
+ val student = ParentPrefs.currentStudent
+ val color = student.studentColor
ViewStyler.setStatusBarDark(requireActivity(), color)
}
diff --git a/apps/parent/src/main/java/com/instructure/parentapp/features/calendar/ParentCalendarRepository.kt b/apps/parent/src/main/java/com/instructure/parentapp/features/calendar/ParentCalendarRepository.kt
index 2a12e6151d..081e455499 100644
--- a/apps/parent/src/main/java/com/instructure/parentapp/features/calendar/ParentCalendarRepository.kt
+++ b/apps/parent/src/main/java/com/instructure/parentapp/features/calendar/ParentCalendarRepository.kt
@@ -22,14 +22,12 @@ import com.instructure.canvasapi2.apis.FeaturesAPI
import com.instructure.canvasapi2.apis.PlannerAPI
import com.instructure.canvasapi2.builders.RestParams
import com.instructure.canvasapi2.models.CanvasContext
-import com.instructure.canvasapi2.models.Plannable
import com.instructure.canvasapi2.models.PlannableType
import com.instructure.canvasapi2.models.PlannerItem
import com.instructure.canvasapi2.models.toPlannerItems
import com.instructure.canvasapi2.utils.ApiPrefs
import com.instructure.canvasapi2.utils.DataResult
import com.instructure.canvasapi2.utils.depaginate
-import com.instructure.canvasapi2.utils.toDate
import com.instructure.pandautils.features.calendar.CalendarRepository
import com.instructure.pandautils.room.calendar.daos.CalendarFilterDao
import com.instructure.pandautils.room.calendar.entities.CalendarFilterEntity
@@ -47,9 +45,7 @@ class ParentCalendarRepository(
private val featuresApi: FeaturesAPI.FeaturesInterface,
private val parentPrefs: ParentPrefs,
private val calendarFilterDao: CalendarFilterDao
-) : CalendarRepository {
-
- private var canvasContexts: List = emptyList()
+) : CalendarRepository() {
override suspend fun getPlannerItems(
startDate: String,
@@ -72,7 +68,10 @@ class ParentCalendarRepository(
restParams
).depaginate {
calendarEventApi.next(it, restParams)
- }.dataOrThrow.toPlannerItems(PlannableType.CALENDAR_EVENT)
+ }.dataOrThrow
+ .filterNot { it.isHidden }
+ .toPlannerItems(PlannableType.CALENDAR_EVENT)
+ .mapContextName()
}
val calendarAssignments = async {
@@ -85,7 +84,10 @@ class ParentCalendarRepository(
restParams
).depaginate {
calendarEventApi.next(it, restParams)
- }.dataOrThrow.toPlannerItems(PlannableType.ASSIGNMENT)
+ }.dataOrThrow
+ .filterNot { it.isHidden }
+ .toPlannerItems(PlannableType.ASSIGNMENT)
+ .mapContextName()
}
val plannerNotes = async {
@@ -132,32 +134,6 @@ class ParentCalendarRepository(
}
}
- private fun List.toPlannerItems(): List {
- return mapNotNull { plannable ->
- val contextType = if (plannable.courseId != null) CanvasContext.Type.COURSE.apiString else CanvasContext.Type.USER.apiString
- val contextName = if (plannable.courseId != null) canvasContexts.find { it.id == plannable.courseId }?.name else null
- val plannableDate = plannable.todoDate.toDate()
- if (plannableDate == null) {
- null
- } else {
- PlannerItem(
- courseId = plannable.courseId,
- groupId = plannable.groupId,
- userId = plannable.userId,
- contextType = contextType,
- contextName = contextName,
- plannableType = PlannableType.PLANNER_NOTE,
- plannable = plannable,
- plannableDate = plannableDate,
- htmlUrl = null,
- submissionState = null,
- newActivity = false,
- plannerOverride = null
- )
- }
- }
- }
-
override suspend fun getCalendarFilters(): CalendarFilterEntity? {
return calendarFilterDao.findByUserIdAndDomainAndObserveeId(
apiPrefs.user?.id.orDefault(),
diff --git a/apps/parent/src/main/java/com/instructure/parentapp/features/calendar/ParentCalendarRouter.kt b/apps/parent/src/main/java/com/instructure/parentapp/features/calendar/ParentCalendarRouter.kt
index 9532b182e1..3027809ac3 100644
--- a/apps/parent/src/main/java/com/instructure/parentapp/features/calendar/ParentCalendarRouter.kt
+++ b/apps/parent/src/main/java/com/instructure/parentapp/features/calendar/ParentCalendarRouter.kt
@@ -31,7 +31,7 @@ class ParentCalendarRouter(
override fun openNavigationDrawer() = Unit
override fun openAssignment(canvasContext: CanvasContext, assignmentId: Long) {
- // TODO Implement in the assignment details ticket
+ navigation.navigate(activity, navigation.assignmentDetailsRoute(canvasContext.id, assignmentId))
}
override fun openDiscussion(canvasContext: CanvasContext, discussionId: Long) {
diff --git a/apps/parent/src/main/java/com/instructure/parentapp/features/courses/details/CourseDetailsFragment.kt b/apps/parent/src/main/java/com/instructure/parentapp/features/courses/details/CourseDetailsFragment.kt
index 8d0805de3e..a81aa9f527 100644
--- a/apps/parent/src/main/java/com/instructure/parentapp/features/courses/details/CourseDetailsFragment.kt
+++ b/apps/parent/src/main/java/com/instructure/parentapp/features/courses/details/CourseDetailsFragment.kt
@@ -30,14 +30,19 @@ import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import com.instructure.pandautils.utils.ViewStyler
import com.instructure.pandautils.utils.collectOneOffEvents
-import com.instructure.pandautils.utils.color
+import com.instructure.pandautils.utils.studentColor
import com.instructure.parentapp.util.ParentPrefs
+import com.instructure.parentapp.util.navigation.Navigation
import dagger.hilt.android.AndroidEntryPoint
+import javax.inject.Inject
@AndroidEntryPoint
class CourseDetailsFragment : Fragment() {
+ @Inject
+ lateinit var navigation: Navigation
+
private val viewModel: CourseDetailsViewModel by viewModels()
override fun onCreateView(
@@ -58,18 +63,19 @@ class CourseDetailsFragment : Fragment() {
}
private fun applyTheme() {
- val color = ParentPrefs.currentStudent.color
+ val color = ParentPrefs.currentStudent.studentColor
ViewStyler.setStatusBarDark(requireActivity(), color)
}
private fun handleAction(action: CourseDetailsViewModelAction) {
when (action) {
is CourseDetailsViewModelAction.NavigateToComposeMessageScreen -> {
-
+ val route = navigation.inboxComposeRoute(action.options)
+ navigation.navigate(requireActivity(), route)
}
is CourseDetailsViewModelAction.NavigateToAssignmentDetails -> {
-
+ navigation.navigate(activity, navigation.assignmentDetailsRoute(action.courseId, action.assignmentId))
}
}
}
diff --git a/apps/parent/src/main/java/com/instructure/parentapp/features/courses/details/CourseDetailsScreen.kt b/apps/parent/src/main/java/com/instructure/parentapp/features/courses/details/CourseDetailsScreen.kt
index 6712ea2aa7..5f4ed66ef5 100644
--- a/apps/parent/src/main/java/com/instructure/parentapp/features/courses/details/CourseDetailsScreen.kt
+++ b/apps/parent/src/main/java/com/instructure/parentapp/features/courses/details/CourseDetailsScreen.kt
@@ -31,7 +31,9 @@ import androidx.compose.material.Tab
import androidx.compose.material.TabRow
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.graphics.Color
@@ -107,6 +109,12 @@ private fun CourseDetailsScreenContent(
val pagerState = rememberPagerState { uiState.tabs.size }
val coroutineScope = rememberCoroutineScope()
+ LaunchedEffect(pagerState) {
+ snapshotFlow { pagerState.currentPage }.collect { page ->
+ actionHandler(CourseDetailsAction.CurrentTabChanged(uiState.tabs[page]))
+ }
+ }
+
val tabContents: List<@Composable () -> Unit> = uiState.tabs.map {
when (it) {
TabType.GRADES -> {
diff --git a/apps/parent/src/main/java/com/instructure/parentapp/features/courses/details/CourseDetailsUiState.kt b/apps/parent/src/main/java/com/instructure/parentapp/features/courses/details/CourseDetailsUiState.kt
index 7f042c986d..0ff03ca2f0 100644
--- a/apps/parent/src/main/java/com/instructure/parentapp/features/courses/details/CourseDetailsUiState.kt
+++ b/apps/parent/src/main/java/com/instructure/parentapp/features/courses/details/CourseDetailsUiState.kt
@@ -20,6 +20,7 @@ package com.instructure.parentapp.features.courses.details
import android.graphics.Color
import androidx.annotation.ColorInt
import androidx.annotation.StringRes
+import com.instructure.pandautils.features.inbox.utils.InboxComposeOptions
import com.instructure.parentapp.R
@@ -28,7 +29,8 @@ data class CourseDetailsUiState(
@ColorInt val studentColor: Int = Color.BLACK,
val isLoading: Boolean = false,
val isError: Boolean = false,
- val tabs: List = emptyList()
+ val tabs: List = emptyList(),
+ val currentTab: TabType? = null
)
enum class TabType(@StringRes val labelRes: Int) {
@@ -41,10 +43,11 @@ enum class TabType(@StringRes val labelRes: Int) {
sealed class CourseDetailsAction {
data object Refresh : CourseDetailsAction()
data object SendAMessage : CourseDetailsAction()
- data class NavigateToAssignmentDetails(val id: Long) : CourseDetailsAction()
+ data class NavigateToAssignmentDetails(val courseId: Long, val assignmentId: Long) : CourseDetailsAction()
+ data class CurrentTabChanged(val newTab: TabType) : CourseDetailsAction()
}
sealed class CourseDetailsViewModelAction {
- data object NavigateToComposeMessageScreen : CourseDetailsViewModelAction()
- data class NavigateToAssignmentDetails(val id: Long) : CourseDetailsViewModelAction()
+ data class NavigateToComposeMessageScreen(val options: InboxComposeOptions) : CourseDetailsViewModelAction()
+ data class NavigateToAssignmentDetails(val courseId: Long, val assignmentId: Long) : CourseDetailsViewModelAction()
}
diff --git a/apps/parent/src/main/java/com/instructure/parentapp/features/courses/details/CourseDetailsViewModel.kt b/apps/parent/src/main/java/com/instructure/parentapp/features/courses/details/CourseDetailsViewModel.kt
index 3ca0c29f43..869a6214ef 100644
--- a/apps/parent/src/main/java/com/instructure/parentapp/features/courses/details/CourseDetailsViewModel.kt
+++ b/apps/parent/src/main/java/com/instructure/parentapp/features/courses/details/CourseDetailsViewModel.kt
@@ -17,18 +17,24 @@
package com.instructure.parentapp.features.courses.details
+import android.content.Context
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.instructure.canvasapi2.models.Course
import com.instructure.canvasapi2.models.Tab
+import com.instructure.canvasapi2.type.EnrollmentType
+import com.instructure.canvasapi2.utils.ApiPrefs
import com.instructure.canvasapi2.utils.weave.catch
import com.instructure.canvasapi2.utils.weave.tryLaunch
-import com.instructure.pandautils.utils.color
+import com.instructure.pandautils.features.inbox.utils.InboxComposeOptions
import com.instructure.pandautils.utils.orDefault
+import com.instructure.pandautils.utils.studentColor
+import com.instructure.parentapp.R
import com.instructure.parentapp.util.ParentPrefs
import com.instructure.parentapp.util.navigation.Navigation
import dagger.hilt.android.lifecycle.HiltViewModel
+import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
@@ -40,9 +46,11 @@ import javax.inject.Inject
@HiltViewModel
class CourseDetailsViewModel @Inject constructor(
+ @ApplicationContext private val context: Context,
savedStateHandle: SavedStateHandle,
private val repository: CourseDetailsRepository,
- private val parentPrefs: ParentPrefs
+ private val parentPrefs: ParentPrefs,
+ private val apiPrefs: ApiPrefs
) : ViewModel() {
private val courseId = savedStateHandle.get(Navigation.COURSE_ID).orDefault()
@@ -62,7 +70,7 @@ class CourseDetailsViewModel @Inject constructor(
_uiState.update {
it.copy(
isLoading = true,
- studentColor = parentPrefs.currentStudent.color
+ studentColor = parentPrefs.currentStudent.studentColor
)
}
@@ -107,15 +115,62 @@ class CourseDetailsViewModel @Inject constructor(
is CourseDetailsAction.SendAMessage -> {
viewModelScope.launch {
- _events.send(CourseDetailsViewModelAction.NavigateToComposeMessageScreen)
+ _events.send(CourseDetailsViewModelAction.NavigateToComposeMessageScreen(getInboxComposeOptions()))
}
}
is CourseDetailsAction.NavigateToAssignmentDetails -> {
viewModelScope.launch {
- _events.send(CourseDetailsViewModelAction.NavigateToAssignmentDetails(action.id))
+ _events.send(CourseDetailsViewModelAction.NavigateToAssignmentDetails(action.courseId, action.assignmentId))
}
}
+
+ is CourseDetailsAction.CurrentTabChanged -> {
+ viewModelScope.launch {
+ _uiState.update {
+ it.copy(currentTab = action.newTab)
+ }
+ }
+ }
+ }
+ }
+
+ private fun getInboxComposeOptions(): InboxComposeOptions {
+ val courseContextId = Course(courseId).contextId
+ var options = InboxComposeOptions.buildNewMessage()
+ options = options.copy(
+ defaultValues = options.defaultValues.copy(
+ contextCode = courseContextId,
+ contextName = uiState.value.courseName,
+ subject = context.getString(
+ R.string.regardingHiddenMessage,
+ parentPrefs.currentStudent?.shortName.orEmpty(),
+ uiState.value.currentTab?.labelRes?.let { context.getString(it) }.orEmpty()
+ )
+ ),
+ disabledFields = options.disabledFields.copy(
+ isContextDisabled = true
+ ),
+ autoSelectRecipientsFromRoles = listOf(EnrollmentType.TEACHERENROLLMENT),
+ hiddenBodyMessage = context.getString(
+ R.string.regardingHiddenMessage,
+ parentPrefs.currentStudent?.shortName.orEmpty(),
+ getContextURL(courseId)
+ )
+ )
+
+ return options
+ }
+
+ private fun getContextURL(courseId: Long): String {
+ val tabUrlSegment = uiState.value.currentTab?.let { tab ->
+ when (tab) {
+ TabType.GRADES -> "grades"
+ TabType.FRONT_PAGE -> ""
+ TabType.SYLLABUS -> "assignments/syllabus"
+ TabType.SUMMARY -> "assignments/syllabus"
+ }
}
+ return "${apiPrefs.fullDomain}/courses/$courseId/${tabUrlSegment}"
}
}
diff --git a/apps/parent/src/main/java/com/instructure/parentapp/features/courses/details/ParentGradesScreen.kt b/apps/parent/src/main/java/com/instructure/parentapp/features/courses/details/ParentGradesScreen.kt
index 465240d6c3..e92bf4a303 100644
--- a/apps/parent/src/main/java/com/instructure/parentapp/features/courses/details/ParentGradesScreen.kt
+++ b/apps/parent/src/main/java/com/instructure/parentapp/features/courses/details/ParentGradesScreen.kt
@@ -39,7 +39,7 @@ internal fun ParentGradesScreen(
events.collect { action ->
when (action) {
is GradesViewModelAction.NavigateToAssignmentDetails -> {
- actionHandler(CourseDetailsAction.NavigateToAssignmentDetails(action.assignmentId))
+ actionHandler(CourseDetailsAction.NavigateToAssignmentDetails(action.courseId, action.assignmentId))
}
}
}
diff --git a/apps/parent/src/main/java/com/instructure/parentapp/features/courses/list/CoursesViewModel.kt b/apps/parent/src/main/java/com/instructure/parentapp/features/courses/list/CoursesViewModel.kt
index 64bfd88241..23a5c1cdb2 100644
--- a/apps/parent/src/main/java/com/instructure/parentapp/features/courses/list/CoursesViewModel.kt
+++ b/apps/parent/src/main/java/com/instructure/parentapp/features/courses/list/CoursesViewModel.kt
@@ -22,7 +22,8 @@ import androidx.lifecycle.viewModelScope
import com.instructure.canvasapi2.models.User
import com.instructure.canvasapi2.utils.weave.catch
import com.instructure.canvasapi2.utils.weave.tryLaunch
-import com.instructure.pandautils.utils.color
+import com.instructure.pandautils.utils.ColorKeeper
+import com.instructure.pandautils.utils.studentColor
import com.instructure.parentapp.features.dashboard.SelectedStudentHolder
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.channels.Channel
@@ -55,11 +56,25 @@ class CoursesViewModel @Inject constructor(
studentChanged(it)
}
}
+
+ viewModelScope.launch {
+ selectedStudentHolder.selectedStudentColorChanged.collect {
+ updateColor()
+ }
+ }
+ }
+
+ private fun updateColor() {
+ selectedStudent?.let { student ->
+ _uiState.update {
+ it.copy(studentColor = student.studentColor)
+ }
+ }
}
private fun loadCourses(forceRefresh: Boolean = false) {
viewModelScope.tryLaunch {
- val color = selectedStudent.color
+ val color = selectedStudent.studentColor
_uiState.update {
it.copy(
diff --git a/apps/parent/src/main/java/com/instructure/parentapp/features/dashboard/AddStudentItemViewModel.kt b/apps/parent/src/main/java/com/instructure/parentapp/features/dashboard/AddStudentItemViewModel.kt
index a4909add2f..6663ee6422 100644
--- a/apps/parent/src/main/java/com/instructure/parentapp/features/dashboard/AddStudentItemViewModel.kt
+++ b/apps/parent/src/main/java/com/instructure/parentapp/features/dashboard/AddStudentItemViewModel.kt
@@ -16,13 +16,21 @@
*/ package com.instructure.parentapp.features.dashboard
import androidx.annotation.ColorInt
+import androidx.databinding.BaseObservable
+import androidx.databinding.Bindable
import com.instructure.pandautils.mvvm.ItemViewModel
import com.instructure.parentapp.R
+import com.instructure.parentapp.BR
data class AddStudentItemViewModel(
- @ColorInt val color: Int,
+ @Bindable @ColorInt var color: Int,
val onAddStudentClicked: () -> Unit
-) : ItemViewModel {
+) : BaseObservable(), ItemViewModel {
override val viewType: Int = StudentListViewType.ADD_STUDENT.viewType
override val layoutId = R.layout.item_add_student
+
+ fun updateColor(@ColorInt color: Int) {
+ this.color = color
+ notifyPropertyChanged(BR.color)
+ }
}
\ No newline at end of file
diff --git a/apps/parent/src/main/java/com/instructure/parentapp/features/dashboard/DashboardFragment.kt b/apps/parent/src/main/java/com/instructure/parentapp/features/dashboard/DashboardFragment.kt
index 45757d6dbe..4e8a57e3ab 100644
--- a/apps/parent/src/main/java/com/instructure/parentapp/features/dashboard/DashboardFragment.kt
+++ b/apps/parent/src/main/java/com/instructure/parentapp/features/dashboard/DashboardFragment.kt
@@ -36,7 +36,10 @@ import androidx.lifecycle.flowWithLifecycle
import androidx.lifecycle.lifecycleScope
import androidx.navigation.NavController
import androidx.navigation.fragment.NavHostFragment
+import com.google.android.material.bottomnavigation.BottomNavigationView
+import com.google.android.material.navigation.NavigationBarView
import com.google.firebase.crashlytics.FirebaseCrashlytics
+import com.instructure.canvasapi2.models.LaunchDefinition
import com.instructure.canvasapi2.models.User
import com.instructure.loginapi.login.tasks.LogoutTask
import com.instructure.pandautils.features.calendar.CalendarSharedEvents
@@ -47,12 +50,12 @@ import com.instructure.pandautils.utils.ViewStyler
import com.instructure.pandautils.utils.animateCircularBackgroundColorChange
import com.instructure.pandautils.utils.applyTheme
import com.instructure.pandautils.utils.collectOneOffEvents
-import com.instructure.pandautils.utils.color
import com.instructure.pandautils.utils.getDrawableCompat
import com.instructure.pandautils.utils.onClick
import com.instructure.pandautils.utils.setGone
import com.instructure.pandautils.utils.setVisible
import com.instructure.pandautils.utils.showThemed
+import com.instructure.pandautils.utils.studentColor
import com.instructure.pandautils.utils.toPx
import com.instructure.parentapp.R
import com.instructure.parentapp.databinding.FragmentDashboardBinding
@@ -88,11 +91,44 @@ class DashboardFragment : Fragment(), NavigationCallbacks {
private lateinit var navController: NavController
private lateinit var headerLayoutBinding: NavigationDrawerHeaderLayoutBinding
+ private lateinit var bottomNavigationView: BottomNavigationView
private var inboxBadge: TextView? = null
private val addStudentViewModel: AddStudentViewModel by activityViewModels()
+ private val onItemSelectedListener = NavigationBarView.OnItemSelectedListener {
+ when (it.itemId) {
+ R.id.courses -> navigateWithPopBackStack(navigation.courses)
+ R.id.calendar -> navigateWithPopBackStack(navigation.calendar)
+ R.id.alerts -> navigateWithPopBackStack(navigation.alerts)
+ else -> false
+ }
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ val navHostFragment =
+ childFragmentManager.findFragmentById(R.id.nav_host_fragment) as? NavHostFragment
+ navHostFragment?.let {
+ navController = it.navController
+ navController.graph = navigation.createDashboardNavGraph(navController)
+ }
+ }
+
+ private val onDestinationChangedListener = NavController.OnDestinationChangedListener { _, destination, _ ->
+ if (destination.route == navigation.alerts || destination.route == navigation.courses) {
+ binding.todayButtonHolder.setGone()
+ }
+ val menuId = when (destination.route) {
+ navigation.alerts -> R.id.alerts
+ navigation.courses -> R.id.courses
+ navigation.calendar -> R.id.calendar
+ else -> return@OnDestinationChangedListener
+ }
+ bottomNavigationView.menu.findItem(menuId).isChecked = true
+ }
+
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
@@ -136,6 +172,7 @@ class DashboardFragment : Fragment(), NavigationCallbacks {
lifecycleScope.launch {
viewModel.data.flowWithLifecycle(lifecycle, Lifecycle.State.STARTED).collectLatest {
setupNavigationDrawerHeader(it.userViewData)
+ setupLaunchDefinitions(it.launchDefinitionViewData)
setupAppColors(it.selectedStudent)
updateUnreadCount(it.unreadCount)
updateAlertCount(it.alertCount)
@@ -145,6 +182,12 @@ class DashboardFragment : Fragment(), NavigationCallbacks {
lifecycleScope.collectOneOffEvents(viewModel.events, ::handleAction)
}
+ override fun onDestroyView() {
+ super.onDestroyView()
+ bottomNavigationView.setOnItemSelectedListener(null)
+ navController.removeOnDestinationChangedListener(onDestinationChangedListener)
+ }
+
private fun handleAction(action: DashboardViewModelAction) {
when (action) {
is DashboardViewModelAction.AddStudent -> {
@@ -157,6 +200,9 @@ class DashboardFragment : Fragment(), NavigationCallbacks {
firebaseCrashlytics.recordException(e)
}
}
+ is DashboardViewModelAction.OpenLtiTool -> {
+ navigation.navigate(requireActivity(), navigation.ltiLaunchRoute(action.url, action.name))
+ }
}
}
@@ -186,9 +232,12 @@ class DashboardFragment : Fragment(), NavigationCallbacks {
}
private fun setupNavigation() {
- val navHostFragment = childFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment
- navController = navHostFragment.navController
- navController.graph = navigation.createDashboardNavGraph(navController)
+ if (!this::navController.isInitialized) {
+ val navHostFragment =
+ childFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment
+ navController = navHostFragment.navController
+ navController.graph = navigation.createDashboardNavGraph(navController)
+ }
setupToolbar()
setupNavigationDrawer()
@@ -231,6 +280,8 @@ class DashboardFragment : Fragment(), NavigationCallbacks {
when (it.itemId) {
R.id.inbox -> menuItemSelected { navigation.navigate(activity, navigation.inbox) }
R.id.manage_students -> menuItemSelected { navigation.navigate(activity, navigation.manageStudents) }
+ R.id.mastery -> menuItemSelected { viewModel.openMastery() }
+ R.id.studio -> menuItemSelected { viewModel.openStudio() }
R.id.settings -> menuItemSelected { navigation.navigate(activity, navigation.settings) }
R.id.help -> menuItemSelected { activity?.let { HelpDialogFragment.show(it) } }
R.id.log_out -> menuItemSelected { onLogout() }
@@ -246,32 +297,9 @@ class DashboardFragment : Fragment(), NavigationCallbacks {
}
private fun setupBottomNavigationView() {
- val bottomNavigationView = binding.bottomNav
-
- bottomNavigationView.setOnItemSelectedListener {
- when (it.itemId) {
- R.id.courses -> navigateWithPopBackStack(navigation.courses)
- R.id.calendar -> navigateWithPopBackStack(navigation.calendar)
- R.id.alerts -> navigateWithPopBackStack(navigation.alerts)
- else -> false
- }
- }
-
- navController.addOnDestinationChangedListener { _, destination, _ ->
- if (destination.route == navigation.alerts || destination.route == navigation.courses) {
- binding.todayButtonHolder.setGone()
- }
- }
-
- navController.addOnDestinationChangedListener { _, destination, _ ->
- val menuId = when (destination.route) {
- navigation.courses -> R.id.courses
- navigation.calendar -> R.id.calendar
- navigation.alerts -> R.id.alerts
- else -> return@addOnDestinationChangedListener
- }
- bottomNavigationView.menu.findItem(menuId).isChecked = true
- }
+ bottomNavigationView = binding.bottomNav
+ bottomNavigationView.setOnItemSelectedListener(onItemSelectedListener)
+ navController.addOnDestinationChangedListener(onDestinationChangedListener)
}
private fun navigateWithPopBackStack(route: String): Boolean {
@@ -281,7 +309,7 @@ class DashboardFragment : Fragment(), NavigationCallbacks {
}
private fun setupAppColors(student: User?) {
- val color = student.color
+ val color = student.studentColor
if (binding.toolbar.background == null) {
binding.toolbar.setBackgroundColor(color)
} else {
@@ -297,6 +325,7 @@ class DashboardFragment : Fragment(), NavigationCallbacks {
binding.unreadCountBadge.setTextColor(color)
binding.bottomNav.getOrCreateBadge(R.id.alerts).backgroundColor = color
+ viewModel.updateColor(color)
}
private fun openNavigationDrawer() {
@@ -318,13 +347,27 @@ class DashboardFragment : Fragment(), NavigationCallbacks {
ParentLogoutTask(LogoutTask.Type.LOGOUT).execute()
}
.setNegativeButton(android.R.string.cancel, null)
- .showThemed(ParentPrefs.currentStudent.color)
+ .showThemed(ParentPrefs.currentStudent.studentColor)
}
private fun onSwitchUsers() {
ParentLogoutTask(LogoutTask.Type.SWITCH_USERS).execute()
}
+ private fun setupLaunchDefinitions(launchDefinitionViewData: List) {
+ val masteryItem = launchDefinitionViewData.find { it.domain == LaunchDefinition.MASTERY_DOMAIN }
+ if (masteryItem != null) {
+ val masteryMenuItem = binding.navView.menu.findItem(R.id.mastery)
+ masteryMenuItem.isVisible = true
+ }
+
+ val studioItem = launchDefinitionViewData.find { it.domain == LaunchDefinition.STUDIO_DOMAIN }
+ if (studioItem != null) {
+ val studioMenuItem = binding.navView.menu.findItem(R.id.studio)
+ studioMenuItem.isVisible = true
+ }
+ }
+
override fun onHandleBackPressed(): Boolean {
if (binding.drawerLayout.isDrawerOpen(GravityCompat.START)) {
closeNavigationDrawer()
diff --git a/apps/parent/src/main/java/com/instructure/parentapp/features/dashboard/DashboardRepository.kt b/apps/parent/src/main/java/com/instructure/parentapp/features/dashboard/DashboardRepository.kt
index 4b9d28f671..1fc6dd0b83 100644
--- a/apps/parent/src/main/java/com/instructure/parentapp/features/dashboard/DashboardRepository.kt
+++ b/apps/parent/src/main/java/com/instructure/parentapp/features/dashboard/DashboardRepository.kt
@@ -18,8 +18,10 @@
package com.instructure.parentapp.features.dashboard
import com.instructure.canvasapi2.apis.EnrollmentAPI
+import com.instructure.canvasapi2.apis.LaunchDefinitionsAPI
import com.instructure.canvasapi2.apis.UnreadCountAPI
import com.instructure.canvasapi2.builders.RestParams
+import com.instructure.canvasapi2.models.LaunchDefinition
import com.instructure.canvasapi2.models.User
import com.instructure.canvasapi2.utils.depaginate
import com.instructure.pandautils.utils.orDefault
@@ -27,7 +29,8 @@ import com.instructure.pandautils.utils.orDefault
class DashboardRepository(
private val enrollmentApi: EnrollmentAPI.EnrollmentInterface,
- private val unreadCountApi: UnreadCountAPI.UnreadCountsInterface
+ private val unreadCountApi: UnreadCountAPI.UnreadCountsInterface,
+ private val launchDefinitionsApi: LaunchDefinitionsAPI.LaunchDefinitionsInterface
) {
suspend fun getStudents(forceNetwork: Boolean): List {
@@ -46,4 +49,9 @@ class DashboardRepository(
val unreadCount = unreadCountApi.getUnreadConversationCount(params).dataOrNull?.unreadCount ?: "0"
return unreadCount.toIntOrNull().orDefault()
}
+
+ suspend fun getLaunchDefinitions(): List {
+ val params = RestParams(isForceReadFromNetwork = false)
+ return launchDefinitionsApi.getLaunchDefinitions(params).dataOrNull.orEmpty()
+ }
}
diff --git a/apps/parent/src/main/java/com/instructure/parentapp/features/dashboard/DashboardViewData.kt b/apps/parent/src/main/java/com/instructure/parentapp/features/dashboard/DashboardViewData.kt
index a8263937d7..495bebd25f 100644
--- a/apps/parent/src/main/java/com/instructure/parentapp/features/dashboard/DashboardViewData.kt
+++ b/apps/parent/src/main/java/com/instructure/parentapp/features/dashboard/DashboardViewData.kt
@@ -29,6 +29,7 @@ data class DashboardViewData(
val selectedStudent: User? = null,
val unreadCount: Int = 0,
val alertCount: Int = 0,
+ val launchDefinitionViewData: List = emptyList(),
)
data class StudentItemViewData(
@@ -45,9 +46,16 @@ data class UserViewData(
val email: String?
)
+data class LaunchDefinitionViewData(
+ val name: String,
+ val domain: String,
+ val url: String
+)
+
sealed class DashboardViewModelAction {
data object AddStudent : DashboardViewModelAction()
data class NavigateDeepLink(val deepLinkUri: Uri) : DashboardViewModelAction()
+ data class OpenLtiTool(val url: String, val name: String) : DashboardViewModelAction()
}
enum class StudentListViewType(val viewType: Int) {
diff --git a/apps/parent/src/main/java/com/instructure/parentapp/features/dashboard/DashboardViewModel.kt b/apps/parent/src/main/java/com/instructure/parentapp/features/dashboard/DashboardViewModel.kt
index 26546a7e07..535de35954 100644
--- a/apps/parent/src/main/java/com/instructure/parentapp/features/dashboard/DashboardViewModel.kt
+++ b/apps/parent/src/main/java/com/instructure/parentapp/features/dashboard/DashboardViewModel.kt
@@ -20,23 +20,26 @@ package com.instructure.parentapp.features.dashboard
import android.annotation.SuppressLint
import android.content.Context
import android.content.Intent
+import androidx.annotation.ColorInt
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.navigation.NavController.Companion.KEY_DEEP_LINK_INTENT
+import com.instructure.canvasapi2.models.LaunchDefinition
import com.instructure.canvasapi2.models.User
import com.instructure.canvasapi2.utils.ApiPrefs
import com.instructure.canvasapi2.utils.weave.catch
import com.instructure.canvasapi2.utils.weave.tryLaunch
import com.instructure.loginapi.login.util.PreviousUsersUtils
import com.instructure.pandautils.mvvm.ViewState
-import com.instructure.pandautils.utils.color
import com.instructure.pandautils.utils.orDefault
+import com.instructure.pandautils.utils.studentColor
import com.instructure.parentapp.R
import com.instructure.parentapp.features.alerts.list.AlertsRepository
import com.instructure.parentapp.util.ParentPrefs
import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext
+import kotlinx.coroutines.async
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
@@ -116,6 +119,7 @@ class DashboardViewModel @Inject constructor(
viewModelScope.tryLaunch {
_state.value = ViewState.Loading
+ loadLaunchDefinitions()
setupUserInfo()
loadStudents(forceNetwork)
updateUnreadCount()
@@ -137,6 +141,22 @@ class DashboardViewModel @Inject constructor(
}
}
+ private fun loadLaunchDefinitions() {
+ viewModelScope.async {
+ val launchDefinitions = repository.getLaunchDefinitions()
+ val launchDefinitionsViewData = launchDefinitions
+ .filter { it.domain != null && it.placements?.globalNavigation?.url != null }
+ .map { LaunchDefinitionViewData(it.name.orEmpty(), it.domain.orEmpty(), it.placements?.globalNavigation?.url.orEmpty()) }
+ if (launchDefinitionsViewData.isNotEmpty()) {
+ _data.update {
+ it.copy(
+ launchDefinitionViewData = launchDefinitionsViewData
+ )
+ }
+ }
+ }
+ }
+
private fun setupUserInfo() {
apiPrefs.user?.let { user ->
_data.update {
@@ -181,7 +201,7 @@ class DashboardViewModel @Inject constructor(
val studentItemsWithAddStudent = if (studentItems.isNotEmpty()) {
studentItems + AddStudentItemViewModel(
- selectedStudent.color,
+ selectedStudent.studentColor,
::addStudent
)
} else {
@@ -223,7 +243,7 @@ class DashboardViewModel @Inject constructor(
selectedStudent = student,
studentItems = it.studentItems.map { item ->
if (item is AddStudentItemViewModel) {
- item.copy(color = student.color)
+ item.copy(color = student.studentColor)
} else {
item
}
@@ -246,8 +266,8 @@ class DashboardViewModel @Inject constructor(
}
private suspend fun updateAlertCount() {
- _data.value.selectedStudent?.id?.let {
- val alertCount = alertsRepository.getUnreadAlertCount(it)
+ _data.value.selectedStudent?.id?.let { selectedStudentId ->
+ val alertCount = alertsRepository.getUnreadAlertCount(selectedStudentId)
_data.update {
it.copy(
alertCount = alertCount
@@ -261,4 +281,28 @@ class DashboardViewModel @Inject constructor(
it.copy(studentSelectorExpanded = !it.studentSelectorExpanded)
}
}
+
+ fun updateColor(@ColorInt color: Int) {
+ _data.value.studentItems.find { it is AddStudentItemViewModel }?.let { addStudentItem ->
+ (addStudentItem as AddStudentItemViewModel).updateColor(color)
+ }
+ }
+
+ fun openMastery() {
+ val masteryLaunchDefinition = _data.value.launchDefinitionViewData.find { it.domain == LaunchDefinition.MASTERY_DOMAIN }
+ openLtiTool(masteryLaunchDefinition)
+ }
+
+ fun openStudio() {
+ val studioLaunchDefinition = _data.value.launchDefinitionViewData.find { it.domain == LaunchDefinition.STUDIO_DOMAIN }
+ openLtiTool(studioLaunchDefinition)
+ }
+
+ private fun openLtiTool(ltiViewData: LaunchDefinitionViewData?) {
+ ltiViewData?.let {
+ viewModelScope.launch {
+ _events.send(DashboardViewModelAction.OpenLtiTool(it.url, it.name))
+ }
+ }
+ }
}
diff --git a/apps/parent/src/main/java/com/instructure/parentapp/features/dashboard/SelectedStudentHolder.kt b/apps/parent/src/main/java/com/instructure/parentapp/features/dashboard/SelectedStudentHolder.kt
index 0992d1a9e0..dbc291d4c9 100644
--- a/apps/parent/src/main/java/com/instructure/parentapp/features/dashboard/SelectedStudentHolder.kt
+++ b/apps/parent/src/main/java/com/instructure/parentapp/features/dashboard/SelectedStudentHolder.kt
@@ -29,7 +29,9 @@ import kotlinx.coroutines.flow.asStateFlow
interface SelectedStudentHolder {
val selectedStudentState: StateFlow
val selectedStudentChangedFlow: SharedFlow
+ val selectedStudentColorChanged: SharedFlow
suspend fun updateSelectedStudent(user: User)
+ suspend fun selectedStudentColorChanged()
}
class SelectedStudentHolderImpl : SelectedStudentHolder {
@@ -39,8 +41,15 @@ class SelectedStudentHolderImpl : SelectedStudentHolder {
private val _selectedStudentChangedFlow = MutableSharedFlow()
override val selectedStudentChangedFlow: SharedFlow = _selectedStudentChangedFlow.asSharedFlow()
+ private val _selectedStudentColorChanged = MutableSharedFlow()
+ override val selectedStudentColorChanged: SharedFlow = _selectedStudentColorChanged.asSharedFlow()
+
override suspend fun updateSelectedStudent(user: User) {
_selectedStudentState.value = user
_selectedStudentChangedFlow.emit(user)
}
+
+ override suspend fun selectedStudentColorChanged() {
+ _selectedStudentColorChanged.emit(Unit)
+ }
}
diff --git a/apps/parent/src/main/java/com/instructure/parentapp/features/grades/ParentGradesBehaviour.kt b/apps/parent/src/main/java/com/instructure/parentapp/features/grades/ParentGradesBehaviour.kt
index 833551ce77..660a80f3cd 100644
--- a/apps/parent/src/main/java/com/instructure/parentapp/features/grades/ParentGradesBehaviour.kt
+++ b/apps/parent/src/main/java/com/instructure/parentapp/features/grades/ParentGradesBehaviour.kt
@@ -18,7 +18,7 @@
package com.instructure.parentapp.features.grades
import com.instructure.pandautils.features.grades.GradesBehaviour
-import com.instructure.pandautils.utils.color
+import com.instructure.pandautils.utils.studentColor
import com.instructure.parentapp.util.ParentPrefs
@@ -26,5 +26,5 @@ class ParentGradesBehaviour(
parentPrefs: ParentPrefs
) : GradesBehaviour {
- override val canvasContextColor = parentPrefs.currentStudent.color
+ override val canvasContextColor = parentPrefs.currentStudent.studentColor
}
diff --git a/apps/parent/src/main/java/com/instructure/parentapp/features/inbox/coursepicker/ParentInboxCoursePickerBottomSheetDialog.kt b/apps/parent/src/main/java/com/instructure/parentapp/features/inbox/coursepicker/ParentInboxCoursePickerBottomSheetDialog.kt
new file mode 100644
index 0000000000..e539641673
--- /dev/null
+++ b/apps/parent/src/main/java/com/instructure/parentapp/features/inbox/coursepicker/ParentInboxCoursePickerBottomSheetDialog.kt
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2024 - 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.parentapp.features.inbox.coursepicker
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.FrameLayout
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.platform.ComposeView
+import androidx.fragment.app.viewModels
+import androidx.lifecycle.lifecycleScope
+import com.google.android.material.bottomsheet.BottomSheetBehavior
+import com.google.android.material.bottomsheet.BottomSheetDialog
+import com.google.android.material.bottomsheet.BottomSheetDialogFragment
+import com.instructure.pandautils.utils.collectOneOffEvents
+import com.instructure.parentapp.features.inbox.coursepicker.composables.ParentInboxCoursePickerScreen
+import com.instructure.parentapp.util.navigation.Navigation
+import dagger.hilt.android.AndroidEntryPoint
+import javax.inject.Inject
+
+
+@AndroidEntryPoint
+class ParentInboxCoursePickerBottomSheetDialog: BottomSheetDialogFragment() {
+ @Inject
+ lateinit var navigation: Navigation
+
+ private val viewModel: ParentInboxCoursePickerViewModel by viewModels()
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View {
+ viewLifecycleOwner.lifecycleScope.collectOneOffEvents(viewModel.events, ::handleAction)
+
+ return ComposeView(requireContext()).apply {
+ setContent {
+ val uiState by viewModel.uiState.collectAsState()
+ ParentInboxCoursePickerScreen(uiState, viewModel::actionHandler)
+ }
+ }
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+
+ // Landscape fix, make sure the bottom sheet is fully expanded
+ view.viewTreeObserver.addOnGlobalLayoutListener {
+ val dialog = dialog as? BottomSheetDialog
+ dialog?.let {
+ val bottomSheet =
+ dialog.findViewById(com.google.android.material.R.id.design_bottom_sheet) as? FrameLayout
+ bottomSheet?.let {
+ val behavior: BottomSheetBehavior<*> = BottomSheetBehavior.from(bottomSheet)
+ behavior.state = BottomSheetBehavior.STATE_EXPANDED
+ behavior.peekHeight = 0
+ behavior.skipCollapsed = true
+ behavior.isDraggable = false
+ }
+ }
+ }
+ }
+
+ private fun handleAction(action: ParentInboxCoursePickerBottomSheetAction) {
+ when(action) {
+ is ParentInboxCoursePickerBottomSheetAction.NavigateToCompose -> {
+ val route = navigation.inboxComposeRoute(action.options)
+ navigation.navigate(requireActivity(), route)
+ dismiss()
+ }
+ is ParentInboxCoursePickerBottomSheetAction.Dismiss -> dismiss()
+ }
+ }
+}
\ No newline at end of file
diff --git a/apps/parent/src/main/java/com/instructure/parentapp/features/inbox/coursepicker/ParentInboxCoursePickerRepository.kt b/apps/parent/src/main/java/com/instructure/parentapp/features/inbox/coursepicker/ParentInboxCoursePickerRepository.kt
new file mode 100644
index 0000000000..a71a1db028
--- /dev/null
+++ b/apps/parent/src/main/java/com/instructure/parentapp/features/inbox/coursepicker/ParentInboxCoursePickerRepository.kt
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2024 - 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.parentapp.features.inbox.coursepicker
+
+import com.instructure.canvasapi2.apis.CourseAPI
+import com.instructure.canvasapi2.apis.EnrollmentAPI
+import com.instructure.canvasapi2.builders.RestParams
+import com.instructure.canvasapi2.models.Course
+import com.instructure.canvasapi2.models.Enrollment
+import com.instructure.canvasapi2.utils.DataResult
+import com.instructure.canvasapi2.utils.depaginate
+import com.instructure.canvasapi2.utils.hasActiveEnrollment
+import com.instructure.canvasapi2.utils.isValidTerm
+import javax.inject.Inject
+
+class ParentInboxCoursePickerRepository @Inject constructor(
+ private val courseAPI: CourseAPI.CoursesInterface,
+ private val enrollmentApi: EnrollmentAPI.EnrollmentInterface
+) {
+ suspend fun getCourses(): DataResult> {
+ val params = RestParams(usePerPageQueryParam = true)
+
+ val coursesResult = courseAPI.getCoursesByEnrollmentType(Enrollment.EnrollmentType.Observer.apiTypeString, params)
+ .depaginate { nextUrl -> courseAPI.next(nextUrl, params) }
+
+ val courses = coursesResult.dataOrNull ?: return coursesResult
+
+ val validCourses = courses.filter { it.isValidTerm() && it.hasActiveEnrollment() }
+
+ return DataResult.Success(validCourses)
+ }
+
+ suspend fun getEnrollments(): DataResult> {
+ val params = RestParams(usePerPageQueryParam = true)
+ return enrollmentApi.firstPageObserveeEnrollmentsParent(params)
+ .depaginate { nextUrl -> enrollmentApi.getNextPage(nextUrl, params) }
+ }
+}
\ No newline at end of file
diff --git a/apps/parent/src/main/java/com/instructure/parentapp/features/inbox/coursepicker/ParentInboxCoursePickerUiState.kt b/apps/parent/src/main/java/com/instructure/parentapp/features/inbox/coursepicker/ParentInboxCoursePickerUiState.kt
new file mode 100644
index 0000000000..970d4544f7
--- /dev/null
+++ b/apps/parent/src/main/java/com/instructure/parentapp/features/inbox/coursepicker/ParentInboxCoursePickerUiState.kt
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2024 - 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.parentapp.features.inbox.coursepicker
+
+import com.instructure.canvasapi2.models.Course
+import com.instructure.canvasapi2.models.User
+import com.instructure.pandautils.features.inbox.utils.InboxComposeOptions
+
+data class ParentInboxCoursePickerUiState(
+ val screenState: ScreenState = ScreenState.Loading,
+ val studentContextItems: List = emptyList()
+)
+
+data class StudentContextItem(
+ val course: Course,
+ val user: User
+)
+
+sealed class ScreenState {
+ data object Loading: ScreenState()
+ data object Data: ScreenState()
+ data object Error: ScreenState()
+}
+
+sealed class ParentInboxCoursePickerAction {
+ data class StudentContextSelected(val studentContextItem: StudentContextItem): ParentInboxCoursePickerAction()
+ data object CloseDialog: ParentInboxCoursePickerAction()
+}
+
+sealed class ParentInboxCoursePickerBottomSheetAction {
+ data class NavigateToCompose(val options: InboxComposeOptions): ParentInboxCoursePickerBottomSheetAction()
+ data object Dismiss: ParentInboxCoursePickerBottomSheetAction()
+}
\ No newline at end of file
diff --git a/apps/parent/src/main/java/com/instructure/parentapp/features/inbox/coursepicker/ParentInboxCoursePickerViewModel.kt b/apps/parent/src/main/java/com/instructure/parentapp/features/inbox/coursepicker/ParentInboxCoursePickerViewModel.kt
new file mode 100644
index 0000000000..a95fc3ce63
--- /dev/null
+++ b/apps/parent/src/main/java/com/instructure/parentapp/features/inbox/coursepicker/ParentInboxCoursePickerViewModel.kt
@@ -0,0 +1,114 @@
+/*
+ * Copyright (C) 2024 - 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.parentapp.features.inbox.coursepicker
+
+import android.content.Context
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.instructure.canvasapi2.type.EnrollmentType
+import com.instructure.canvasapi2.utils.ApiPrefs
+import com.instructure.pandautils.features.inbox.utils.InboxComposeOptions
+import com.instructure.parentapp.R
+import dagger.hilt.android.lifecycle.HiltViewModel
+import dagger.hilt.android.qualifiers.ApplicationContext
+import kotlinx.coroutines.channels.Channel
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.receiveAsFlow
+import kotlinx.coroutines.flow.update
+import kotlinx.coroutines.launch
+import javax.inject.Inject
+
+@HiltViewModel
+class ParentInboxCoursePickerViewModel @Inject constructor(
+ @ApplicationContext private val context: Context,
+ private val repository: ParentInboxCoursePickerRepository,
+ private val apiPrefs: ApiPrefs
+): ViewModel() {
+
+ private val _uiState = MutableStateFlow(ParentInboxCoursePickerUiState())
+ val uiState = _uiState.asStateFlow()
+
+ private val _events = Channel()
+ val events = _events.receiveAsFlow()
+
+ init {
+ loadCoursePickerItems()
+ }
+
+ private fun getContextURL(courseId: Long): String {
+ return "${apiPrefs.fullDomain}/courses/${courseId}"
+ }
+
+ private fun loadCoursePickerItems() {
+ viewModelScope.launch {
+ _uiState.update { it.copy(screenState = ScreenState.Loading) }
+ val courses = repository.getCourses().dataOrNull
+ val enrollments = repository.getEnrollments().dataOrNull
+
+ if (enrollments == null || courses == null) {
+ _uiState.update { it.copy(screenState = ScreenState.Error) }
+ return@launch
+ }
+
+ val studentContextItems = enrollments.mapNotNull { enrollment ->
+ val course = courses.find { it.id == enrollment.courseId } ?: return@mapNotNull null
+ val user = enrollment.observedUser ?: return@mapNotNull null
+ StudentContextItem(course, user)
+ }
+
+ _uiState.update { it.copy(screenState = ScreenState.Data, studentContextItems = studentContextItems) }
+ }
+ }
+
+ fun actionHandler(action: ParentInboxCoursePickerAction) {
+ when (action) {
+ is ParentInboxCoursePickerAction.StudentContextSelected -> {
+ val options = getMessageOptions(action.studentContextItem)
+ viewModelScope.launch {
+ _events.send(ParentInboxCoursePickerBottomSheetAction.NavigateToCompose(options))
+ }
+ }
+ is ParentInboxCoursePickerAction.CloseDialog -> {
+ viewModelScope.launch {
+ _events.send(ParentInboxCoursePickerBottomSheetAction.Dismiss)
+ }
+ }
+ }
+ }
+
+ private fun getMessageOptions(item: StudentContextItem): InboxComposeOptions {
+ var options = InboxComposeOptions.buildNewMessage()
+ options = options.copy(
+ defaultValues = options.defaultValues.copy(
+ contextCode = item.course.contextId,
+ contextName = item.course.name,
+ subject = item.course.name
+ ),
+ disabledFields = options.disabledFields.copy(
+ isContextDisabled = true
+ ),
+ autoSelectRecipientsFromRoles = listOf(EnrollmentType.TEACHERENROLLMENT),
+ hiddenBodyMessage = context.getString(
+ R.string.regardingHiddenMessage,
+ item.user.name,
+ getContextURL(item.course.id)
+ )
+ )
+ return options
+ }
+}
\ No newline at end of file
diff --git a/apps/parent/src/main/java/com/instructure/parentapp/features/inbox/coursepicker/composables/ParentInboxCoursePickerScreen.kt b/apps/parent/src/main/java/com/instructure/parentapp/features/inbox/coursepicker/composables/ParentInboxCoursePickerScreen.kt
new file mode 100644
index 0000000000..30dd0713d6
--- /dev/null
+++ b/apps/parent/src/main/java/com/instructure/parentapp/features/inbox/coursepicker/composables/ParentInboxCoursePickerScreen.kt
@@ -0,0 +1,147 @@
+/*
+ * Copyright (C) 2024 - 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.parentapp.features.inbox.coursepicker.composables
+
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.heightIn
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
+import androidx.compose.material.Icon
+import androidx.compose.material.IconButton
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalConfiguration
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.colorResource
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import com.instructure.canvasapi2.models.Course
+import com.instructure.canvasapi2.models.User
+import com.instructure.canvasapi2.utils.ContextKeeper
+import com.instructure.pandautils.compose.CanvasTheme
+import com.instructure.parentapp.R
+import com.instructure.parentapp.features.inbox.coursepicker.ParentInboxCoursePickerAction
+import com.instructure.parentapp.features.inbox.coursepicker.ParentInboxCoursePickerUiState
+import com.instructure.parentapp.features.inbox.coursepicker.StudentContextItem
+
+@Composable
+fun ParentInboxCoursePickerScreen(
+ uiState: ParentInboxCoursePickerUiState,
+ actionHandler: (ParentInboxCoursePickerAction) -> Unit
+) {
+ CanvasTheme {
+ Column(
+ modifier = Modifier.padding(horizontal = 12.dp)
+ ) {
+ Row(
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Text(
+ text = stringResource(R.string.chooseACourseToMessage),
+ fontSize = 14.sp,
+ color = colorResource(id = R.color.textDark)
+ )
+
+ Spacer(modifier = Modifier.weight(1f))
+
+ IconButton(onClick = { actionHandler(ParentInboxCoursePickerAction.CloseDialog) }) {
+ Icon(
+ painter = painterResource(id = R.drawable.ic_close),
+ contentDescription = stringResource(id = R.string.close),
+ tint = colorResource(id = R.color.textDarkest)
+ )
+ }
+ }
+
+ LazyColumn(
+ modifier = Modifier
+ .heightIn(min = 0.dp, max = LocalConfiguration.current.screenHeightDp.dp / 2)
+ ) {
+ items(uiState.studentContextItems) { studentContextItem ->
+ StudentContextItemRow(
+ studentContextItem = studentContextItem,
+ onClick = {
+ actionHandler(
+ ParentInboxCoursePickerAction.StudentContextSelected(
+ studentContextItem
+ )
+ )
+ }
+ )
+ }
+ }
+ }
+ }
+}
+
+@Composable
+private fun StudentContextItemRow(
+ studentContextItem: StudentContextItem,
+ onClick: () -> Unit
+) {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .clickable { onClick() }
+ .padding(vertical = 12.dp)
+ ) {
+ Text(
+ text = studentContextItem.course.name,
+ fontSize = 16.sp,
+ color = colorResource(id = R.color.textDarkest)
+ )
+ Text(
+ text = stringResource(
+ R.string.forStudentLabel,
+ studentContextItem.user.shortName ?: studentContextItem.user.name
+ ),
+ fontSize = 14.sp,
+ color = colorResource(id = R.color.textDark)
+ )
+ }
+}
+
+@Preview
+@Composable
+fun ParentInboxCoursePickerScreenPreview() {
+ ContextKeeper.appContext = LocalContext.current
+ ParentInboxCoursePickerScreen(
+ uiState = ParentInboxCoursePickerUiState(
+ studentContextItems = listOf(
+ StudentContextItem(
+ course = Course(name = "Course 1"),
+ user = User(name = "Student 1")
+ ),
+ StudentContextItem(
+ course = Course(name = "Course 2"),
+ user = User(name = "Student 2")
+ )
+ )
+ ),
+ actionHandler = {}
+ )
+}
\ No newline at end of file
diff --git a/apps/parent/src/main/java/com/instructure/parentapp/features/inbox/list/ParentInboxRouter.kt b/apps/parent/src/main/java/com/instructure/parentapp/features/inbox/list/ParentInboxRouter.kt
index 0273d650d4..4405181d95 100644
--- a/apps/parent/src/main/java/com/instructure/parentapp/features/inbox/list/ParentInboxRouter.kt
+++ b/apps/parent/src/main/java/com/instructure/parentapp/features/inbox/list/ParentInboxRouter.kt
@@ -24,6 +24,7 @@ import com.instructure.canvasapi2.models.Conversation
import com.instructure.pandautils.features.inbox.list.InboxRouter
import com.instructure.pandautils.features.inbox.utils.InboxComposeOptions
import com.instructure.pandautils.utils.setupAsBackButton
+import com.instructure.parentapp.features.inbox.coursepicker.ParentInboxCoursePickerBottomSheetDialog
import com.instructure.parentapp.util.navigation.Navigation
import org.greenrobot.eventbus.Subscribe
@@ -40,9 +41,8 @@ class ParentInboxRouter(private val activity: FragmentActivity, private val navi
}
}
- override fun routeToNewMessage() {
- val route = navigation.inboxComposeRoute(InboxComposeOptions.buildNewMessage())
- navigation.navigate(activity, route)
+ override fun routeToNewMessage(activity: FragmentActivity) {
+ ParentInboxCoursePickerBottomSheetDialog().show(activity.supportFragmentManager, "ParentInboxCoursePickerBottomSheetDialog")
}
override fun routeToCompose(options: InboxComposeOptions) {
diff --git a/apps/parent/src/main/java/com/instructure/parentapp/features/lti/LtiLaunchAction.kt b/apps/parent/src/main/java/com/instructure/parentapp/features/lti/LtiLaunchAction.kt
new file mode 100644
index 0000000000..ddc223be46
--- /dev/null
+++ b/apps/parent/src/main/java/com/instructure/parentapp/features/lti/LtiLaunchAction.kt
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2024 - 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.parentapp.features.lti
+
+sealed class LtiLaunchAction {
+ data class LaunchCustomTab(val url: String) : LtiLaunchAction()
+ data object ShowError : LtiLaunchAction()
+}
\ No newline at end of file
diff --git a/apps/parent/src/main/java/com/instructure/parentapp/features/lti/LtiLaunchFragment.kt b/apps/parent/src/main/java/com/instructure/parentapp/features/lti/LtiLaunchFragment.kt
new file mode 100644
index 0000000000..c075e27967
--- /dev/null
+++ b/apps/parent/src/main/java/com/instructure/parentapp/features/lti/LtiLaunchFragment.kt
@@ -0,0 +1,113 @@
+/*
+ * Copyright (C) 2024 - 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.parentapp.features.lti
+
+import android.net.Uri
+import android.os.Bundle
+import android.os.Handler
+import android.os.Looper
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.browser.customtabs.CustomTabColorSchemeParams
+import androidx.browser.customtabs.CustomTabsIntent
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.viewModels
+import androidx.lifecycle.lifecycleScope
+import com.instructure.canvasapi2.utils.validOrNull
+import com.instructure.pandautils.binding.viewBinding
+import com.instructure.pandautils.utils.NullableStringArg
+import com.instructure.pandautils.utils.ThemePrefs
+import com.instructure.pandautils.utils.asChooserExcludingInstructure
+import com.instructure.pandautils.utils.collectOneOffEvents
+import com.instructure.pandautils.utils.setTextForVisibility
+import com.instructure.pandautils.utils.studentColor
+import com.instructure.pandautils.utils.toast
+import com.instructure.parentapp.R
+import com.instructure.parentapp.databinding.FragmentLtiLaunchBinding
+import com.instructure.parentapp.util.ParentPrefs
+import dagger.hilt.android.AndroidEntryPoint
+
+@AndroidEntryPoint
+class LtiLaunchFragment : Fragment() {
+
+ private val binding by viewBinding(FragmentLtiLaunchBinding::bind)
+
+ private val viewModel: LtiLaunchViewModel by viewModels()
+
+ var title: String? by NullableStringArg(key = LTI_TITLE)
+
+ override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
+ return inflater.inflate(R.layout.fragment_lti_launch, container, false)
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ binding.loadingView.setOverrideColor(ParentPrefs.currentStudent?.studentColor ?: ThemePrefs.primaryColor)
+ binding.toolName.setTextForVisibility(title.validOrNull())
+
+ lifecycleScope.collectOneOffEvents(viewModel.events, ::handleAction)
+ }
+
+ private fun handleAction(action: LtiLaunchAction) {
+ when (action) {
+ is LtiLaunchAction.LaunchCustomTab -> {
+ launchCustomTab(action.url)
+ }
+ is LtiLaunchAction.ShowError -> {
+ toast(R.string.errorOccurred)
+ if (activity != null) {
+ requireActivity().onBackPressed()
+ }
+ }
+ }
+ }
+
+ private fun launchCustomTab(url: String) {
+ val uri = Uri.parse(url)
+ .buildUpon()
+ .appendQueryParameter("display", "borderless")
+ .appendQueryParameter("platform", "android")
+ .build()
+
+ val colorSchemeParams = CustomTabColorSchemeParams.Builder()
+ .setToolbarColor(ThemePrefs.primaryColor)
+ .build()
+
+ var intent = CustomTabsIntent.Builder()
+ .setDefaultColorSchemeParams(colorSchemeParams)
+ .setShowTitle(true)
+ .build()
+ .intent
+
+ intent.data = uri
+
+ // Exclude Instructure apps from chooser options
+ intent = intent.asChooserExcludingInstructure()
+
+ requireContext().startActivity(intent)
+ Handler(Looper.getMainLooper()).postDelayed({
+ if (activity == null) return@postDelayed
+ requireActivity().onBackPressed()
+ }, 500)
+ }
+
+ companion object {
+ const val LTI_URL = "lti_url"
+ const val LTI_TITLE = "lti_title"
+ }
+}
\ No newline at end of file
diff --git a/apps/parent/src/main/java/com/instructure/parentapp/features/lti/LtiLaunchRepository.kt b/apps/parent/src/main/java/com/instructure/parentapp/features/lti/LtiLaunchRepository.kt
new file mode 100644
index 0000000000..1cb12a7556
--- /dev/null
+++ b/apps/parent/src/main/java/com/instructure/parentapp/features/lti/LtiLaunchRepository.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2024 - 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.parentapp.features.lti
+
+import com.instructure.canvasapi2.apis.LaunchDefinitionsAPI
+import com.instructure.canvasapi2.builders.RestParams
+import com.instructure.canvasapi2.models.LTITool
+
+class LtiLaunchRepository(
+ private val launchDefinitionsApi: LaunchDefinitionsAPI.LaunchDefinitionsInterface
+) {
+ suspend fun getLtiFromAuthenticationUrl(url: String): LTITool {
+ return launchDefinitionsApi.getLtiFromAuthenticationUrl(url, RestParams(isForceReadFromNetwork = true)).dataOrThrow
+ }
+}
\ No newline at end of file
diff --git a/apps/parent/src/main/java/com/instructure/parentapp/features/lti/LtiLaunchViewModel.kt b/apps/parent/src/main/java/com/instructure/parentapp/features/lti/LtiLaunchViewModel.kt
new file mode 100644
index 0000000000..8730dae52f
--- /dev/null
+++ b/apps/parent/src/main/java/com/instructure/parentapp/features/lti/LtiLaunchViewModel.kt
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2024 - 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.parentapp.features.lti
+
+import androidx.lifecycle.SavedStateHandle
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.instructure.canvasapi2.utils.weave.catch
+import com.instructure.canvasapi2.utils.weave.tryLaunch
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.channels.Channel
+import kotlinx.coroutines.flow.receiveAsFlow
+import kotlinx.coroutines.launch
+import javax.inject.Inject
+
+@HiltViewModel
+class LtiLaunchViewModel @Inject constructor(
+ savedStateHandle: SavedStateHandle,
+ private val repository: LtiLaunchRepository
+) : ViewModel() {
+
+ private val ltiUrl: String? = savedStateHandle.get(LtiLaunchFragment.LTI_URL)
+
+ private val _events = Channel()
+ val events = _events.receiveAsFlow()
+
+ init {
+ loadLtiAuthenticatedUrl()
+ }
+
+ private fun loadLtiAuthenticatedUrl() {
+ viewModelScope.tryLaunch {
+ ltiUrl?.let {
+ val ltiTool = repository.getLtiFromAuthenticationUrl(it)
+ ltiTool.url?.let { url ->
+ _events.send(LtiLaunchAction.LaunchCustomTab(url))
+ } ?: _events.send(LtiLaunchAction.ShowError)
+ }
+ } catch {
+ viewModelScope.launch {
+ _events.send(LtiLaunchAction.ShowError)
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/apps/parent/src/main/java/com/instructure/parentapp/features/main/MainActivity.kt b/apps/parent/src/main/java/com/instructure/parentapp/features/main/MainActivity.kt
index b794d87545..133de6ac91 100644
--- a/apps/parent/src/main/java/com/instructure/parentapp/features/main/MainActivity.kt
+++ b/apps/parent/src/main/java/com/instructure/parentapp/features/main/MainActivity.kt
@@ -19,6 +19,7 @@ package com.instructure.parentapp.features.main
import android.content.Context
import android.content.Intent
+import android.content.res.Configuration
import android.net.Uri
import android.os.Bundle
import android.util.Log
@@ -29,7 +30,9 @@ import androidx.navigation.fragment.NavHostFragment
import com.instructure.pandautils.binding.viewBinding
import com.instructure.pandautils.features.inbox.list.OnUnreadCountInvalidated
import com.instructure.pandautils.interfaces.NavigationCallbacks
+import com.instructure.pandautils.utils.ColorKeeper
import com.instructure.pandautils.utils.Const
+import com.instructure.pandautils.utils.ThemePrefs
import com.instructure.parentapp.R
import com.instructure.parentapp.databinding.ActivityMainBinding
import com.instructure.parentapp.features.dashboard.InboxCountUpdater
@@ -56,12 +59,19 @@ class MainActivity : AppCompatActivity(), OnUnreadCountInvalidated {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(binding.root)
+ setupTheme()
setupNavigation()
}
- override fun onNewIntent(intent: Intent?) {
+ private fun setupTheme() {
+ ThemePrefs.reapplyCanvasTheme(this)
+ val nightModeFlags: Int = resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK
+ ColorKeeper.darkTheme = nightModeFlags == Configuration.UI_MODE_NIGHT_YES
+ }
+
+ override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
- handleDeeplink(intent?.data)
+ handleDeeplink(intent.data)
}
private fun setupNavigation() {
diff --git a/apps/parent/src/main/java/com/instructure/parentapp/features/managestudents/ManageStudentViewModel.kt b/apps/parent/src/main/java/com/instructure/parentapp/features/managestudents/ManageStudentViewModel.kt
index 19b87a1f2a..2070005ad5 100644
--- a/apps/parent/src/main/java/com/instructure/parentapp/features/managestudents/ManageStudentViewModel.kt
+++ b/apps/parent/src/main/java/com/instructure/parentapp/features/managestudents/ManageStudentViewModel.kt
@@ -18,16 +18,15 @@
package com.instructure.parentapp.features.managestudents
import android.content.Context
-import androidx.core.content.ContextCompat
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.instructure.canvasapi2.models.User
import com.instructure.canvasapi2.utils.weave.catch
import com.instructure.canvasapi2.utils.weave.tryLaunch
import com.instructure.pandautils.utils.ColorKeeper
-import com.instructure.pandautils.utils.createThemedColor
import com.instructure.pandautils.utils.orDefault
import com.instructure.parentapp.R
+import com.instructure.parentapp.features.dashboard.SelectedStudentHolder
import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.channels.Channel
@@ -43,7 +42,8 @@ import javax.inject.Inject
class ManageStudentViewModel @Inject constructor(
@ApplicationContext private val context: Context,
private val colorKeeper: ColorKeeper,
- private val repository: ManageStudentsRepository
+ private val repository: ManageStudentsRepository,
+ private val selectedStudentHolder: SelectedStudentHolder
) : ViewModel() {
private val _uiState = MutableStateFlow(ManageStudentsUiState())
@@ -75,7 +75,7 @@ class ManageStudentViewModel @Inject constructor(
colorKeeper.userColors.map {
UserColor(
colorRes = it,
- color = createThemedColor(context.getColor(it)),
+ color = colorKeeper.createThemedColor(context.getColor(it)),
contentDescriptionRes = userColorContentDescriptionMap[it].orDefault()
)
}
@@ -120,7 +120,7 @@ class ManageStudentViewModel @Inject constructor(
private fun saveStudentColor(studentId: Long, selected: UserColor) {
viewModelScope.tryLaunch {
val contextId = "user_$studentId"
- val color = ContextCompat.getColor(context, selected.colorRes)
+ val color = context.getColor(selected.colorRes)
_uiState.update {
it.copy(
@@ -148,6 +148,7 @@ class ManageStudentViewModel @Inject constructor(
} else {
showSavingError()
}
+ selectedStudentHolder.selectedStudentColorChanged()
} catch {
showSavingError()
}
diff --git a/apps/parent/src/main/java/com/instructure/parentapp/features/managestudents/StudentColorPickerDialog.kt b/apps/parent/src/main/java/com/instructure/parentapp/features/managestudents/StudentColorPickerDialog.kt
index 3bea79a5bf..1303bef2d8 100644
--- a/apps/parent/src/main/java/com/instructure/parentapp/features/managestudents/StudentColorPickerDialog.kt
+++ b/apps/parent/src/main/java/com/instructure/parentapp/features/managestudents/StudentColorPickerDialog.kt
@@ -57,7 +57,6 @@ import com.instructure.canvasapi2.utils.ContextKeeper
import com.instructure.pandautils.R
import com.instructure.pandautils.utils.ColorKeeper
import com.instructure.pandautils.utils.ThemePrefs
-import com.instructure.pandautils.utils.createThemedColor
@Composable
@@ -183,7 +182,7 @@ fun StudentColorPickerDialogPreview() {
val colors = ColorKeeper.userColors.map {
UserColor(
colorRes = it,
- color = createThemedColor(context.getColor(it)),
+ color = ColorKeeper.createThemedColor(context.getColor(it)),
contentDescriptionRes = 0
)
}
diff --git a/apps/parent/src/main/java/com/instructure/parentapp/features/splash/SplashAction.kt b/apps/parent/src/main/java/com/instructure/parentapp/features/splash/SplashAction.kt
index fe76d7d5e5..4302195cec 100644
--- a/apps/parent/src/main/java/com/instructure/parentapp/features/splash/SplashAction.kt
+++ b/apps/parent/src/main/java/com/instructure/parentapp/features/splash/SplashAction.kt
@@ -17,9 +17,12 @@
package com.instructure.parentapp.features.splash
+import com.instructure.canvasapi2.models.CanvasTheme
+
sealed class SplashAction {
data object LocaleChanged : SplashAction()
data object InitialDataLoadingFinished : SplashAction()
data object NavigateToNotAParentScreen : SplashAction()
+ data class ApplyTheme(val canvasTheme: CanvasTheme) : SplashAction()
}
diff --git a/apps/parent/src/main/java/com/instructure/parentapp/features/splash/SplashFragment.kt b/apps/parent/src/main/java/com/instructure/parentapp/features/splash/SplashFragment.kt
index 3d3946067b..72fd396258 100644
--- a/apps/parent/src/main/java/com/instructure/parentapp/features/splash/SplashFragment.kt
+++ b/apps/parent/src/main/java/com/instructure/parentapp/features/splash/SplashFragment.kt
@@ -37,6 +37,7 @@ import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import com.instructure.canvasapi2.utils.LocaleUtils
import com.instructure.loginapi.login.view.CanvasLoadingView
+import com.instructure.pandautils.utils.ThemePrefs
import com.instructure.pandautils.utils.collectOneOffEvents
import com.instructure.parentapp.R
import com.instructure.parentapp.util.navigation.Navigation
@@ -92,6 +93,8 @@ class SplashFragment : Fragment() {
findNavController().popBackStack()
navigation.navigate(activity, navigation.notAParent)
}
+
+ is SplashAction.ApplyTheme -> ThemePrefs.applyCanvasTheme(action.canvasTheme, requireContext())
}
}
diff --git a/apps/parent/src/main/java/com/instructure/parentapp/features/splash/SplashViewModel.kt b/apps/parent/src/main/java/com/instructure/parentapp/features/splash/SplashViewModel.kt
index dbb8f658f4..e67060e5ab 100644
--- a/apps/parent/src/main/java/com/instructure/parentapp/features/splash/SplashViewModel.kt
+++ b/apps/parent/src/main/java/com/instructure/parentapp/features/splash/SplashViewModel.kt
@@ -39,8 +39,7 @@ class SplashViewModel @Inject constructor(
@ApplicationContext private val context: Context,
private val repository: SplashRepository,
private val apiPrefs: ApiPrefs,
- private val colorKeeper: ColorKeeper,
- private val themePrefs: ThemePrefs,
+ private val colorKeeper: ColorKeeper
) : ViewModel() {
private val _events = Channel()
@@ -59,7 +58,7 @@ class SplashViewModel @Inject constructor(
colors?.let { colorKeeper.addToCache(it) }
val theme = repository.getTheme()
- theme?.let { themePrefs.applyCanvasTheme(it, context) }
+ theme?.let { _events.send(SplashAction.ApplyTheme(it)) }
val students = repository.getStudents()
if (students.isEmpty()) {
diff --git a/apps/parent/src/main/java/com/instructure/parentapp/util/navigation/Navigation.kt b/apps/parent/src/main/java/com/instructure/parentapp/util/navigation/Navigation.kt
index 739745011b..97b5cbfb6c 100644
--- a/apps/parent/src/main/java/com/instructure/parentapp/util/navigation/Navigation.kt
+++ b/apps/parent/src/main/java/com/instructure/parentapp/util/navigation/Navigation.kt
@@ -14,6 +14,7 @@ import com.instructure.canvasapi2.models.PlannerItem
import com.instructure.canvasapi2.models.ScheduleItem
import com.instructure.canvasapi2.models.User
import com.instructure.canvasapi2.utils.ApiPrefs
+import com.instructure.pandautils.features.assignments.details.AssignmentDetailsFragment
import com.instructure.pandautils.features.calendarevent.createupdate.CreateUpdateEventFragment
import com.instructure.pandautils.features.calendarevent.details.EventFragment
import com.instructure.pandautils.features.calendartodo.createupdate.CreateUpdateToDoFragment
@@ -28,12 +29,14 @@ import com.instructure.pandautils.utils.fromJson
import com.instructure.pandautils.utils.toJson
import com.instructure.parentapp.R
import com.instructure.parentapp.features.addstudent.qr.QrPairingFragment
+import com.instructure.parentapp.features.alerts.details.AnnouncementDetailsFragment
import com.instructure.parentapp.features.alerts.list.AlertsFragment
import com.instructure.parentapp.features.alerts.settings.AlertSettingsFragment
import com.instructure.parentapp.features.calendar.ParentCalendarFragment
import com.instructure.parentapp.features.courses.details.CourseDetailsFragment
import com.instructure.parentapp.features.courses.list.CoursesFragment
import com.instructure.parentapp.features.dashboard.DashboardFragment
+import com.instructure.parentapp.features.lti.LtiLaunchFragment
import com.instructure.parentapp.features.managestudents.ManageStudentsFragment
import com.instructure.parentapp.features.notaparent.NotAParentFragment
import com.instructure.parentapp.features.splash.SplashFragment
@@ -45,6 +48,10 @@ class Navigation(apiPrefs: ApiPrefs) {
private val courseDetails = "$baseUrl/courses/{$COURSE_ID}"
+ private val announcementId = "announcement-id"
+ private val courseAnnouncementDetails = "$baseUrl/courses/{$COURSE_ID}/discussion_topics/{$announcementId}"
+ private val globalAnnouncementDetails = "$baseUrl/account_notifications/{$announcementId}"
+
val splash = "$baseUrl/splash"
val notAParent = "$baseUrl/not-a-parent"
val courses = "$baseUrl/courses"
@@ -55,6 +62,9 @@ class Navigation(apiPrefs: ApiPrefs) {
val qrPairing = "$baseUrl/qr-pairing"
val settings = "$baseUrl/settings"
+ private val assignmentDetails = "$baseUrl/courses/{${Const.COURSE_ID}}/assignments/{${Const.ASSIGNMENT_ID}}"
+ fun assignmentDetailsRoute(courseId: Long, assignmentId: Long) = "$baseUrl/courses/${courseId}/assignments/${assignmentId}"
+
private val inboxCompose = "$baseUrl/conversations/compose/{${InboxComposeOptions.COMPOSE_PARAMETERS}}"
fun inboxComposeRoute(options: InboxComposeOptions) = "$baseUrl/conversations/compose/${InboxComposeOptionsParametersType.serializeAsValue(options)}"
@@ -71,6 +81,8 @@ class Navigation(apiPrefs: ApiPrefs) {
private val updateToDo = "$baseUrl/update-todo/{${CreateUpdateToDoFragment.PLANNER_ITEM}}"
private val alertSettings = "$baseUrl/alert-settings/{${Const.USER}}"
+ private val ltiLaunch = "$baseUrl/lti-launch/{${LtiLaunchFragment.LTI_URL}}/{${LtiLaunchFragment.LTI_TITLE}}"
+
fun courseDetailsRoute(id: Long) = "$baseUrl/courses/$id"
fun calendarEventRoute(contextTypeString: String, contextId: Long, eventId: Long) = "$baseUrl/$contextTypeString/$contextId/calendar_events/$eventId"
@@ -83,6 +95,10 @@ class Navigation(apiPrefs: ApiPrefs) {
fun alertSettingsRoute(student: User) = "$baseUrl/alert-settings/${UserParametersType.serializeAsValue(student)}"
+ fun globalAnnouncementRoute(alertId: Long) = "$baseUrl/account_notifications/$alertId"
+
+ fun ltiLaunchRoute(url: String, title: String) = "$baseUrl/lti-launch/${Uri.encode(url)}/${Uri.encode(title)}"
+
fun crateMainNavGraph(navController: NavController): NavGraph {
return navController.createGraph(
splash
@@ -132,6 +148,22 @@ class Navigation(apiPrefs: ApiPrefs) {
uriPattern = courseDetails
}
}
+ fragment(courseAnnouncementDetails) {
+ argument(AnnouncementDetailsFragment.COURSE_ID) {
+ type = NavType.LongType
+ nullable = false
+ }
+ argument(AnnouncementDetailsFragment.ANNOUNCEMENT_ID) {
+ type = NavType.LongType
+ nullable = false
+ }
+ }
+ fragment(globalAnnouncementDetails) {
+ argument(AnnouncementDetailsFragment.ANNOUNCEMENT_ID) {
+ type = NavType.LongType
+ nullable = false
+ }
+ }
fragment(calendarEvent) {
argument(EventFragment.CONTEXT_TYPE) {
type = NavType.StringType
@@ -179,12 +211,35 @@ class Navigation(apiPrefs: ApiPrefs) {
nullable = false
}
}
+ fragment(assignmentDetails) {
+ argument(Const.COURSE_ID) {
+ type = NavType.LongType
+ nullable = false
+ }
+ argument(Const.ASSIGNMENT_ID) {
+ type = NavType.LongType
+ nullable = false
+ }
+ deepLink {
+ uriPattern = assignmentDetails
+ }
+ }
fragment(alertSettings) {
argument(Const.USER) {
type = UserParametersType
nullable = false
}
}
+ fragment(ltiLaunch) {
+ argument(LtiLaunchFragment.LTI_URL) {
+ type = NavType.StringType
+ nullable = false
+ }
+ argument(LtiLaunchFragment.LTI_TITLE) {
+ type = NavType.StringType
+ nullable = false
+ }
+ }
}
}
@@ -300,4 +355,4 @@ private val UserParametersType = object : NavType(isNullableAllowed = fals
override fun parseValue(value: String): User {
return value.fromJson()
}
-}
+}
\ No newline at end of file
diff --git a/apps/parent/src/main/java/com/instructure/parentapp/util/navigation/ParentWebViewRouter.kt b/apps/parent/src/main/java/com/instructure/parentapp/util/navigation/ParentWebViewRouter.kt
index faf62d1622..eda5798e31 100644
--- a/apps/parent/src/main/java/com/instructure/parentapp/util/navigation/ParentWebViewRouter.kt
+++ b/apps/parent/src/main/java/com/instructure/parentapp/util/navigation/ParentWebViewRouter.kt
@@ -16,6 +16,7 @@
*/
package com.instructure.parentapp.util.navigation
+import android.os.Bundle
import androidx.fragment.app.FragmentActivity
import com.instructure.canvasapi2.models.CanvasContext
import com.instructure.pandautils.navigation.WebViewRouter
@@ -26,7 +27,7 @@ class ParentWebViewRouter(val activity: FragmentActivity) : WebViewRouter {
TODO("Not yet implemented")
}
- override fun routeInternally(url: String) {
+ override fun routeInternally(url: String, extras: Bundle?) {
TODO("Not yet implemented")
}
diff --git a/apps/parent/src/main/res/layout/fragment_lti_launch.xml b/apps/parent/src/main/res/layout/fragment_lti_launch.xml
new file mode 100644
index 0000000000..5a629b386f
--- /dev/null
+++ b/apps/parent/src/main/res/layout/fragment_lti_launch.xml
@@ -0,0 +1,48 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/parent/src/main/res/menu/nav_drawer.xml b/apps/parent/src/main/res/menu/nav_drawer.xml
index c7006e295e..c8a5e1de55 100644
--- a/apps/parent/src/main/res/menu/nav_drawer.xml
+++ b/apps/parent/src/main/res/menu/nav_drawer.xml
@@ -27,6 +27,18 @@
android:contentDescription=""
android:icon="@drawable/ic_group_2"
android:title="@string/screenTitleManageStudents" />
+
+
- (AnnouncementDetailsFragment.ANNOUNCEMENT_ID) } returns 1
+ coEvery { savedStateHandle.get
(AnnouncementDetailsFragment.COURSE_ID) } returns 10
+ coEvery { repository.getCourseAnnouncement(any(), any(), any()) } returns courseAnnouncementTestResponse
+ coEvery { repository.getCourse(any(), any()) } returns courseTestResponse
+ coEvery { repository.getGlobalAnnouncement(any(), any()) } returns globalAnnouncementTestResponse
+ val student = User(id = 55)
+ coEvery { parentPrefs.currentStudent } returns student
+ coEvery { student.studentColor } returns 1
+ coEvery { context.getString(any()) } returns "Global Announcement"
+ }
+
+ @After
+ fun tearDown() {
+ Dispatchers.resetMain()
+ unmockkAll()
+ }
+
+ @Test
+ fun `Having Course id gets course announcement`() = runTest {
+ val expectedUiState = AnnouncementDetailsUiState(
+ studentColor = 1,
+ pageTitle = "Course Name",
+ announcementTitle = "Alert Title",
+ message = "Alert Message",
+ postedDate = Date.from(
+ Instant.parse("2024-01-03T00:00:00Z")
+ ),
+ attachment = Attachment(
+ id = 1,
+ filename = "attachment_file_name",
+ size = 100,
+ displayName = "File Name",
+ thumbnailUrl = "thumbnail_url"
+ )
+ )
+
+ createViewModel()
+ coVerify { repository.getCourse(10, false) }
+ coVerify { repository.getCourseAnnouncement(10, 1, false) }
+ assertEquals(expectedUiState, viewModel.uiState.value)
+ }
+
+ @Test
+ fun `Not having Course id gets global announcement`() = runTest {
+ val expectedUiState = AnnouncementDetailsUiState(
+ studentColor = 1,
+ pageTitle = "Global Announcement",
+ announcementTitle = "Alert Title",
+ message = "Alert Message",
+ postedDate = Date.from(
+ Instant.parse("2024-01-03T00:00:00Z")
+ )
+ )
+
+ coEvery { savedStateHandle.get(AnnouncementDetailsFragment.COURSE_ID) } returns -1
+
+ createViewModel()
+ coVerify(exactly = 0) { repository.getCourse(10, false) }
+ coVerify { repository.getGlobalAnnouncement(1, false) }
+ assertEquals(expectedUiState, viewModel.uiState.value)
+ }
+
+ @Test
+ fun `Success state if getting course returns null`() = runTest {
+ coEvery { repository.getCourse(10, false) } returns null
+ createViewModel()
+ val expectedUiState = AnnouncementDetailsUiState(
+ studentColor = 1,
+ pageTitle = null,
+ announcementTitle = "Alert Title",
+ message = "Alert Message",
+ postedDate = Date.from(
+ Instant.parse("2024-01-03T00:00:00Z")
+ ),
+ attachment = Attachment(
+ id = 1,
+ filename = "attachment_file_name",
+ size = 100,
+ displayName = "File Name",
+ thumbnailUrl = "thumbnail_url"
+ )
+ )
+ assertEquals(expectedUiState, viewModel.uiState.value)
+ }
+
+ @Test
+ fun `Error state if getting course failed`() = runTest {
+ coEvery { repository.getCourse(10, false) } throws Exception()
+ createViewModel()
+ val expectedUiState = AnnouncementDetailsUiState(
+ isError = true,
+ studentColor = 1
+ )
+ assertEquals(expectedUiState, viewModel.uiState.value)
+ }
+
+ @Test
+ fun `Error state if getting course announcement failed`() = runTest {
+ coEvery { repository.getCourseAnnouncement(10, 1, false) } throws Exception()
+ createViewModel()
+ val expectedUiState = AnnouncementDetailsUiState(
+ isError = true,
+ studentColor = 1
+ )
+ assertEquals(expectedUiState, viewModel.uiState.value)
+ }
+
+ @Test
+ fun `Error state if getting global announcement failed`() = runTest {
+ coEvery { savedStateHandle.get(AnnouncementDetailsFragment.COURSE_ID) } returns -1
+ coEvery { repository.getGlobalAnnouncement(1, false) } throws Exception()
+ createViewModel()
+ val expectedUiState = AnnouncementDetailsUiState(
+ isError = true,
+ studentColor = 1
+ )
+ assertEquals(expectedUiState, viewModel.uiState.value)
+ }
+
+ @Test
+ fun `Refresh after get course failed`() = runTest {
+ coEvery { repository.getCourse(10, false) } throws Exception()
+ createViewModel()
+
+ coEvery { repository.getCourse(any(), any()) } returns courseTestResponse
+ viewModel.handleAction(AnnouncementDetailsAction.Refresh)
+
+ val expectedUiStateRefreshed = AnnouncementDetailsUiState(
+ studentColor = 1,
+ pageTitle = "Course Name",
+ announcementTitle = "Alert Title",
+ message = "Alert Message",
+ postedDate = Date.from(
+ Instant.parse("2024-01-03T00:00:00Z")
+ ),
+ attachment = Attachment(
+ id = 1,
+ filename = "attachment_file_name",
+ size = 100,
+ displayName = "File Name",
+ thumbnailUrl = "thumbnail_url"
+ )
+ )
+
+ assertEquals(expectedUiStateRefreshed, viewModel.uiState.value)
+ }
+
+ @Test
+ fun `Refresh after get course announcement failed`() = runTest {
+ coEvery { repository.getCourseAnnouncement(10, 1, false) } throws Exception()
+ createViewModel()
+
+ coEvery {
+ repository.getCourseAnnouncement(
+ 10,
+ 1,
+ false
+ )
+ } returns courseAnnouncementTestResponse
+ viewModel.handleAction(AnnouncementDetailsAction.Refresh)
+
+ val expectedUiStateRefreshed = AnnouncementDetailsUiState(
+ studentColor = 1,
+ pageTitle = "Course Name",
+ announcementTitle = "Alert Title",
+ message = "Alert Message",
+ postedDate = Date.from(
+ Instant.parse("2024-01-03T00:00:00Z")
+ ),
+ attachment = Attachment(
+ id = 1,
+ filename = "attachment_file_name",
+ size = 100,
+ displayName = "File Name",
+ thumbnailUrl = "thumbnail_url"
+ )
+ )
+
+ assertEquals(expectedUiStateRefreshed, viewModel.uiState.value)
+ }
+
+ @Test
+ fun `Refresh after get global announcement failed`() = runTest {
+ coEvery { savedStateHandle.get(AnnouncementDetailsFragment.COURSE_ID) } returns -1
+ coEvery { repository.getGlobalAnnouncement(1, false) } throws Exception()
+ createViewModel()
+
+ coEvery {
+ repository.getGlobalAnnouncement(
+ 1,
+ false
+ )
+ } returns globalAnnouncementTestResponse
+ viewModel.handleAction(AnnouncementDetailsAction.Refresh)
+
+ val expectedUiStateRefreshed = AnnouncementDetailsUiState(
+ studentColor = 1,
+ pageTitle = "Global Announcement",
+ announcementTitle = "Alert Title",
+ message = "Alert Message",
+ postedDate = Date.from(
+ Instant.parse("2024-01-03T00:00:00Z")
+ )
+ )
+
+ assertEquals(expectedUiStateRefreshed, viewModel.uiState.value)
+ }
+
+ @Test
+ fun `When refresh fails while having data, snackbar is shown`() = runTest {
+ createViewModel()
+ coEvery { repository.getCourseAnnouncement(10, 1, true) } throws Exception()
+
+ viewModel.handleAction(AnnouncementDetailsAction.Refresh)
+
+ val expectedUiStateRefreshed = AnnouncementDetailsUiState(
+ studentColor = 1,
+ pageTitle = "Course Name",
+ announcementTitle = "Alert Title",
+ message = "Alert Message",
+ postedDate = Date.from(
+ Instant.parse("2024-01-03T00:00:00Z")
+ ),
+ attachment = Attachment(
+ id = 1,
+ filename = "attachment_file_name",
+ size = 100,
+ displayName = "File Name",
+ thumbnailUrl = "thumbnail_url"
+ ),
+ showErrorSnack = true
+ )
+ assertEquals(expectedUiStateRefreshed, viewModel.uiState.value)
+ }
+
+ @Test
+ fun `Dismiss snackbar`() = runTest {
+ val expectedUiState = AnnouncementDetailsUiState(
+ studentColor = 1,
+ pageTitle = "Course Name",
+ announcementTitle = "Alert Title",
+ message = "Alert Message",
+ postedDate = Date.from(
+ Instant.parse("2024-01-03T00:00:00Z")
+ ),
+ attachment = Attachment(
+ id = 1,
+ filename = "attachment_file_name",
+ size = 100,
+ displayName = "File Name",
+ thumbnailUrl = "thumbnail_url"
+ )
+ )
+
+ createViewModel()
+ coEvery { repository.getCourseAnnouncement(10, 1, true) } throws Exception()
+
+ viewModel.handleAction(AnnouncementDetailsAction.Refresh)
+
+ viewModel.handleAction(AnnouncementDetailsAction.SnackbarDismissed)
+ assertEquals(expectedUiState, viewModel.uiState.value)
+ }
+
+ @Test
+ fun `File download`() = runTest {
+ val expectedUiState = AnnouncementDetailsUiState(
+ studentColor = 1,
+ pageTitle = "Course Name",
+ announcementTitle = "Alert Title",
+ message = "Alert Message",
+ postedDate = Date.from(
+ Instant.parse("2024-01-03T00:00:00Z")
+ ),
+ attachment = Attachment(
+ id = 1,
+ filename = "attachment_file_name",
+ size = 100,
+ displayName = "File Name",
+ thumbnailUrl = "thumbnail_url"
+ )
+ )
+
+ createViewModel()
+ expectedUiState.attachment?.let {
+ viewModel.handleAction(AnnouncementDetailsAction.OpenAttachment(it))
+ coVerify { fileDownloader.downloadFileToDevice(any()) }
+ }
+ }
+}
diff --git a/apps/parent/src/test/java/com/instructure/parentapp/features/alerts/list/AlertsViewModelTest.kt b/apps/parent/src/test/java/com/instructure/parentapp/features/alerts/list/AlertsViewModelTest.kt
index 6b2a857a37..6636800652 100644
--- a/apps/parent/src/test/java/com/instructure/parentapp/features/alerts/list/AlertsViewModelTest.kt
+++ b/apps/parent/src/test/java/com/instructure/parentapp/features/alerts/list/AlertsViewModelTest.kt
@@ -27,8 +27,7 @@ import com.instructure.canvasapi2.models.AlertType
import com.instructure.canvasapi2.models.AlertWorkflowState
import com.instructure.canvasapi2.models.ThresholdWorkflowState
import com.instructure.canvasapi2.models.User
-import com.instructure.pandautils.utils.ColorKeeper
-import com.instructure.pandautils.utils.ThemedColor
+import com.instructure.pandautils.utils.studentColor
import com.instructure.parentapp.R
import com.instructure.parentapp.features.dashboard.AlertCountUpdater
import com.instructure.parentapp.features.dashboard.TestSelectStudentHolder
@@ -36,7 +35,7 @@ import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.every
import io.mockk.mockk
-import io.mockk.mockkObject
+import io.mockk.mockkStatic
import io.mockk.unmockkAll
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -76,9 +75,8 @@ class AlertsViewModelTest {
fun setup() {
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE)
Dispatchers.setMain(testDispatcher)
+ mockkStatic(User::studentColor)
- mockkObject(ColorKeeper)
- every { ColorKeeper.getOrGenerateUserColor(any()) } returns ThemedColor(1, 1)
coEvery { repository.getAlertThresholdForStudent(any(), any()) } returns emptyList()
}
@@ -91,6 +89,7 @@ class AlertsViewModelTest {
@Test
fun `Load alerts on student change`() = runTest {
val student = User(1L)
+ every { student.studentColor } returns 1
val alerts = listOf(
Alert(
@@ -162,6 +161,7 @@ class AlertsViewModelTest {
alerts = alerts.map {
AlertsItemUiState(
alertId = it.id,
+ contextId = it.contextId,
title = it.title,
alertType = it.alertType,
date = it.actionDate,
@@ -180,6 +180,7 @@ class AlertsViewModelTest {
@Test
fun `Empty state`() = runTest {
val student = User(1L)
+ every { student.studentColor } returns 1
coEvery {
repository.getAlertsForStudent(student.id, any())
@@ -217,6 +218,7 @@ class AlertsViewModelTest {
@Test
fun `Error state if getting alerts fail`() = runTest {
val student = User(1L)
+ every { student.studentColor } returns 1
coEvery {
repository.getAlertsForStudent(student.id, any())
@@ -238,6 +240,7 @@ class AlertsViewModelTest {
@Test
fun `Refresh data`() = runTest {
val student = User(1L)
+ every { student.studentColor } returns 1
val alerts = listOf(
Alert(
@@ -281,6 +284,7 @@ class AlertsViewModelTest {
alerts = alerts.map {
AlertsItemUiState(
alertId = it.id,
+ contextId = it.contextId,
title = it.title,
alertType = it.alertType,
date = it.actionDate,
@@ -299,6 +303,7 @@ class AlertsViewModelTest {
@Test
fun `Dismiss alert`() = runTest {
val student = User(1L)
+ every { student.studentColor } returns 1
val alerts = listOf(
Alert(
@@ -331,6 +336,7 @@ class AlertsViewModelTest {
alerts = alerts.map {
AlertsItemUiState(
alertId = it.id,
+ contextId = it.contextId,
title = it.title,
alertType = it.alertType,
date = it.actionDate,
@@ -354,13 +360,20 @@ class AlertsViewModelTest {
viewModel.events.toList(events)
}
- assertEquals(R.string.alertDismissMessage, (events.last() as AlertsViewModelAction.ShowSnackbar).message)
- assertEquals(R.string.alertDismissAction, (events.last() as AlertsViewModelAction.ShowSnackbar).action)
+ assertEquals(
+ R.string.alertDismissMessage,
+ (events.last() as AlertsViewModelAction.ShowSnackbar).message
+ )
+ assertEquals(
+ R.string.alertDismissAction,
+ (events.last() as AlertsViewModelAction.ShowSnackbar).action
+ )
}
@Test
fun `Dismiss error resets event`() = runTest {
val student = User(1L)
+ every { student.studentColor } returns 1
val alerts = listOf(
Alert(
@@ -393,6 +406,7 @@ class AlertsViewModelTest {
alerts = alerts.map {
AlertsItemUiState(
alertId = it.id,
+ contextId = it.contextId,
title = it.title,
alertType = it.alertType,
date = it.actionDate,
@@ -415,13 +429,17 @@ class AlertsViewModelTest {
viewModel.events.toList(events)
}
- assertEquals(R.string.alertDismissErrorMessage, (events.last() as AlertsViewModelAction.ShowSnackbar).message)
+ assertEquals(
+ R.string.alertDismissErrorMessage,
+ (events.last() as AlertsViewModelAction.ShowSnackbar).message
+ )
assertEquals(expected, viewModel.uiState.value)
}
@Test
fun `Undo dismissal`() = runTest {
val student = User(1L)
+ every { student.studentColor } returns 1
val alerts = listOf(
Alert(
@@ -454,6 +472,7 @@ class AlertsViewModelTest {
alerts = alerts.map {
AlertsItemUiState(
alertId = it.id,
+ contextId = it.contextId,
title = it.title,
alertType = it.alertType,
date = it.actionDate,
@@ -477,8 +496,14 @@ class AlertsViewModelTest {
viewModel.events.toList(events)
}
- assertEquals(R.string.alertDismissMessage, (events.last() as AlertsViewModelAction.ShowSnackbar).message)
- assertEquals(R.string.alertDismissAction, (events.last() as AlertsViewModelAction.ShowSnackbar).action)
+ assertEquals(
+ R.string.alertDismissMessage,
+ (events.last() as AlertsViewModelAction.ShowSnackbar).message
+ )
+ assertEquals(
+ R.string.alertDismissAction,
+ (events.last() as AlertsViewModelAction.ShowSnackbar).action
+ )
(events.last() as AlertsViewModelAction.ShowSnackbar).actionCallback?.invoke()
@@ -488,6 +513,7 @@ class AlertsViewModelTest {
@Test
fun `Undo does not reset event on error`() = runTest {
val student = User(1L)
+ every { student.studentColor } returns 1
val alerts = listOf(
Alert(
@@ -520,6 +546,7 @@ class AlertsViewModelTest {
alerts = alerts.map {
AlertsItemUiState(
alertId = it.id,
+ contextId = it.contextId,
title = it.title,
alertType = it.alertType,
date = it.actionDate,
@@ -550,8 +577,9 @@ class AlertsViewModelTest {
}
@Test
- fun `Navigate to URL`() = runTest {
+ fun `Navigate to Course Announcement`() = runTest {
val student = User(1L)
+ every { student.studentColor } returns 1
val alerts = listOf(
Alert(
@@ -561,7 +589,7 @@ class AlertsViewModelTest {
),
title = "Alert 1",
workflowState = AlertWorkflowState.READ,
- alertType = AlertType.ASSIGNMENT_MISSING,
+ alertType = AlertType.COURSE_ANNOUNCEMENT,
htmlUrl = "https://example.com/alert1",
contextId = 1L,
contextType = "Course",
@@ -583,6 +611,79 @@ class AlertsViewModelTest {
alerts = alerts.map {
AlertsItemUiState(
alertId = it.id,
+ contextId = it.contextId,
+ title = it.title,
+ alertType = it.alertType,
+ date = it.actionDate,
+ observerAlertThreshold = null,
+ lockedForUser = it.lockedForUser,
+ unread = it.workflowState == AlertWorkflowState.UNREAD,
+ htmlUrl = it.htmlUrl
+ )
+ }.sortedByDescending { it.date },
+ studentColor = 1
+ )
+
+ assertEquals(expected, viewModel.uiState.value)
+
+ viewModel.handleAction(
+ AlertsAction.Navigate(
+ 1L,
+ 1L,
+ "https://example.com/alert1",
+ AlertType.COURSE_ANNOUNCEMENT
+ )
+ )
+
+ val events = mutableListOf()
+
+ backgroundScope.launch(testDispatcher) {
+ viewModel.events.toList(events)
+ }
+
+ assertEquals(
+ AlertsViewModelAction.NavigateToRoute(
+ "https://example.com/alert1"
+ ), events.last()
+ )
+ }
+
+ @Test
+ fun `Navigate to Global Announcement`() = runTest {
+ val student = User(1L)
+ every { student.studentColor } returns 1
+
+ val alerts = listOf(
+ Alert(
+ id = 1,
+ actionDate = Date.from(
+ Instant.parse("2024-01-03T00:00:00Z")
+ ),
+ title = "Alert 1",
+ workflowState = AlertWorkflowState.READ,
+ alertType = AlertType.INSTITUTION_ANNOUNCEMENT,
+ htmlUrl = "https://example.com/alert1",
+ contextId = 10L,
+ contextType = "Course",
+ lockedForUser = false,
+ observerAlertThresholdId = 1L,
+ observerId = 1L,
+ userId = 2L
+ )
+ )
+
+ coEvery { repository.getAlertsForStudent(student.id, any()) } returns alerts
+
+ createViewModel()
+ selectedStudentFlow.emit(student)
+
+ val expected = AlertsUiState(
+ isLoading = false,
+ isError = false,
+ alerts = alerts.map {
+ AlertsItemUiState(
+ alertId = it.id,
+ contextId = it.contextId,
title = it.title,
alertType = it.alertType,
date = it.actionDate,
@@ -597,7 +698,14 @@ class AlertsViewModelTest {
assertEquals(expected, viewModel.uiState.value)
- viewModel.handleAction(AlertsAction.Navigate(1L, "https://example.com/alert1"))
+ viewModel.handleAction(
+ AlertsAction.Navigate(
+ 1L,
+ 10L,
+ "https://example.com/alert1",
+ AlertType.INSTITUTION_ANNOUNCEMENT
+ )
+ )
val events = mutableListOf()
@@ -605,12 +713,17 @@ class AlertsViewModelTest {
viewModel.events.toList(events)
}
- assertEquals(AlertsViewModelAction.Navigate("https://example.com/alert1"), events.last())
+ assertEquals(
+ AlertsViewModelAction.NavigateToGlobalAnnouncement(
+ 10L
+ ), events.last()
+ )
}
@Test
fun `Navigation to alert marks it read`() = runTest {
val student = User(1L)
+ every { student.studentColor } returns 1
val alerts = listOf(
Alert(
@@ -620,7 +733,7 @@ class AlertsViewModelTest {
),
title = "Alert 1",
workflowState = AlertWorkflowState.UNREAD,
- alertType = AlertType.ASSIGNMENT_MISSING,
+ alertType = AlertType.COURSE_ANNOUNCEMENT,
htmlUrl = "https://example.com/alert1",
contextId = 1L,
contextType = "Course",
@@ -643,6 +756,7 @@ class AlertsViewModelTest {
alerts = alerts.map {
AlertsItemUiState(
alertId = it.id,
+ contextId = it.contextId,
title = it.title,
alertType = it.alertType,
date = it.actionDate,
@@ -655,14 +769,25 @@ class AlertsViewModelTest {
studentColor = 1
)
- viewModel.handleAction(AlertsAction.Navigate(1L, "https://example.com/alert1"))
+ viewModel.handleAction(
+ AlertsAction.Navigate(
+ 1L,
+ 1L,
+ "https://example.com/alert1",
+ AlertType.COURSE_ANNOUNCEMENT
+ )
+ )
val events = mutableListOf()
backgroundScope.launch(testDispatcher) {
viewModel.events.toList(events)
}
- assertEquals(AlertsViewModelAction.Navigate("https://example.com/alert1"), events.last())
+ assertEquals(
+ AlertsViewModelAction.NavigateToRoute(
+ "https://example.com/alert1"
+ ), events.last()
+ )
assertEquals(expected, viewModel.uiState.value)
coVerify {
@@ -673,6 +798,7 @@ class AlertsViewModelTest {
@Test
fun `If marking the alert read fails the alert will remain read until refresh`() = runTest {
val student = User(1L)
+ every { student.studentColor } returns 1
val alerts = listOf(
Alert(
@@ -682,7 +808,7 @@ class AlertsViewModelTest {
),
title = "Alert 1",
workflowState = AlertWorkflowState.UNREAD,
- alertType = AlertType.ASSIGNMENT_MISSING,
+ alertType = AlertType.COURSE_ANNOUNCEMENT,
htmlUrl = "https://example.com/alert1",
contextId = 1L,
contextType = "Course",
@@ -705,6 +831,7 @@ class AlertsViewModelTest {
alerts = alerts.map {
AlertsItemUiState(
alertId = it.id,
+ contextId = it.contextId,
title = it.title,
alertType = it.alertType,
date = it.actionDate,
@@ -717,19 +844,47 @@ class AlertsViewModelTest {
studentColor = 1
)
- viewModel.handleAction(AlertsAction.Navigate(1L, "https://example.com/alert1"))
+ viewModel.handleAction(
+ AlertsAction.Navigate(
+ 1L,
+ 1L,
+ "https://example.com/alert1",
+ AlertType.COURSE_ANNOUNCEMENT
+ )
+ )
val events = mutableListOf()
backgroundScope.launch(testDispatcher) {
viewModel.events.toList(events)
}
- assertEquals(AlertsViewModelAction.Navigate("https://example.com/alert1"), events.last())
+ assertEquals(
+ AlertsViewModelAction.NavigateToRoute(
+ "https://example.com/alert1"
+ ), events.last()
+ )
assertEquals(expected, viewModel.uiState.value)
}
+ @Test
+ fun `Change color when student color is changed`() = runTest {
+ val student = User(1L)
+ mockkStatic(User::studentColor)
+ every { student.studentColor } returns 1
+ createViewModel()
+ selectedStudentFlow.emit(student)
+
+ assertEquals(1, viewModel.uiState.value.studentColor)
+
+ every { student.studentColor } returns 2
+ selectedStudentHolder.selectedStudentColorChanged()
+
+ assertEquals(2, viewModel.uiState.value.studentColor)
+ unmockkAll()
+ }
+
private fun createViewModel() {
viewModel =
AlertsViewModel(repository, selectedStudentHolder, alertCountUpdater)
diff --git a/apps/parent/src/test/java/com/instructure/parentapp/features/assignments/details/ParentAssignmentDetailsColorProviderTest.kt b/apps/parent/src/test/java/com/instructure/parentapp/features/assignments/details/ParentAssignmentDetailsColorProviderTest.kt
new file mode 100644
index 0000000000..1d6df91683
--- /dev/null
+++ b/apps/parent/src/test/java/com/instructure/parentapp/features/assignments/details/ParentAssignmentDetailsColorProviderTest.kt
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2024 - 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.parentapp.features.assignments.details
+
+import com.instructure.canvasapi2.models.Course
+import com.instructure.pandautils.utils.ColorKeeper
+import com.instructure.pandautils.utils.ThemedColor
+import com.instructure.pandautils.utils.studentColor
+import com.instructure.parentapp.features.assignment.details.ParentAssignmentDetailsColorProvider
+import com.instructure.parentapp.util.ParentPrefs
+import io.mockk.every
+import io.mockk.mockk
+import io.mockk.mockkObject
+import io.mockk.unmockkAll
+import junit.framework.TestCase.assertEquals
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+
+class ParentAssignmentDetailsColorProviderTest {
+
+ @Before
+ fun setUp() {
+ mockkObject(ColorKeeper)
+ every { ColorKeeper.getOrGenerateUserColor(any()) } returns ThemedColor(0)
+ }
+
+ @After
+ fun tearDown() {
+ unmockkAll()
+ }
+
+ @Test
+ fun `submissionAndRubricLabelColor should return ThemePrefs textButtonColor`() {
+ val colorKeeper: ColorKeeper = mockk(relaxed = true)
+ val parentPrefs: ParentPrefs = mockk(relaxed = true)
+ every { parentPrefs.currentStudent.studentColor } returns 1
+ val colorProvider = ParentAssignmentDetailsColorProvider(parentPrefs, colorKeeper)
+
+ assertEquals(parentPrefs.currentStudent.studentColor, colorProvider.submissionAndRubricLabelColor)
+ }
+
+ @Test
+ fun `getContentColor should return colorKeeper getOrGenerateColor`() {
+ val colorKeeper: ColorKeeper = mockk(relaxed = true)
+ val parentPrefs: ParentPrefs = mockk(relaxed = true)
+ every { parentPrefs.currentStudent.studentColor } returns 1
+ val colorProvider = ParentAssignmentDetailsColorProvider(parentPrefs, colorKeeper)
+
+ val course = mockk()
+ val expected = ThemedColor(0)
+ val result = colorProvider.getContentColor(course)
+ assertEquals(expected, result)
+ }
+}
\ No newline at end of file
diff --git a/apps/parent/src/test/java/com/instructure/parentapp/features/assignments/details/ParentAssignmentDetailsSubmissionHandlerTest.kt b/apps/parent/src/test/java/com/instructure/parentapp/features/assignments/details/ParentAssignmentDetailsSubmissionHandlerTest.kt
new file mode 100644
index 0000000000..99f69d9ed1
--- /dev/null
+++ b/apps/parent/src/test/java/com/instructure/parentapp/features/assignments/details/ParentAssignmentDetailsSubmissionHandlerTest.kt
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2024 - 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.parentapp.features.assignments.details
+
+import com.instructure.canvasapi2.models.Assignment
+import com.instructure.parentapp.features.assignment.details.ParentAssignmentDetailsSubmissionHandler
+import io.mockk.mockk
+import junit.framework.Assert.assertEquals
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+
+class ParentAssignmentDetailsSubmissionHandlerTest {
+ private val submissionHandler = ParentAssignmentDetailsSubmissionHandler()
+
+ @Test
+ fun testUploadDefaultValues() {
+ assertEquals(false, submissionHandler.isUploading)
+ assertEquals(false, submissionHandler.lastSubmissionIsDraft)
+ assertEquals(null, submissionHandler.lastSubmissionEntry)
+ assertEquals(null, submissionHandler.lastSubmissionAssignmentId)
+ assertEquals(null, submissionHandler.lastSubmissionSubmissionType)
+ }
+
+ @Test
+ fun testUploadDefaultFunctions() = runTest {
+ assertEquals(null, submissionHandler.getVideoUri(mockk(relaxed = true)))
+ val ltiTool = submissionHandler.getStudioLTITool(Assignment(), 0L)
+ assertEquals(null, ltiTool)
+ }
+}
\ No newline at end of file
diff --git a/apps/parent/src/test/java/com/instructure/parentapp/features/calendar/ParentCalendarRepositoryTest.kt b/apps/parent/src/test/java/com/instructure/parentapp/features/calendar/ParentCalendarRepositoryTest.kt
index 9c1f73176e..8735704971 100644
--- a/apps/parent/src/test/java/com/instructure/parentapp/features/calendar/ParentCalendarRepositoryTest.kt
+++ b/apps/parent/src/test/java/com/instructure/parentapp/features/calendar/ParentCalendarRepositoryTest.kt
@@ -24,6 +24,7 @@ import com.instructure.canvasapi2.models.CanvasContext
import com.instructure.canvasapi2.models.Course
import com.instructure.canvasapi2.models.Enrollment
import com.instructure.canvasapi2.models.Plannable
+import com.instructure.canvasapi2.models.PlannableType
import com.instructure.canvasapi2.models.ScheduleItem
import com.instructure.canvasapi2.models.User
import com.instructure.canvasapi2.utils.ApiPrefs
@@ -165,6 +166,76 @@ class ParentCalendarRepositoryTest {
coVerify(exactly = 1) { plannerApi.getPlannerNotes(any(), any(), any(), any()) }
}
+ @Test
+ fun `getPlannerItems filters hidden events`() = runTest {
+ val assignment = ScheduleItem(
+ itemId = "123",
+ title = "assignment",
+ assignment = Assignment(id = 123L, dueAt = LocalDateTime.now().toApiString()),
+ itemType = ScheduleItem.Type.TYPE_ASSIGNMENT,
+ contextCode = "course_1"
+ )
+
+ val assignmentHidden = ScheduleItem(
+ itemId = "124",
+ title = "assignment hidden",
+ assignment = Assignment(id = 124L, dueAt = LocalDateTime.now().toApiString()),
+ itemType = ScheduleItem.Type.TYPE_ASSIGNMENT,
+ contextCode = "course_1",
+ isHidden = true
+ )
+
+ val calendarEvent = ScheduleItem(
+ itemId = "0",
+ title = "calendar event",
+ assignment = null,
+ startAt = LocalDateTime.now().toApiString(),
+ endAt = LocalDateTime.now().toApiString(),
+ itemType = ScheduleItem.Type.TYPE_CALENDAR,
+ contextCode = "course_1"
+ )
+
+ val calendarEventHidden = ScheduleItem(
+ itemId = "1",
+ title = "calendar event hidden",
+ assignment = null,
+ startAt = LocalDateTime.now().toApiString(),
+ endAt = LocalDateTime.now().toApiString(),
+ itemType = ScheduleItem.Type.TYPE_CALENDAR,
+ contextCode = "course_1",
+ isHidden = true
+ )
+
+ coEvery {
+ calendarEventApi.getCalendarEvents(
+ any(),
+ CalendarEventAPI.CalendarEventType.ASSIGNMENT.apiName,
+ any(), any(), any(), any()
+ )
+ } returns DataResult.Success(listOf(assignment, assignmentHidden))
+
+ coEvery {
+ calendarEventApi.getCalendarEvents(
+ any(),
+ CalendarEventAPI.CalendarEventType.CALENDAR.apiName,
+ any(), any(), any(), any()
+ )
+ } returns DataResult.Success(listOf(calendarEvent, calendarEventHidden))
+
+ coEvery { plannerApi.getPlannerNotes(any(), any(), any(), any()) } returns DataResult.Success(emptyList())
+
+ val result = calendarRepository.getPlannerItems("2023-1-1", "2023-1-2", listOf("course_1"), true)
+
+ assertEquals(2, result.size)
+ val assignmentResult = result.find { it.plannableType == PlannableType.ASSIGNMENT }!!
+ val calendarEventResult = result.find { it.plannableType == PlannableType.CALENDAR_EVENT }!!
+ assertEquals(assignment.assignment?.id, assignmentResult.plannable.id)
+ assertEquals(assignment.title, assignmentResult.plannable.title)
+
+ assertEquals(calendarEvent.itemId, calendarEventResult.plannable.id.toString())
+ assertEquals(calendarEvent.title, calendarEventResult.plannable.title)
+ }
+
@Test
fun `getPlannerItems returns empty list when no canvas contexts are given`() = runTest {
val assignment = ScheduleItem(
diff --git a/apps/parent/src/test/java/com/instructure/parentapp/features/courses/details/CourseDetailsViewModelTest.kt b/apps/parent/src/test/java/com/instructure/parentapp/features/courses/details/CourseDetailsViewModelTest.kt
index 61c0cecce0..8ad64e5104 100644
--- a/apps/parent/src/test/java/com/instructure/parentapp/features/courses/details/CourseDetailsViewModelTest.kt
+++ b/apps/parent/src/test/java/com/instructure/parentapp/features/courses/details/CourseDetailsViewModelTest.kt
@@ -17,6 +17,7 @@
package com.instructure.parentapp.features.courses.details
+import android.content.Context
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
@@ -25,8 +26,13 @@ import androidx.lifecycle.SavedStateHandle
import com.instructure.canvasapi2.models.Course
import com.instructure.canvasapi2.models.CourseSettings
import com.instructure.canvasapi2.models.Tab
+import com.instructure.canvasapi2.models.User
+import com.instructure.canvasapi2.type.EnrollmentType
+import com.instructure.canvasapi2.utils.ApiPrefs
+import com.instructure.pandautils.features.inbox.utils.InboxComposeOptions
import com.instructure.pandautils.utils.ColorKeeper
import com.instructure.pandautils.utils.ThemedColor
+import com.instructure.parentapp.R
import com.instructure.parentapp.util.ParentPrefs
import com.instructure.parentapp.util.navigation.Navigation
import io.mockk.coEvery
@@ -62,6 +68,8 @@ class CourseDetailsViewModelTest {
private val savedStateHandle: SavedStateHandle = mockk(relaxed = true)
private val repository: CourseDetailsRepository = mockk(relaxed = true)
private val parentPrefs: ParentPrefs = mockk(relaxed = true)
+ private val apiPrefs: ApiPrefs = mockk(relaxed = true)
+ private val context: Context = mockk(relaxed = true)
private lateinit var viewModel: CourseDetailsViewModel
@@ -72,6 +80,11 @@ class CourseDetailsViewModelTest {
mockkObject(ColorKeeper)
every { ColorKeeper.getOrGenerateUserColor(any()) } returns ThemedColor(1, 1)
coEvery { savedStateHandle.get(Navigation.COURSE_ID) } returns 1
+ coEvery { repository.getCourse(1, any()) } returns Course(id = 1, name = "Course 1")
+ every { parentPrefs.currentStudent } returns User(shortName = "User 1")
+ every { apiPrefs.fullDomain } returns "https://domain.com"
+ every { context.getString(R.string.regardingHiddenMessage, any(), any()) } returns "https://domain.com/courses/1"
+ every { context.getString(R.string.regardingHiddenMessage, any(), "") } returns "Regarding: User 1"
}
@After
@@ -205,9 +218,9 @@ class CourseDetailsViewModelTest {
viewModel.events.toList(events)
}
- viewModel.handleAction(CourseDetailsAction.NavigateToAssignmentDetails(1))
+ viewModel.handleAction(CourseDetailsAction.NavigateToAssignmentDetails(1, 1))
- val expected = CourseDetailsViewModelAction.NavigateToAssignmentDetails(1)
+ val expected = CourseDetailsViewModelAction.NavigateToAssignmentDetails(1, 1)
Assert.assertEquals(expected, events.last())
}
@@ -222,11 +235,30 @@ class CourseDetailsViewModelTest {
viewModel.handleAction(CourseDetailsAction.SendAMessage)
- val expected = CourseDetailsViewModelAction.NavigateToComposeMessageScreen
+ val expected = CourseDetailsViewModelAction.NavigateToComposeMessageScreen(getInboxComposeOptions())
Assert.assertEquals(expected, events.last())
}
private fun createViewModel() {
- viewModel = CourseDetailsViewModel(savedStateHandle, repository, parentPrefs)
+ viewModel = CourseDetailsViewModel(context, savedStateHandle, repository, parentPrefs, apiPrefs)
+ }
+
+ private fun getInboxComposeOptions(): InboxComposeOptions {
+ val courseContextId = Course(1).contextId
+ var options = InboxComposeOptions.buildNewMessage()
+ options = options.copy(
+ defaultValues = options.defaultValues.copy(
+ contextCode = courseContextId,
+ contextName = "Course 1",
+ subject = "Regarding: User 1"
+ ),
+ disabledFields = options.disabledFields.copy(
+ isContextDisabled = true
+ ),
+ autoSelectRecipientsFromRoles = listOf(EnrollmentType.TEACHERENROLLMENT),
+ hiddenBodyMessage = "https://domain.com/courses/1"
+ )
+
+ return options
}
}
diff --git a/apps/parent/src/test/java/com/instructure/parentapp/features/courses/list/CoursesViewModelTest.kt b/apps/parent/src/test/java/com/instructure/parentapp/features/courses/list/CoursesViewModelTest.kt
index dc9fd84412..499a29c85d 100644
--- a/apps/parent/src/test/java/com/instructure/parentapp/features/courses/list/CoursesViewModelTest.kt
+++ b/apps/parent/src/test/java/com/instructure/parentapp/features/courses/list/CoursesViewModelTest.kt
@@ -23,14 +23,13 @@ import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LifecycleRegistry
import com.instructure.canvasapi2.models.Course
import com.instructure.canvasapi2.models.User
-import com.instructure.pandautils.utils.ColorKeeper
-import com.instructure.pandautils.utils.ThemedColor
+import com.instructure.pandautils.utils.studentColor
import com.instructure.parentapp.features.dashboard.TestSelectStudentHolder
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.every
import io.mockk.mockk
-import io.mockk.mockkObject
+import io.mockk.mockkStatic
import io.mockk.unmockkAll
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -42,7 +41,7 @@ import kotlinx.coroutines.test.resetMain
import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.test.setMain
import org.junit.After
-import org.junit.Assert
+import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Rule
import org.junit.Test
@@ -68,9 +67,8 @@ class CoursesViewModelTest {
@Before
fun setup() {
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE)
+ mockkStatic(User::studentColor)
Dispatchers.setMain(testDispatcher)
- mockkObject(ColorKeeper)
- every { ColorKeeper.getOrGenerateUserColor(any()) } returns ThemedColor(1, 1)
}
@After
@@ -84,6 +82,7 @@ class CoursesViewModelTest {
val student = User(1L)
coEvery { repository.getCourses(student.id, any()) } returns listOf(Course(id = 1L, name = "Course 1", courseCode = "code-1"))
every { courseGradeFormatter.getGradeText(any(), any()) } returns "A+"
+ every { student.studentColor } returns 1
createViewModel()
selectedStudentFlow.emit(student)
@@ -102,7 +101,7 @@ class CoursesViewModelTest {
studentColor = 1
)
- Assert.assertEquals(expectedState, viewModel.uiState.value)
+ assertEquals(expectedState, viewModel.uiState.value)
}
@Test
@@ -115,6 +114,7 @@ class CoursesViewModelTest {
)
coEvery { repository.getCourses(any(), any()) } returns courses
every { courseGradeFormatter.getGradeText(any(), any()) } returns "A+"
+ every { student.studentColor } returns 1
createViewModel()
selectedStudentFlow.emit(student)
@@ -133,13 +133,14 @@ class CoursesViewModelTest {
studentColor = 1
)
- Assert.assertEquals(expectedState, viewModel.uiState.value)
+ assertEquals(expectedState, viewModel.uiState.value)
}
@Test
fun `Error load courses`() = runTest {
val student = User(1L)
coEvery { repository.getCourses(student.id, any()) } throws Exception()
+ every { student.studentColor } returns 1
createViewModel()
selectedStudentFlow.emit(student)
@@ -150,13 +151,15 @@ class CoursesViewModelTest {
studentColor = 1
)
- Assert.assertEquals(expectedState, viewModel.uiState.value)
+ assertEquals(expectedState, viewModel.uiState.value)
}
@Test
fun `Refresh reloads courses`() = runTest {
createViewModel()
- selectedStudentHolder.updateSelectedStudent(User(1L))
+ val student = User(1L)
+ selectedStudentHolder.updateSelectedStudent(student)
+ every { student.studentColor } returns 1
viewModel.handleAction(CoursesAction.Refresh)
@@ -175,7 +178,24 @@ class CoursesViewModelTest {
viewModel.handleAction(CoursesAction.CourseTapped(1L))
val expected = CoursesViewModelAction.NavigateToCourseDetails(1L)
- Assert.assertEquals(expected, events.last())
+ assertEquals(expected, events.last())
+ }
+
+ @Test
+ fun `Change color when student color is changed`() = runTest {
+ val student = User(1L)
+ mockkStatic(User::studentColor)
+ every { student.studentColor } returns 1
+ createViewModel()
+ selectedStudentFlow.emit(student)
+
+ assertEquals(1, viewModel.uiState.value.studentColor)
+
+ every { student.studentColor } returns 2
+ selectedStudentHolder.selectedStudentColorChanged()
+
+ assertEquals(2, viewModel.uiState.value.studentColor)
+ unmockkAll()
}
private fun createViewModel() {
diff --git a/apps/parent/src/test/java/com/instructure/parentapp/features/dashboard/DashboardRepositoryTest.kt b/apps/parent/src/test/java/com/instructure/parentapp/features/dashboard/DashboardRepositoryTest.kt
index d1f10889e6..4b1eb481d0 100644
--- a/apps/parent/src/test/java/com/instructure/parentapp/features/dashboard/DashboardRepositoryTest.kt
+++ b/apps/parent/src/test/java/com/instructure/parentapp/features/dashboard/DashboardRepositoryTest.kt
@@ -18,8 +18,10 @@
package com.instructure.parentapp.features.dashboard
import com.instructure.canvasapi2.apis.EnrollmentAPI
+import com.instructure.canvasapi2.apis.LaunchDefinitionsAPI
import com.instructure.canvasapi2.apis.UnreadCountAPI
import com.instructure.canvasapi2.models.Enrollment
+import com.instructure.canvasapi2.models.LaunchDefinition
import com.instructure.canvasapi2.models.UnreadConversationCount
import com.instructure.canvasapi2.models.User
import com.instructure.canvasapi2.utils.DataResult
@@ -35,8 +37,9 @@ class DashboardRepositoryTest {
private val enrollmentApi: EnrollmentAPI.EnrollmentInterface = mockk(relaxed = true)
private val unreadCountApi: UnreadCountAPI.UnreadCountsInterface = mockk(relaxed = true)
+ private val launchDefinitionsApi: LaunchDefinitionsAPI.LaunchDefinitionsInterface = mockk(relaxed = true)
- private val repository = DashboardRepository(enrollmentApi, unreadCountApi)
+ private val repository = DashboardRepository(enrollmentApi, unreadCountApi, launchDefinitionsApi)
@Test
fun `Get students successfully returns data`() = runTest {
@@ -104,4 +107,21 @@ class DashboardRepositoryTest {
val result = repository.getUnreadCounts()
assertEquals(42, result)
}
+
+ @Test
+ fun `Get launch definitions returns empty list when failed`() = runTest {
+ coEvery { launchDefinitionsApi.getLaunchDefinitions(any()) } returns DataResult.Fail()
+
+ val result = repository.getLaunchDefinitions()
+ assertEquals(emptyList(), result)
+ }
+
+ @Test
+ fun `Get launch definitions returns data when successful`() = runTest {
+ val expected = listOf(LaunchDefinition("type", 1, "name", null, null, null, null))
+ coEvery { launchDefinitionsApi.getLaunchDefinitions(any()) } returns DataResult.Success(expected)
+
+ val result = repository.getLaunchDefinitions()
+ assertEquals(expected, result)
+ }
}
diff --git a/apps/parent/src/test/java/com/instructure/parentapp/features/dashboard/DashboardViewModelTest.kt b/apps/parent/src/test/java/com/instructure/parentapp/features/dashboard/DashboardViewModelTest.kt
index a169edf660..400af6bd8d 100644
--- a/apps/parent/src/test/java/com/instructure/parentapp/features/dashboard/DashboardViewModelTest.kt
+++ b/apps/parent/src/test/java/com/instructure/parentapp/features/dashboard/DashboardViewModelTest.kt
@@ -19,7 +19,6 @@ package com.instructure.parentapp.features.dashboard
import android.content.Context
import android.content.Intent
-import android.graphics.Color
import android.net.Uri
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import androidx.lifecycle.Lifecycle
@@ -27,14 +26,15 @@ import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LifecycleRegistry
import androidx.lifecycle.SavedStateHandle
import androidx.navigation.NavController.Companion.KEY_DEEP_LINK_INTENT
+import com.instructure.canvasapi2.models.LaunchDefinition
+import com.instructure.canvasapi2.models.Placement
+import com.instructure.canvasapi2.models.Placements
import com.instructure.canvasapi2.models.User
import com.instructure.canvasapi2.utils.ApiPrefs
import com.instructure.canvasapi2.utils.ContextKeeper
import com.instructure.loginapi.login.model.SignedInUser
import com.instructure.loginapi.login.util.PreviousUsersUtils
import com.instructure.pandautils.mvvm.ViewState
-import com.instructure.pandautils.utils.ColorKeeper
-import com.instructure.pandautils.utils.ThemedColor
import com.instructure.parentapp.R
import com.instructure.parentapp.features.alerts.list.AlertsRepository
import com.instructure.parentapp.util.ParentPrefs
@@ -42,7 +42,6 @@ import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.every
import io.mockk.mockk
-import io.mockk.mockkObject
import io.mockk.unmockkAll
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -90,8 +89,6 @@ class DashboardViewModelTest {
@Before
fun setup() {
every { savedStateHandle.get(KEY_DEEP_LINK_INTENT) } returns null
- mockkObject(ColorKeeper)
- every { ColorKeeper.getOrGenerateUserColor(any()) } returns ThemedColor(Color.BLUE)
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE)
Dispatchers.setMain(testDispatcher)
ContextKeeper.appContext = context
@@ -275,6 +272,97 @@ class DashboardViewModelTest {
assertEquals(DashboardViewModelAction.NavigateDeepLink(uri), events.first())
}
+ @Test
+ fun `Update color updates add student item color`() {
+ val students = listOf(User(id = 1L), User(id = 2L))
+ coEvery { repository.getStudents(any()) } returns students
+
+ createViewModel()
+
+ val items = viewModel.data.value.studentItems
+ viewModel.updateColor(123)
+
+ assertEquals(123, (items[2] as AddStudentItemViewModel).color)
+ }
+
+ @Test
+ fun `Update date with launch definitions when launch definitions are received`() = runTest {
+ val students = listOf(User(id = 1L), User(id = 2L))
+ coEvery { repository.getStudents(any()) } returns students
+ coEvery { repository.getLaunchDefinitions() } returns listOf(
+ LaunchDefinition("type", 1, "name", null, "domain",
+ Placements(Placement("", "global.url", "")), null)
+ )
+
+ createViewModel()
+
+ assertEquals(1, viewModel.data.value.launchDefinitionViewData.size)
+ val launchDefinition = viewModel.data.value.launchDefinitionViewData.first()
+ assertEquals("name", launchDefinition.name)
+ assertEquals("domain", launchDefinition.domain)
+ assertEquals("global.url", launchDefinition.url)
+ }
+
+ @Test
+ fun `Do not update launch definitions when url or domain is null`() = runTest {
+ val students = listOf(User(id = 1L), User(id = 2L))
+ coEvery { repository.getStudents(any()) } returns students
+ coEvery { repository.getLaunchDefinitions() } returns listOf(
+ LaunchDefinition("type", 1, "name", null, "domain",
+ Placements(null), null),
+ LaunchDefinition("type", 1, "name", null, null,
+ Placements(Placement("", "global.url", "")), null)
+ )
+
+ createViewModel()
+
+ assertEquals(0, viewModel.data.value.launchDefinitionViewData.size)
+ }
+
+ @Test
+ fun `Open Mastery sends correct open LTI event`() = runTest {
+ val students = listOf(User(id = 1L), User(id = 2L))
+ coEvery { repository.getStudents(any()) } returns students
+ coEvery { repository.getLaunchDefinitions() } returns listOf(
+ LaunchDefinition("type", 1, "name", null, LaunchDefinition.MASTERY_DOMAIN,
+ Placements(Placement("", "global.url", "")), null)
+ )
+
+ createViewModel()
+
+ val events = mutableListOf()
+
+ backgroundScope.launch(testDispatcher) {
+ viewModel.events.toList(events)
+ }
+
+ viewModel.openMastery()
+
+ assertEquals(DashboardViewModelAction.OpenLtiTool("global.url", "name"), events.first())
+ }
+
+ @Test
+ fun `Open Studio sends correct open LTI event`() = runTest {
+ val students = listOf(User(id = 1L), User(id = 2L))
+ coEvery { repository.getStudents(any()) } returns students
+ coEvery { repository.getLaunchDefinitions() } returns listOf(
+ LaunchDefinition("type", 1, "name", null, LaunchDefinition.STUDIO_DOMAIN,
+ Placements(Placement("", "global.url", "")), null)
+ )
+
+ createViewModel()
+
+ val events = mutableListOf()
+
+ backgroundScope.launch(testDispatcher) {
+ viewModel.events.toList(events)
+ }
+
+ viewModel.openStudio()
+
+ assertEquals(DashboardViewModelAction.OpenLtiTool("global.url", "name"), events.first())
+ }
+
private fun createViewModel() {
viewModel = DashboardViewModel(
context = context,
diff --git a/apps/parent/src/test/java/com/instructure/parentapp/features/dashboard/TestSelectStudentHolder.kt b/apps/parent/src/test/java/com/instructure/parentapp/features/dashboard/TestSelectStudentHolder.kt
index ae5bceb205..d4758c4712 100644
--- a/apps/parent/src/test/java/com/instructure/parentapp/features/dashboard/TestSelectStudentHolder.kt
+++ b/apps/parent/src/test/java/com/instructure/parentapp/features/dashboard/TestSelectStudentHolder.kt
@@ -25,9 +25,14 @@ import kotlinx.coroutines.flow.SharedFlow
class TestSelectStudentHolder(
override val selectedStudentState: MutableStateFlow,
- override val selectedStudentChangedFlow: SharedFlow = MutableSharedFlow()
+ override val selectedStudentChangedFlow: SharedFlow = MutableSharedFlow(),
+ override val selectedStudentColorChanged: SharedFlow = MutableSharedFlow()
) : SelectedStudentHolder {
override suspend fun updateSelectedStudent(user: User) {
selectedStudentState.emit(user)
}
+
+ override suspend fun selectedStudentColorChanged() {
+ (selectedStudentColorChanged as MutableSharedFlow).emit(Unit)
+ }
}
diff --git a/apps/parent/src/test/java/com/instructure/parentapp/features/inbox/coursepicker/ParentInboxCoursePickerRepositoryTest.kt b/apps/parent/src/test/java/com/instructure/parentapp/features/inbox/coursepicker/ParentInboxCoursePickerRepositoryTest.kt
new file mode 100644
index 0000000000..2e5c40f426
--- /dev/null
+++ b/apps/parent/src/test/java/com/instructure/parentapp/features/inbox/coursepicker/ParentInboxCoursePickerRepositoryTest.kt
@@ -0,0 +1,136 @@
+/*
+ * Copyright (C) 2024 - 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.parentapp.features.inbox.coursepicker
+
+import com.instructure.canvasapi2.apis.CourseAPI
+import com.instructure.canvasapi2.apis.EnrollmentAPI
+import com.instructure.canvasapi2.models.Course
+import com.instructure.canvasapi2.models.Enrollment
+import com.instructure.canvasapi2.models.Term
+import com.instructure.canvasapi2.utils.DataResult
+import com.instructure.canvasapi2.utils.LinkHeaders
+import io.mockk.coEvery
+import io.mockk.mockk
+import io.mockk.unmockkAll
+import junit.framework.TestCase.assertEquals
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.test.resetMain
+import kotlinx.coroutines.test.runTest
+import org.junit.After
+import org.junit.Test
+import java.util.Date
+
+class ParentInboxCoursePickerRepositoryTest {
+ private val courseAPI: CourseAPI.CoursesInterface = mockk(relaxed = true)
+ private val enrollmentAPI: EnrollmentAPI.EnrollmentInterface = mockk(relaxed = true)
+ private val repository = ParentInboxCoursePickerRepository(courseAPI, enrollmentAPI)
+
+ @After
+ fun tearDown() {
+ Dispatchers.resetMain()
+ unmockkAll()
+ }
+
+ @Test
+ fun `Test get Courses successfully`() = runTest {
+ val expectedCourses = listOf(
+ Course(id = 1L, enrollments = mutableListOf(Enrollment(enrollmentState = EnrollmentAPI.STATE_ACTIVE)), term = Term(endAt = Date(Date().time + 10000).toString())),
+ Course(id = 2L, enrollments = mutableListOf(Enrollment(enrollmentState = EnrollmentAPI.STATE_ACTIVE)), term = Term(endAt = Date(Date().time + 10000).toString())),
+ )
+ coEvery { courseAPI.getCoursesByEnrollmentType(any(), any()) } returns DataResult.Success(expectedCourses)
+
+ val result = repository.getCourses()
+
+ assertEquals(expectedCourses, result.dataOrNull)
+ }
+
+ @Test
+ fun `Test get Courses successfully with pagination`() = runTest {
+ val expectedCourses = listOf(
+ Course(id = 1L, enrollments = mutableListOf(Enrollment(enrollmentState = EnrollmentAPI.STATE_ACTIVE)), term = Term(endAt = Date(Date().time + 10000).toString())),
+ Course(id = 2L, enrollments = mutableListOf(Enrollment(enrollmentState = EnrollmentAPI.STATE_ACTIVE)), term = Term(endAt = Date(Date().time + 10000).toString())),
+ Course(id = 3L, enrollments = mutableListOf(Enrollment(enrollmentState = EnrollmentAPI.STATE_ACTIVE)), term = Term(endAt = Date(Date().time + 10000).toString())),
+ )
+ coEvery { courseAPI.getCoursesByEnrollmentType(any(), any()) } returns DataResult.Success(expectedCourses.take(2), LinkHeaders(nextUrl = "nextUrl"))
+ coEvery { courseAPI.next(any(), any()) } returns DataResult.Success(expectedCourses.takeLast(1))
+
+ val result = repository.getCourses()
+
+ assertEquals(expectedCourses, result.dataOrNull)
+ }
+
+ @Test
+ fun `Test get Courses successfully with filtering`() = runTest {
+ val expectedCourses = listOf(
+ Course(id = 1L, enrollments = mutableListOf(Enrollment(enrollmentState = EnrollmentAPI.STATE_ACTIVE)), term = Term(endAt = Date(Date().time + 10000).toString())),
+ Course(id = 2L, enrollments = mutableListOf(Enrollment(enrollmentState = EnrollmentAPI.STATE_ACTIVE))),
+ Course(id = 3L, term = Term(endAt = Date(Date().time + 10000).toString())),
+ Course(id = 5L),
+ )
+ coEvery { courseAPI.getCoursesByEnrollmentType(any(), any()) } returns DataResult.Success(expectedCourses)
+
+ val result = repository.getCourses()
+
+ assertEquals(expectedCourses.take(2), result.dataOrNull)
+ }
+
+ @Test
+ fun `Test get Courses failed`() = runTest {
+ coEvery { courseAPI.getCoursesByEnrollmentType(any(), any()) } returns DataResult.Fail()
+
+ val result = repository.getCourses()
+
+ assertEquals(result, DataResult.Fail())
+ }
+
+ @Test
+ fun `Test get Enrollments successfully`() = runTest {
+ val expectedEnrollments = listOf(
+ Enrollment(id = 1L),
+ Enrollment(id = 2L),
+ )
+ coEvery { enrollmentAPI.firstPageObserveeEnrollmentsParent(any()) } returns DataResult.Success(expectedEnrollments)
+
+ val result = repository.getEnrollments()
+
+ assertEquals(expectedEnrollments, result.dataOrNull)
+ }
+
+ @Test
+ fun `Test get Enrollments successfully with pagination`() = runTest {
+ val expectedEnrollments = listOf(
+ Enrollment(id = 1L),
+ Enrollment(id = 2L),
+ Enrollment(id = 3L),
+ )
+ coEvery { enrollmentAPI.firstPageObserveeEnrollmentsParent(any()) } returns DataResult.Success(expectedEnrollments.take(2), LinkHeaders(nextUrl = "nextUrl"))
+ coEvery { enrollmentAPI.getNextPage(any(), any()) } returns DataResult.Success(expectedEnrollments.takeLast(1))
+
+ val result = repository.getEnrollments()
+
+ assertEquals(expectedEnrollments, result.dataOrNull)
+ }
+
+ @Test
+ fun `Test get Enrollments failed`() = runTest {
+ coEvery { enrollmentAPI.firstPageObserveeEnrollmentsParent(any()) } returns DataResult.Fail()
+
+ val result = repository.getEnrollments()
+
+ assertEquals(result, DataResult.Fail())
+ }
+}
\ No newline at end of file
diff --git a/apps/parent/src/test/java/com/instructure/parentapp/features/inbox/coursepicker/ParentInboxCoursePickerViewModelTest.kt b/apps/parent/src/test/java/com/instructure/parentapp/features/inbox/coursepicker/ParentInboxCoursePickerViewModelTest.kt
new file mode 100644
index 0000000000..dd67a58b2d
--- /dev/null
+++ b/apps/parent/src/test/java/com/instructure/parentapp/features/inbox/coursepicker/ParentInboxCoursePickerViewModelTest.kt
@@ -0,0 +1,117 @@
+/*
+ * Copyright (C) 2024 - 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.parentapp.features.inbox.coursepicker
+
+import android.content.Context
+import com.instructure.canvasapi2.models.Course
+import com.instructure.canvasapi2.models.Enrollment
+import com.instructure.canvasapi2.models.User
+import com.instructure.canvasapi2.type.EnrollmentType
+import com.instructure.canvasapi2.utils.ApiPrefs
+import com.instructure.canvasapi2.utils.DataResult
+import com.instructure.parentapp.R
+import io.mockk.coEvery
+import io.mockk.every
+import io.mockk.mockk
+import io.mockk.unmockkAll
+import junit.framework.TestCase.assertEquals
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.toList
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.resetMain
+import kotlinx.coroutines.test.runTest
+import kotlinx.coroutines.test.setMain
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+
+@OptIn(ExperimentalCoroutinesApi::class)
+class ParentInboxCoursePickerViewModelTest {
+ private val testDispatcher = UnconfinedTestDispatcher()
+ private val repository: ParentInboxCoursePickerRepository = mockk(relaxed = true)
+ private val apiPrefs: ApiPrefs = mockk(relaxed = true)
+ private val context: Context = mockk(relaxed = true)
+
+ @Before
+ fun setup() {
+ Dispatchers.setMain(testDispatcher)
+ }
+
+ @After
+ fun tearDown() {
+ Dispatchers.resetMain()
+ unmockkAll()
+ }
+
+ @Test
+ fun `Selecting a course navigates to the compose screen with proper attributes`() = runTest {
+ coEvery { repository.getCourses() } returns DataResult.Success(emptyList())
+ coEvery { repository.getEnrollments() } returns DataResult.Success(emptyList())
+ every { apiPrefs.fullDomain } returns "https://canvas.instructure.com"
+ val studentContextItem = StudentContextItem(Course(1, "Course 1"), User(1, "User 1"))
+ val viewModel = getViewModel()
+ val courseId = 123L
+ val expectedHiddenMessage = "Regarding: User 1, https://canvas.instructure.com/courses/$courseId"
+ every { context.getString(R.string.regardingHiddenMessage, any(), any()) } returns expectedHiddenMessage
+
+ val events = mutableListOf()
+ backgroundScope.launch(testDispatcher) {
+ viewModel.events.toList(events)
+ }
+
+ viewModel.actionHandler(ParentInboxCoursePickerAction.StudentContextSelected(studentContextItem))
+ val options = (events.last() as ParentInboxCoursePickerBottomSheetAction.NavigateToCompose).options
+ assertEquals(expectedHiddenMessage, options.hiddenBodyMessage)
+ assertEquals("Course 1", options.defaultValues.contextName)
+ assertEquals("course_1", options.defaultValues.contextCode)
+ assertEquals("Course 1", options.defaultValues.subject)
+ assertEquals(true, options.disabledFields.isContextDisabled)
+ assertEquals(listOf(EnrollmentType.TEACHERENROLLMENT), options.autoSelectRecipientsFromRoles)
+ }
+
+ @Test
+ fun `loadCoursePickerItems should update uiState with error when enrollments or courses fail`() {
+ coEvery { repository.getCourses() } returns DataResult.Fail()
+ coEvery { repository.getEnrollments() } returns DataResult.Fail()
+
+ val viewModel = getViewModel()
+ assertEquals(ScreenState.Error, viewModel.uiState.value.screenState)
+ }
+
+ @Test
+ fun `loadCoursePickerItems should update uiState with data when enrollments and courses are successful`() {
+ val courses = listOf(Course(1, "Course 1"), Course(2, "Course 2"))
+ val users = listOf(User(1, "User 1"), User(2, "User 2"))
+ val enrollments = listOf(Enrollment(1, courseId = 1, observedUser = users[0]), Enrollment(2, courseId = 2, observedUser = users[1]))
+ coEvery { repository.getCourses() } returns DataResult.Success(courses)
+ coEvery { repository.getEnrollments() } returns DataResult.Success(enrollments)
+
+ val viewModel = getViewModel()
+ assertEquals(ScreenState.Data, viewModel.uiState.value.screenState)
+ assertEquals(2, viewModel.uiState.value.studentContextItems.size)
+ assertEquals(courses[0], viewModel.uiState.value.studentContextItems[0].course)
+ assertEquals(users[0], viewModel.uiState.value.studentContextItems[0].user)
+ assertEquals(courses[1], viewModel.uiState.value.studentContextItems[1].course)
+ assertEquals(users[1], viewModel.uiState.value.studentContextItems[1].user)
+ }
+
+ private fun getViewModel(): ParentInboxCoursePickerViewModel {
+ return ParentInboxCoursePickerViewModel(context, repository, apiPrefs)
+ }
+}
\ No newline at end of file
diff --git a/apps/parent/src/test/java/com/instructure/parentapp/features/lti/LtiLaunchRepositoryTest.kt b/apps/parent/src/test/java/com/instructure/parentapp/features/lti/LtiLaunchRepositoryTest.kt
new file mode 100644
index 0000000000..05ab5f04d3
--- /dev/null
+++ b/apps/parent/src/test/java/com/instructure/parentapp/features/lti/LtiLaunchRepositoryTest.kt
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2024 - 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.parentapp.features.lti
+
+import com.instructure.canvasapi2.apis.LaunchDefinitionsAPI
+import com.instructure.canvasapi2.models.LTITool
+import com.instructure.canvasapi2.utils.DataResult
+import io.mockk.coEvery
+import io.mockk.mockk
+import kotlinx.coroutines.test.runTest
+import org.junit.Assert.assertEquals
+import org.junit.Test
+
+class LtiLaunchRepositoryTest {
+
+ private val launchDefinitionsApi: LaunchDefinitionsAPI.LaunchDefinitionsInterface = mockk(relaxed = true)
+
+ private val repository = LtiLaunchRepository(launchDefinitionsApi)
+
+ @Test
+ fun `Get lti from authentication url throws exception when fails`() = runTest {
+ val url = "https://www.instructure.com"
+ val result = runCatching { repository.getLtiFromAuthenticationUrl(url) }
+ assert(result.isFailure)
+ }
+
+ @Test
+ fun `Get lti from authentication url returns data when successful`() = runTest {
+ val url = "https://www.instructure.com"
+ val expected = LTITool()
+ coEvery { launchDefinitionsApi.getLtiFromAuthenticationUrl(url, any()) } returns DataResult.Success(expected)
+
+ val result = repository.getLtiFromAuthenticationUrl(url)
+
+ assertEquals(expected, result)
+ }
+}
\ No newline at end of file
diff --git a/apps/parent/src/test/java/com/instructure/parentapp/features/lti/LtiLaunchViewModelTest.kt b/apps/parent/src/test/java/com/instructure/parentapp/features/lti/LtiLaunchViewModelTest.kt
new file mode 100644
index 0000000000..19e8745f60
--- /dev/null
+++ b/apps/parent/src/test/java/com/instructure/parentapp/features/lti/LtiLaunchViewModelTest.kt
@@ -0,0 +1,108 @@
+/*
+ * Copyright (C) 2024 - 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.parentapp.features.lti
+
+import androidx.arch.core.executor.testing.InstantTaskExecutorRule
+import androidx.lifecycle.SavedStateHandle
+import com.instructure.canvasapi2.models.LTITool
+import io.mockk.coEvery
+import io.mockk.every
+import io.mockk.mockk
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.toList
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.resetMain
+import kotlinx.coroutines.test.runTest
+import kotlinx.coroutines.test.setMain
+import org.junit.After
+import org.junit.Assert.assertEquals
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+
+@OptIn(ExperimentalCoroutinesApi::class)
+class LtiLaunchViewModelTest {
+
+ @get:Rule
+ val instantExecutorRule = InstantTaskExecutorRule()
+
+ private val savedStateHandle: SavedStateHandle = mockk(relaxed = true)
+ private val repository: LtiLaunchRepository = mockk(relaxed = true)
+ private val testDispatcher = UnconfinedTestDispatcher()
+
+ private lateinit var viewModel: LtiLaunchViewModel
+
+ @Before
+ fun setup() {
+ every { savedStateHandle.get(LtiLaunchFragment.LTI_URL) } returns "url"
+ Dispatchers.setMain(testDispatcher)
+ }
+
+ @After
+ fun tearDown() {
+ Dispatchers.resetMain()
+ }
+
+ @Test
+ fun `Launch custom tab when lti tool url is successful`() = runTest {
+ val ltiTool = LTITool(url = "url")
+ coEvery { repository.getLtiFromAuthenticationUrl("url") } returns ltiTool
+
+ viewModel = LtiLaunchViewModel(savedStateHandle, repository)
+
+ val events = mutableListOf()
+
+ backgroundScope.launch(testDispatcher) {
+ viewModel.events.toList(events)
+ }
+
+ assertEquals(events[0], LtiLaunchAction.LaunchCustomTab("url"))
+ }
+
+ @Test
+ fun `Show error when lti tool url is null`() = runTest {
+ val ltiTool = LTITool(url = null)
+ coEvery { repository.getLtiFromAuthenticationUrl("url") } returns ltiTool
+
+ viewModel = LtiLaunchViewModel(savedStateHandle, repository)
+
+ val events = mutableListOf()
+
+ backgroundScope.launch(testDispatcher) {
+ viewModel.events.toList(events)
+ }
+
+ assertEquals(events[0], LtiLaunchAction.ShowError)
+ }
+
+ @Test
+ fun `Show error when lti request fails`() = runTest {
+ coEvery { repository.getLtiFromAuthenticationUrl("url") } throws Exception()
+
+ viewModel = LtiLaunchViewModel(savedStateHandle, repository)
+
+ val events = mutableListOf()
+
+ backgroundScope.launch(testDispatcher) {
+ viewModel.events.toList(events)
+ }
+
+ assertEquals(events[0], LtiLaunchAction.ShowError)
+ }
+}
\ No newline at end of file
diff --git a/apps/parent/src/test/java/com/instructure/parentapp/features/managestudents/ManageStudentsViewModelTest.kt b/apps/parent/src/test/java/com/instructure/parentapp/features/managestudents/ManageStudentsViewModelTest.kt
index 63704fa24b..790bd493ec 100644
--- a/apps/parent/src/test/java/com/instructure/parentapp/features/managestudents/ManageStudentsViewModelTest.kt
+++ b/apps/parent/src/test/java/com/instructure/parentapp/features/managestudents/ManageStudentsViewModelTest.kt
@@ -28,8 +28,8 @@ import com.instructure.canvasapi2.utils.ContextKeeper
import com.instructure.pandautils.utils.ColorKeeper
import com.instructure.pandautils.utils.ColorUtils
import com.instructure.pandautils.utils.ThemedColor
-import com.instructure.pandautils.utils.createThemedColor
import com.instructure.parentapp.R
+import com.instructure.parentapp.features.dashboard.SelectedStudentHolder
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.every
@@ -49,6 +49,7 @@ import org.junit.After
import org.junit.Assert
import org.junit.Before
import org.junit.Rule
+import org.junit.Test
@ExperimentalCoroutinesApi
@@ -64,6 +65,7 @@ class ManageStudentsViewModelTest {
private val context: Context = mockk(relaxed = true)
private val repository: ManageStudentsRepository = mockk(relaxed = true)
private val colorKeeper: ColorKeeper = spyk()
+ private val selectedStudentHolder: SelectedStudentHolder = mockk(relaxed = true)
private lateinit var viewModel: ManageStudentViewModel
@@ -76,7 +78,7 @@ class ManageStudentsViewModelTest {
every { ColorUtils.correctContrastForText(any(), any()) } answers { firstArg() }
every { ColorUtils.correctContrastForButtonBackground(any(), any(), any()) } answers { firstArg() }
every { context.getColor(any()) } answers { firstArg() }
- every { createThemedColor(any()) } answers { ThemedColor(firstArg()) }
+ every { colorKeeper.createThemedColor(any()) } answers { ThemedColor(firstArg()) }
}
@After
@@ -85,7 +87,7 @@ class ManageStudentsViewModelTest {
unmockkObject(ColorUtils)
}
- //@Test - Gonna be fixed when new student colors will be added
+ @Test
fun `Load students`() {
val students = listOf(User(id = 1, shortName = "Student 1", pronouns = "He/Him"))
val expectedState = ManageStudentsUiState(
@@ -101,7 +103,7 @@ class ManageStudentsViewModelTest {
Assert.assertEquals(expectedState, viewModel.uiState.value)
}
- //@Test - Gonna be fixed when new student colors will be added
+ @Test
fun `Load students error`() {
val expectedState = ManageStudentsUiState(isLoadError = true)
coEvery { repository.getStudents(any()) } throws Exception()
@@ -111,7 +113,7 @@ class ManageStudentsViewModelTest {
Assert.assertEquals(expectedState, viewModel.uiState.value)
}
- //@Test - Gonna be fixed when new student colors will be added
+ @Test
fun `Load students empty`() {
val expectedState = ManageStudentsUiState(isLoading = false, isLoadError = false, studentListItems = emptyList())
coEvery { repository.getStudents(any()) } returns emptyList()
@@ -121,7 +123,7 @@ class ManageStudentsViewModelTest {
Assert.assertEquals(expectedState, viewModel.uiState.value)
}
- //@Test - Gonna be fixed when new student colors will be added
+ @Test
fun `Navigate to alert settings screen`() = runTest {
coEvery { repository.getStudents(any()) } returns listOf(User(id = 1))
createViewModel()
@@ -137,7 +139,7 @@ class ManageStudentsViewModelTest {
Assert.assertEquals(expected, events.last())
}
- //@Test - Gonna be fixed when new student colors will be added
+ @Test
fun `Refresh reloads students`() {
createViewModel()
@@ -146,7 +148,7 @@ class ManageStudentsViewModelTest {
coVerify { repository.getStudents(true) }
}
- //@Test - Gonna be fixed when new student colors will be added
+ @Test
fun `Show color picker dialog`() {
val userColors = listOf(
UserColor(
@@ -198,7 +200,7 @@ class ManageStudentsViewModelTest {
Assert.assertEquals(expected, viewModel.uiState.value)
}
- //@Test - Gonna be fixed when new student colors will be added
+ @Test
fun `Hide color picker dialog`() = runTest {
every { colorKeeper.userColors } returns emptyList()
@@ -211,7 +213,7 @@ class ManageStudentsViewModelTest {
Assert.assertFalse(viewModel.uiState.value.colorPickerDialogUiState.showColorPickerDialog)
}
- //@Test - Gonna be fixed when new student colors will be added
+ @Test
fun `Save student color`() {
val expectedUiState = ManageStudentsUiState(
colorPickerDialogUiState = ColorPickerDialogUiState(),
@@ -237,7 +239,7 @@ class ManageStudentsViewModelTest {
Assert.assertEquals(expectedUiState, viewModel.uiState.value)
}
- //@Test - Gonna be fixed when new student colors will be added
+ @Test
fun `Save student color error`() {
val expectedUiState = ManageStudentsUiState(
colorPickerDialogUiState = ColorPickerDialogUiState(isSavingColorError = true),
@@ -264,6 +266,6 @@ class ManageStudentsViewModelTest {
}
private fun createViewModel() {
- viewModel = ManageStudentViewModel(context, colorKeeper, repository)
+ viewModel = ManageStudentViewModel(context, colorKeeper, repository, selectedStudentHolder)
}
}
diff --git a/apps/parent/src/test/java/com/instructure/parentapp/features/splash/SplashViewModelTest.kt b/apps/parent/src/test/java/com/instructure/parentapp/features/splash/SplashViewModelTest.kt
index 69695cb976..ec0f8c6f4e 100644
--- a/apps/parent/src/test/java/com/instructure/parentapp/features/splash/SplashViewModelTest.kt
+++ b/apps/parent/src/test/java/com/instructure/parentapp/features/splash/SplashViewModelTest.kt
@@ -62,7 +62,6 @@ class SplashViewModelTest {
private val repository: SplashRepository = mockk(relaxed = true)
private val apiPrefs: ApiPrefs = mockk(relaxed = true)
private val colorKeeper: ColorKeeper = mockk(relaxed = true)
- private val themePrefs: ThemePrefs = mockk(relaxed = true)
private lateinit var viewModel: SplashViewModel
@@ -96,7 +95,6 @@ class SplashViewModelTest {
coVerify { apiPrefs.user = user }
coVerify { colorKeeper.addToCache(colors) }
- coVerify { themePrefs.applyCanvasTheme(theme, context) }
val events = mutableListOf()
backgroundScope.launch(testDispatcher) {
@@ -104,6 +102,7 @@ class SplashViewModelTest {
}
Assert.assertEquals(SplashAction.InitialDataLoadingFinished, events.last())
+ Assert.assertEquals(SplashAction.ApplyTheme(theme), events.first())
}
@Test
@@ -166,8 +165,7 @@ class SplashViewModelTest {
context = context,
repository = repository,
apiPrefs = apiPrefs,
- colorKeeper = colorKeeper,
- themePrefs = themePrefs,
+ colorKeeper = colorKeeper
)
}
}
diff --git a/apps/student/build.gradle b/apps/student/build.gradle
index 8968eb85dc..451ec4425e 100644
--- a/apps/student/build.gradle
+++ b/apps/student/build.gradle
@@ -40,11 +40,10 @@ android {
applicationId "com.instructure.candroid"
minSdkVersion Versions.MIN_SDK
targetSdkVersion Versions.TARGET_SDK
- versionCode = 266
- versionName = '7.5.3'
+ versionCode = 268
+ versionName = '7.6.1'
vectorDrawables.useSupportLibrary = true
- multiDexEnabled = true
testInstrumentationRunner 'com.instructure.student.espresso.StudentHiltTestRunner'
testInstrumentationRunnerArguments disableAnalytics: 'true'
buildConfigField "boolean", "IS_TESTING", "false"
@@ -113,12 +112,6 @@ android {
buildConfigField "String", "HEAP_APP_ID", "\"$heapStagingId\""
- buildConfigField "String", "PRONOUN_STUDENT_TEST_USER", "\"$pronounTestStudent\""
- buildConfigField "String", "PRONOUN_STUDENT_TEST_PASSWORD", "\"$pronounTestStudentPassword\""
-
- buildConfigField "String", "PUSH_NOTIFICATIONS_STUDENT_TEST_USER", "\"$pushNotificationsTestStudent\""
- buildConfigField "String", "PUSH_NOTIFICATIONS_STUDENT_TEST_PASSWORD", "\"$pushNotificationsTestStudentPassword\""
-
ext {
heapEnabled = true
}
@@ -163,6 +156,11 @@ android {
qa {
buildConfigField "boolean", "IS_TESTING", "true"
+ buildConfigField "String", "PRONOUN_STUDENT_TEST_USER", "\"$pronounTestStudent\""
+ buildConfigField "String", "PRONOUN_STUDENT_TEST_PASSWORD", "\"$pronounTestStudentPassword\""
+
+ buildConfigField "String", "PUSH_NOTIFICATIONS_STUDENT_TEST_USER", "\"$pushNotificationsTestStudent\""
+ buildConfigField "String", "PUSH_NOTIFICATIONS_STUDENT_TEST_PASSWORD", "\"$pushNotificationsTestStudentPassword\""
dimension 'default'
}
@@ -306,7 +304,7 @@ dependencies {
implementation Libs.ANDROIDX_BROWSER
implementation Libs.ANDROIDX_CARDVIEW
implementation Libs.ANDROIDX_CONSTRAINT_LAYOUT
- implementation Libs.ANDROIDX_DESIGN
+ implementation Libs.MATERIAL_DESIGN
implementation Libs.ANDROIDX_RECYCLERVIEW
implementation Libs.ANDROIDX_PALETTE
implementation Libs.PLAY_IN_APP_UPDATES
@@ -319,7 +317,7 @@ dependencies {
implementation Libs.VIEW_MODEL
implementation Libs.LIVE_DATA
implementation Libs.VIEW_MODE_SAVED_STATE
- implementation Libs.FRAGMENT_KTX
+ implementation Libs.ANDROIDX_FRAGMENT_KTX
kapt Libs.LIFECYCLE_COMPILER
/* DI */
diff --git a/apps/student/src/androidTest/java/com/instructure/student/db/CreateFileSubmissionDaoTest.kt b/apps/student/src/androidTest/java/com/instructure/student/db/CreateFileSubmissionDaoTest.kt
index fdf690cae5..d61cc560b6 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/db/CreateFileSubmissionDaoTest.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/db/CreateFileSubmissionDaoTest.kt
@@ -27,13 +27,11 @@ import com.instructure.student.room.entities.daos.CreateFileSubmissionDao
import com.instructure.student.room.entities.daos.CreateSubmissionDao
import junit.framework.TestCase.assertEquals
import junit.framework.TestCase.assertNull
-import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
-@ExperimentalCoroutinesApi
@RunWith(AndroidJUnit4::class)
class CreateFileSubmissionDaoTest {
diff --git a/apps/student/src/androidTest/java/com/instructure/student/db/CreatePendingSubmissionCommentDaoTest.kt b/apps/student/src/androidTest/java/com/instructure/student/db/CreatePendingSubmissionCommentDaoTest.kt
index ff7104a3c8..f804d92f44 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/db/CreatePendingSubmissionCommentDaoTest.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/db/CreatePendingSubmissionCommentDaoTest.kt
@@ -23,7 +23,6 @@ import com.instructure.student.room.StudentDb
import com.instructure.student.room.entities.CreatePendingSubmissionCommentEntity
import com.instructure.student.room.entities.daos.CreatePendingSubmissionCommentDao
import junit.framework.TestCase.assertEquals
-import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.After
import org.junit.Before
@@ -31,7 +30,6 @@ import org.junit.Test
import org.junit.runner.RunWith
import java.util.Date
-@ExperimentalCoroutinesApi
@RunWith(AndroidJUnit4::class)
class CreatePendingSubmissionCommentDaoTest {
diff --git a/apps/student/src/androidTest/java/com/instructure/student/db/CreateSubmissionCommentFileDaoTest.kt b/apps/student/src/androidTest/java/com/instructure/student/db/CreateSubmissionCommentFileDaoTest.kt
index 8dc2b1afdd..7249979dad 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/db/CreateSubmissionCommentFileDaoTest.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/db/CreateSubmissionCommentFileDaoTest.kt
@@ -26,7 +26,6 @@ import com.instructure.student.room.entities.CreateSubmissionCommentFileEntity
import com.instructure.student.room.entities.daos.CreatePendingSubmissionCommentDao
import com.instructure.student.room.entities.daos.CreateSubmissionCommentFileDao
import junit.framework.TestCase.assertEquals
-import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.After
import org.junit.Before
@@ -34,7 +33,6 @@ import org.junit.Test
import org.junit.runner.RunWith
import java.util.Date
-@ExperimentalCoroutinesApi
@RunWith(AndroidJUnit4::class)
class CreateSubmissionCommentFileDaoTest {
diff --git a/apps/student/src/androidTest/java/com/instructure/student/db/CreateSubmissionDaoTest.kt b/apps/student/src/androidTest/java/com/instructure/student/db/CreateSubmissionDaoTest.kt
index f6c44cdcae..c712c680c8 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/db/CreateSubmissionDaoTest.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/db/CreateSubmissionDaoTest.kt
@@ -26,15 +26,12 @@ import com.instructure.student.room.entities.CreateSubmissionEntity
import com.instructure.student.room.entities.daos.CreateSubmissionDao
import junit.framework.TestCase.assertEquals
import junit.framework.TestCase.assertNull
-import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.After
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
-
-@ExperimentalCoroutinesApi
@RunWith(AndroidJUnit4::class)
class CreateSubmissionDaoTest {
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 84189f2ea9..cf91f416b9 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
@@ -98,12 +98,12 @@ class AnnouncementsE2ETest : StudentTest() {
announcementListPage.searchable.clickOnSearchButton()
announcementListPage.searchable.typeToSearchBar(announcement.title)
- Log.d(STEP_TAG,"Assert that only the matching announcement is displayed on the Discussion List Page.")
+ Log.d(STEP_TAG,"Assert that only the matching announcement is displayed on the Announcement List Page.")
announcementListPage.pullToUpdate()
announcementListPage.assertTopicDisplayed(announcement.title)
announcementListPage.assertTopicNotDisplayed(lockedAnnouncement.title)
- Log.d(STEP_TAG,"Clear search input field value and assert if all the announcements are displayed again on the Discussion List Page.")
+ Log.d(STEP_TAG,"Clear search input field value and assert if all the announcements are displayed again on the Announcement List Page.")
announcementListPage.searchable.clickOnClearSearchButton()
announcementListPage.waitForDiscussionTopicToDisplay(lockedAnnouncement.title)
announcementListPage.assertTopicDisplayed(announcement.title)
@@ -117,7 +117,7 @@ class AnnouncementsE2ETest : StudentTest() {
announcementListPage.assertTopicNotDisplayed(announcement.title)
announcementListPage.assertTopicNotDisplayed(lockedAnnouncement.title)
- Log.d(STEP_TAG,"Clear search input field value and assert if all the announcements are displayed again on the Discussion List Page.")
+ Log.d(STEP_TAG,"Clear search input field value and assert if all the announcements are displayed again on the Announcement List Page.")
announcementListPage.searchable.clickOnClearSearchButton()
announcementListPage.waitForDiscussionTopicToDisplay(lockedAnnouncement.title)
announcementListPage.assertTopicDisplayed(announcement.title)
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/k5/HomeroomE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/k5/HomeroomE2ETest.kt
index ed32b8e2e2..830e9ee26c 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/k5/HomeroomE2ETest.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/k5/HomeroomE2ETest.kt
@@ -22,6 +22,7 @@ import com.instructure.canvas.espresso.E2E
import com.instructure.canvas.espresso.FeatureCategory
import com.instructure.canvas.espresso.Priority
import com.instructure.canvas.espresso.SecondaryFeatureCategory
+import com.instructure.canvas.espresso.Stub
import com.instructure.canvas.espresso.TestCategory
import com.instructure.canvas.espresso.TestMetaData
import com.instructure.dataseeding.api.AssignmentsApi
@@ -48,6 +49,7 @@ class HomeroomE2ETest : StudentTest() {
override fun enableAndConfigureAccessibilityChecks() = Unit
+ @Stub // TODO: Investigate flaky test
@E2E
@Test
@TestMetaData(Priority.MANDATORY, FeatureCategory.CANVAS_FOR_ELEMENTARY, TestCategory.E2E, SecondaryFeatureCategory.HOMEROOM)
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineDiscussionsE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineDiscussionsE2ETest.kt
index 814edaf48e..e956b0267c 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineDiscussionsE2ETest.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineDiscussionsE2ETest.kt
@@ -22,6 +22,7 @@ import com.instructure.canvas.espresso.FeatureCategory
import com.instructure.canvas.espresso.OfflineE2E
import com.instructure.canvas.espresso.Priority
import com.instructure.canvas.espresso.SecondaryFeatureCategory
+import com.instructure.canvas.espresso.Stub
import com.instructure.canvas.espresso.TestCategory
import com.instructure.canvas.espresso.TestMetaData
import com.instructure.canvas.espresso.checkToastText
@@ -45,6 +46,7 @@ class OfflineDiscussionsE2ETest : StudentTest() {
override fun enableAndConfigureAccessibilityChecks() = Unit
+ @Stub // TODO: Investigate flaky test
@OfflineE2E
@Test
@TestMetaData(Priority.MANDATORY, FeatureCategory.DISCUSSIONS, TestCategory.E2E, SecondaryFeatureCategory.OFFLINE_MODE)
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/StudentCalendarInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/StudentCalendarInteractionTest.kt
index 21e87bf305..c24adf0737 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/StudentCalendarInteractionTest.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/StudentCalendarInteractionTest.kt
@@ -23,7 +23,7 @@ import com.instructure.espresso.ModuleItemInteractions
import com.instructure.student.BuildConfig
import com.instructure.student.R
import com.instructure.student.activity.LoginActivity
-import com.instructure.student.ui.pages.AssignmentDetailsPage
+import com.instructure.canvas.espresso.common.pages.AssignmentDetailsPage
import com.instructure.student.ui.pages.DashboardPage
import com.instructure.student.ui.pages.DiscussionDetailsPage
import com.instructure.student.ui.utils.StudentActivityTestRule
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 83a593b0a2..1da5850b9e 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
@@ -17,7 +17,11 @@
package com.instructure.student.ui.pages
import androidx.test.espresso.assertion.ViewAssertions.matches
-import androidx.test.espresso.matcher.ViewMatchers.*
+import androidx.test.espresso.matcher.ViewMatchers.hasSibling
+import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
+import androidx.test.espresso.matcher.ViewMatchers.withContentDescription
+import androidx.test.espresso.matcher.ViewMatchers.withParent
+import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.platform.app.InstrumentationRegistry
import com.instructure.canvas.espresso.DirectlyPopulateEditText
import com.instructure.canvas.espresso.explicitClick
@@ -56,7 +60,7 @@ open class DiscussionListPage(val searchable: Searchable) : BasePage(R.id.discus
fun waitForDiscussionTopicToDisplay(topicTitle: String) {
val matcher = allOf(withText(topicTitle), withId(R.id.discussionTitle))
- waitForView(matcher)
+ waitForView(matcher).assertDisplayed()
}
fun assertTopicDisplayed(topicTitle: String) {
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/StudentAssignmentDetailsPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/StudentAssignmentDetailsPage.kt
new file mode 100644
index 0000000000..d8dee9a60c
--- /dev/null
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/StudentAssignmentDetailsPage.kt
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2024 - 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.pages
+
+import androidx.appcompat.widget.AppCompatButton
+import androidx.test.espresso.Espresso
+import androidx.test.espresso.matcher.ViewMatchers.isAssignableFrom
+import com.instructure.canvas.espresso.CanvasTest
+import com.instructure.canvas.espresso.common.pages.AssignmentDetailsPage
+import com.instructure.canvas.espresso.containsTextCaseInsensitive
+import com.instructure.dataseeding.model.SubmissionType
+import com.instructure.espresso.ModuleItemInteractions
+import com.instructure.espresso.assertDisplayed
+import com.instructure.espresso.clearText
+import com.instructure.espresso.click
+import com.instructure.espresso.page.onView
+import com.instructure.espresso.page.plus
+import com.instructure.espresso.page.withAncestor
+import com.instructure.espresso.page.withId
+import com.instructure.espresso.page.withParent
+import com.instructure.espresso.page.withText
+import com.instructure.espresso.typeText
+import com.instructure.student.R
+import org.hamcrest.Matchers.allOf
+
+class StudentAssignmentDetailsPage(moduleItemInteractions: ModuleItemInteractions): AssignmentDetailsPage(moduleItemInteractions) {
+ fun addBookmark(bookmarkName: String) {
+ openOverflowMenu()
+ Espresso.onView(withText("Add Bookmark")).click()
+ Espresso.onView(withId(R.id.bookmarkEditText)).clearText()
+ Espresso.onView(withId(R.id.bookmarkEditText)).typeText(bookmarkName)
+ if(CanvasTest.isLandscapeDevice()) Espresso.pressBack()
+ Espresso.onView(allOf(isAssignableFrom(AppCompatButton::class.java), containsTextCaseInsensitive("Save"))).click()
+ }
+
+ fun selectSubmissionType(submissionType: SubmissionType) {
+ val viewMatcher = when (submissionType) {
+ SubmissionType.ONLINE_TEXT_ENTRY -> withId(R.id.submissionEntryText)
+ SubmissionType.ONLINE_UPLOAD -> withId(R.id.submissionEntryFile)
+ SubmissionType.ONLINE_URL -> withId(R.id.submissionEntryWebsite)
+ SubmissionType.MEDIA_RECORDING -> withId(R.id.submissionEntryMedia)
+
+ else -> {withId(R.id.submissionEntryText)}
+ }
+
+ onView(viewMatcher).click()
+ }
+
+ //OfflineMethod
+ fun assertDetailsNotAvailableOffline() {
+ onView(withId(R.id.notAvailableIcon) + withAncestor(R.id.moduleProgressionPage)).assertDisplayed()
+ onView(withId(R.id.title) + withText(R.string.notAvailableOfflineScreenTitle) + withParent(
+ R.id.textViews) + withAncestor(R.id.moduleProgressionPage)).assertDisplayed()
+ onView(withId(R.id.description) + withText(R.string.notAvailableOfflineDescriptionForTabs) + withParent(
+ R.id.textViews) + withAncestor(R.id.moduleProgressionPage)).assertDisplayed()
+ }
+}
\ No newline at end of file
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/SubmissionCommentsRenderTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/SubmissionCommentsRenderTest.kt
index 7b8b8e5d09..0a7d8b31a8 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/SubmissionCommentsRenderTest.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/SubmissionCommentsRenderTest.kt
@@ -15,8 +15,6 @@
*/
package com.instructure.student.ui.renderTests
-import androidx.room.Room
-import androidx.test.core.app.ApplicationProvider
import androidx.test.espresso.Espresso
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.instructure.canvas.espresso.FeatureCategory
@@ -40,11 +38,10 @@ import com.spotify.mobius.runners.WorkRunner
import dagger.hilt.android.testing.HiltAndroidTest
import junit.framework.Assert.assertTrue
import kotlinx.coroutines.test.runTest
-import org.junit.After
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
-import java.util.*
+import java.util.Date
import javax.inject.Inject
@HiltAndroidTest
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/SubmissionRubricRenderTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/SubmissionRubricRenderTest.kt
index 2713887740..654d0437b3 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/SubmissionRubricRenderTest.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/SubmissionRubricRenderTest.kt
@@ -26,8 +26,14 @@ import com.instructure.canvasapi2.models.Assignment
import com.instructure.canvasapi2.models.RubricCriterion
import com.instructure.canvasapi2.models.RubricCriterionRating
import com.instructure.canvasapi2.models.Submission
-import com.instructure.espresso.*
+import com.instructure.espresso.assertDisplayed
+import com.instructure.espresso.assertGone
+import com.instructure.espresso.assertHasText
+import com.instructure.espresso.assertNotDisplayed
+import com.instructure.espresso.assertVisible
+import com.instructure.espresso.click
import com.instructure.espresso.page.onViewWithText
+import com.instructure.pandautils.features.assignments.details.mobius.gradeCell.GradeCellViewState
import com.instructure.student.espresso.StudentRenderTest
import com.instructure.student.mobius.assignmentDetails.submissionDetails.drawer.rubric.RatingData
import com.instructure.student.mobius.assignmentDetails.submissionDetails.drawer.rubric.RubricListData
@@ -35,7 +41,6 @@ import com.instructure.student.mobius.assignmentDetails.submissionDetails.drawer
import com.instructure.student.mobius.assignmentDetails.submissionDetails.drawer.rubric.SubmissionRubricViewState
import com.instructure.student.mobius.assignmentDetails.submissionDetails.drawer.rubric.ui.SubmissionRubricFragment
import com.instructure.student.mobius.assignmentDetails.submissionDetails.ui.SubmissionDetailsTabData
-import com.instructure.student.mobius.assignmentDetails.ui.gradeCell.GradeCellViewState
import com.instructure.student.ui.pages.renderPages.SubmissionRubricRenderPage
import com.instructure.student.ui.utils.assertFontSizeSP
import com.spotify.mobius.runners.WorkRunner
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/views/GradeCellRenderTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/views/GradeCellRenderTest.kt
index ecb161ed32..17151a2d39 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/views/GradeCellRenderTest.kt
+++ b/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/views/GradeCellRenderTest.kt
@@ -17,12 +17,16 @@ package com.instructure.student.ui.renderTests.views
import android.view.ViewGroup
import androidx.test.ext.junit.runners.AndroidJUnit4
-import com.instructure.espresso.*
+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.page.BasePage
+import com.instructure.pandautils.features.assignments.details.mobius.gradeCell.GradeCellView
+import com.instructure.pandautils.features.assignments.details.mobius.gradeCell.GradeCellViewState
import com.instructure.student.R
import com.instructure.student.espresso.StudentRenderTest
-import com.instructure.student.mobius.assignmentDetails.ui.gradeCell.GradeCellView
-import com.instructure.student.mobius.assignmentDetails.ui.gradeCell.GradeCellViewState
import dagger.hilt.android.testing.HiltAndroidTest
import org.junit.Test
import org.junit.runner.RunWith
@@ -63,7 +67,7 @@ class GradeCellRenderTest : StudentRenderTest() {
gradeCell.submittedTitle.assertDisplayed()
gradeCell.submittedSubtitle.assertDisplayed()
gradeCell.submittedTitle.assertHasText("Successfully submitted!")
- gradeCell.submittedSubtitle.assertHasText("Your submission is now waiting to be graded")
+ gradeCell.submittedSubtitle.assertHasText("The submission is now waiting to be graded")
}
@Test
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 b524f1c508..efdc4c2a85 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
@@ -45,7 +45,6 @@ import com.instructure.student.ui.pages.AboutPage
import com.instructure.student.ui.pages.AllCoursesPage
import com.instructure.student.ui.pages.AnnotationCommentListPage
import com.instructure.student.ui.pages.AnnouncementListPage
-import com.instructure.student.ui.pages.AssignmentDetailsPage
import com.instructure.student.ui.pages.AssignmentListPage
import com.instructure.student.ui.pages.BookmarkPage
import com.instructure.student.ui.pages.CanvasWebViewPage
@@ -93,6 +92,7 @@ import com.instructure.student.ui.pages.SchedulePage
import com.instructure.student.ui.pages.SettingsPage
import com.instructure.student.ui.pages.ShareExtensionStatusPage
import com.instructure.student.ui.pages.ShareExtensionTargetPage
+import com.instructure.student.ui.pages.StudentAssignmentDetailsPage
import com.instructure.student.ui.pages.SubmissionDetailsPage
import com.instructure.student.ui.pages.SyllabusPage
import com.instructure.student.ui.pages.TextSubmissionUploadPage
@@ -121,7 +121,7 @@ abstract class StudentTest : CanvasTest() {
*/
val annotationCommentListPage = AnnotationCommentListPage()
val announcementListPage = AnnouncementListPage(Searchable(R.id.search, R.id.search_src_text, R.id.search_close_btn))
- val assignmentDetailsPage = AssignmentDetailsPage(ModuleItemInteractions(R.id.moduleName, R.id.next_item, R.id.prev_item))
+ val assignmentDetailsPage = StudentAssignmentDetailsPage(ModuleItemInteractions(R.id.moduleName, R.id.next_item, R.id.prev_item))
val assignmentListPage = AssignmentListPage(Searchable(R.id.search, R.id.search_src_text))
val bookmarkPage = BookmarkPage()
val canvasWebViewPage = CanvasWebViewPage()
diff --git a/apps/student/src/main/AndroidManifest.xml b/apps/student/src/main/AndroidManifest.xml
index aaf354eb0e..360bcc3e0e 100644
--- a/apps/student/src/main/AndroidManifest.xml
+++ b/apps/student/src/main/AndroidManifest.xml
@@ -304,7 +304,7 @@
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 c3c945b528..9d64ef0107 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
@@ -141,10 +141,9 @@ abstract class CallbackActivity : ParentActivity(), OnUnreadCountInvalidated, No
StudentPrefs.hideCourseColorOverlay = it.hideDashCardColorOverlays
}
- val launchDefinitions = awaitApi?> { LaunchDefinitionsManager.getLaunchDefinitions(it, false) }
+ val launchDefinitions = awaitApi { LaunchDefinitionsManager.getLaunchDefinitions(it, false) }
launchDefinitions?.let {
- val definitions = launchDefinitions.filter { it.domain == LaunchDefinition.STUDIO_DOMAIN || it.domain == LaunchDefinition.GAUGE_DOMAIN }
- gotLaunchDefinitions(definitions)
+ gotLaunchDefinitions(it)
}
if (!ApiPrefs.isMasquerading) {
diff --git a/apps/student/src/main/java/com/instructure/student/activity/InterwebsToApplication.kt b/apps/student/src/main/java/com/instructure/student/activity/InterwebsToApplication.kt
index 75b0f78651..9f175cbc9a 100644
--- a/apps/student/src/main/java/com/instructure/student/activity/InterwebsToApplication.kt
+++ b/apps/student/src/main/java/com/instructure/student/activity/InterwebsToApplication.kt
@@ -47,7 +47,7 @@ import com.instructure.pandautils.utils.Utils.generateUserAgent
import com.instructure.student.R
import com.instructure.student.databinding.InterwebsToApplicationBinding
import com.instructure.student.databinding.LoadingCanvasViewBinding
-import com.instructure.student.features.assignments.reminder.AlarmScheduler
+import com.instructure.pandautils.features.assignments.details.reminder.AlarmScheduler
import com.instructure.student.router.RouteMatcher
import com.instructure.student.tasks.StudentLogoutTask
import com.instructure.student.util.LoggingUtility
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 bc30d82b8c..709f6b6fb0 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
@@ -78,7 +78,9 @@ import com.instructure.interactions.router.RouteContext
import com.instructure.interactions.router.RouterParams
import com.instructure.loginapi.login.dialog.MasqueradingDialog
import com.instructure.loginapi.login.tasks.LogoutTask
+import com.instructure.pandautils.analytics.OfflineAnalyticsManager
import com.instructure.pandautils.binding.viewBinding
+import com.instructure.pandautils.features.assignments.details.reminder.AlarmScheduler
import com.instructure.pandautils.features.calendar.CalendarFragment
import com.instructure.pandautils.features.calendarevent.details.EventFragment
import com.instructure.pandautils.features.help.HelpDialogFragment
@@ -126,7 +128,6 @@ import com.instructure.student.events.CourseColorOverlayToggledEvent
import com.instructure.student.events.ShowConfettiEvent
import com.instructure.student.events.ShowGradesToggledEvent
import com.instructure.student.events.UserUpdatedEvent
-import com.instructure.student.features.assignments.reminder.AlarmScheduler
import com.instructure.student.features.files.list.FileListFragment
import com.instructure.student.features.modules.progression.CourseModuleProgressionFragment
import com.instructure.student.features.navigation.NavigationRepository
@@ -211,6 +212,9 @@ class NavigationActivity : BaseRouterActivity(), Navigation, MasqueradingDialog.
@Inject
lateinit var oAuthApi: OAuthAPI.OAuthInterface
+ @Inject
+ lateinit var offlineAnalyticsManager: OfflineAnalyticsManager
+
private var routeJob: WeaveJob? = null
private var debounceJob: Job? = null
private var drawerItemSelectedJob: Job? = null
@@ -237,17 +241,9 @@ class NavigationActivity : BaseRouterActivity(), Navigation, MasqueradingDialog.
R.id.navigationDrawerItem_files -> {
ApiPrefs.user?.let { handleRoute(FileListFragment.makeRoute(it)) }
}
- R.id.navigationDrawerItem_gauge, R.id.navigationDrawerItem_studio -> {
+ R.id.navigationDrawerItem_gauge, R.id.navigationDrawerItem_studio, R.id.navigationDrawerItem_mastery -> {
val launchDefinition = v.tag as? LaunchDefinition ?: return@weave
- val user = ApiPrefs.user ?: return@weave
- val title = getString(if (launchDefinition.isGauge) R.string.gauge else R.string.studio)
- val route = LtiLaunchFragment.makeRoute(
- canvasContext = CanvasContext.currentUserContext(user),
- url = launchDefinition.placements.globalNavigation.url,
- title = title,
- sessionLessLaunch = true
- )
- RouteMatcher.route(this@NavigationActivity, route)
+ launchLti(launchDefinition)
}
R.id.navigationDrawerItem_bookmarks -> {
val route = BookmarksFragment.makeRoute(ApiPrefs.user)
@@ -290,6 +286,18 @@ class NavigationActivity : BaseRouterActivity(), Navigation, MasqueradingDialog.
}
}
+ private fun launchLti(launchDefinition: LaunchDefinition) {
+ val user = ApiPrefs.user ?: return
+ val title = launchDefinition.name
+ val route = LtiLaunchFragment.makeRoute(
+ canvasContext = CanvasContext.currentUserContext(user),
+ url = launchDefinition.placements?.globalNavigation?.url.orEmpty(),
+ title = title,
+ sessionLessLaunch = true
+ )
+ RouteMatcher.route(this, route)
+ }
+
private val onBackStackChangedListener = FragmentManager.OnBackStackChangedListener {
currentFragment?.let {
// Sends a broadcast event to notify the backstack has changed and which fragment class is on top.
@@ -372,6 +380,7 @@ class NavigationActivity : BaseRouterActivity(), Navigation, MasqueradingDialog.
requestNotificationsPermission()
networkStateProvider.isOnlineLiveData.observe(this) { isOnline ->
+ logOfflineEvents(isOnline)
setOfflineState(!isOnline)
handleTokenCheck(isOnline)
}
@@ -390,6 +399,16 @@ class NavigationActivity : BaseRouterActivity(), Navigation, MasqueradingDialog.
}
}
+ private fun logOfflineEvents(isOnline: Boolean) {
+ lifecycleScope.launch {
+ if (isOnline) {
+ offlineAnalyticsManager.offlineModeEnded()
+ } else {
+ offlineAnalyticsManager.offlineModeStarted()
+ }
+ }
+ }
+
private fun loadAuthenticatedSession() {
lifecycleScope.launch {
oAuthApi.getAuthenticatedSession(
@@ -475,11 +494,13 @@ class NavigationActivity : BaseRouterActivity(), Navigation, MasqueradingDialog.
override fun onStart() {
super.onStart()
EventBus.getDefault().register(this)
+ logOfflineEvents(networkStateProvider.isOnline())
}
override fun onStop() {
super.onStop()
EventBus.getDefault().unregister(this)
+ logOfflineEvents(true)
}
override fun onDestroy() {
@@ -596,6 +617,7 @@ class NavigationActivity : BaseRouterActivity(), Navigation, MasqueradingDialog.
navigationDrawerBinding.navigationDrawerItemFiles.onClickWithRequireNetwork(mNavigationDrawerItemClickListener)
navigationDrawerBinding.navigationDrawerItemGauge.onClickWithRequireNetwork(mNavigationDrawerItemClickListener)
navigationDrawerBinding.navigationDrawerItemStudio.onClickWithRequireNetwork(mNavigationDrawerItemClickListener)
+ navigationDrawerBinding.navigationDrawerItemMastery.onClickWithRequireNetwork(mNavigationDrawerItemClickListener)
navigationDrawerBinding.navigationDrawerItemBookmarks.onClickWithRequireNetwork(mNavigationDrawerItemClickListener)
navigationDrawerBinding.navigationDrawerItemChangeUser.setOnClickListener(mNavigationDrawerItemClickListener)
navigationDrawerBinding.navigationDrawerItemHelp.onClickWithRequireNetwork(mNavigationDrawerItemClickListener)
@@ -1188,6 +1210,7 @@ class NavigationActivity : BaseRouterActivity(), Navigation, MasqueradingDialog.
override fun gotLaunchDefinitions(launchDefinitions: List?) {
val studioLaunchDefinition = launchDefinitions?.firstOrNull { it.domain == LaunchDefinition.STUDIO_DOMAIN }
val gaugeLaunchDefinition = launchDefinitions?.firstOrNull { it.domain == LaunchDefinition.GAUGE_DOMAIN }
+ val masteryLaunchDefinition = launchDefinitions?.firstOrNull { it.domain == LaunchDefinition.MASTERY_DOMAIN }
val studio = findViewById(R.id.navigationDrawerItem_studio)
studio.visibility = if (studioLaunchDefinition != null) View.VISIBLE else View.GONE
@@ -1196,6 +1219,10 @@ class NavigationActivity : BaseRouterActivity(), Navigation, MasqueradingDialog.
val gauge = findViewById(R.id.navigationDrawerItem_gauge)
gauge.visibility = if (gaugeLaunchDefinition != null) View.VISIBLE else View.GONE
gauge.tag = gaugeLaunchDefinition
+
+ val mastery = findViewById(R.id.navigationDrawerItem_mastery)
+ mastery.visibility = if (masteryLaunchDefinition != null) View.VISIBLE else View.GONE
+ mastery.tag = masteryLaunchDefinition
}
override fun addBookmark() {
diff --git a/apps/student/src/main/java/com/instructure/student/activity/VideoViewActivity.kt b/apps/student/src/main/java/com/instructure/student/activity/VideoViewActivity.kt
index ccf3a18574..ac391875ef 100644
--- a/apps/student/src/main/java/com/instructure/student/activity/VideoViewActivity.kt
+++ b/apps/student/src/main/java/com/instructure/student/activity/VideoViewActivity.kt
@@ -48,6 +48,8 @@ import androidx.media3.extractor.DefaultExtractorsFactory
import com.instructure.pandautils.analytics.SCREEN_VIEW_VIDEO_VIEW
import com.instructure.pandautils.analytics.ScreenView
import com.instructure.pandautils.binding.viewBinding
+import com.instructure.pandautils.utils.ThemePrefs
+import com.instructure.pandautils.utils.ViewStyler
import com.instructure.student.databinding.ActivityVideoViewBinding
import com.instructure.student.util.Const
@@ -78,10 +80,11 @@ class VideoViewActivity : AppCompatActivity() {
player?.playWhenReady = true
player?.setMediaSource(buildMediaSource(Uri.parse(intent?.extras?.getString(Const.URL))))
player?.prepare()
+ ViewStyler.setStatusBarDark(this, ThemePrefs.primaryColor)
}
- public override fun onStop() {
- super.onStop()
+ override fun onDestroy() {
+ super.onDestroy()
player?.release()
}
diff --git a/apps/student/src/main/java/com/instructure/student/adapter/InboxConversationAdapter.kt b/apps/student/src/main/java/com/instructure/student/adapter/InboxConversationAdapter.kt
index 75ede332f3..6bff06811f 100644
--- a/apps/student/src/main/java/com/instructure/student/adapter/InboxConversationAdapter.kt
+++ b/apps/student/src/main/java/com/instructure/student/adapter/InboxConversationAdapter.kt
@@ -17,7 +17,6 @@ package com.instructure.student.adapter
import android.content.Context
import android.view.View
-import com.instructure.student.events.ConversationUpdatedEvent
import com.instructure.student.holders.InboxMessageHolder
import com.instructure.student.interfaces.MessageAdapterCallback
import com.instructure.canvasapi2.managers.InboxManager
@@ -28,6 +27,7 @@ import com.instructure.canvasapi2.utils.weave.WeaveJob
import com.instructure.canvasapi2.utils.weave.awaitApi
import com.instructure.canvasapi2.utils.weave.catch
import com.instructure.canvasapi2.utils.weave.tryWeave
+import com.instructure.pandautils.utils.ConversationUpdatedEvent
import org.greenrobot.eventbus.EventBus
class InboxConversationAdapter(
diff --git a/apps/student/src/main/java/com/instructure/student/binding/BindingAdapters.kt b/apps/student/src/main/java/com/instructure/student/binding/BindingAdapters.kt
index 4c065934d4..a061f72839 100644
--- a/apps/student/src/main/java/com/instructure/student/binding/BindingAdapters.kt
+++ b/apps/student/src/main/java/com/instructure/student/binding/BindingAdapters.kt
@@ -16,13 +16,9 @@
package com.instructure.student.binding
-import androidx.annotation.ColorInt
import androidx.databinding.BindingAdapter
import com.google.android.material.tabs.TabLayout
import com.instructure.student.features.elementary.course.ElementaryCourseTab
-import com.instructure.student.mobius.assignmentDetails.ui.gradeCell.DonutChartView
-import com.instructure.student.mobius.assignmentDetails.ui.gradeCell.GradeCellViewState
-import com.instructure.student.mobius.assignmentDetails.ui.gradeCell.GradeStatisticsView
@BindingAdapter("tabs")
fun bindCourseTabs(tabLayout: TabLayout, tabs: List?) {
@@ -34,18 +30,3 @@ fun bindCourseTabs(tabLayout: TabLayout, tabs: List?) {
})
}
}
-
-@BindingAdapter("progress", "color", "trackColor")
-fun DonutChartView.setProgress(progress: Float, @ColorInt color: Int, @ColorInt trackColor: Int) {
- setColor(color)
- setTrackColor(trackColor)
- setPercentage(progress, true)
-}
-
-@BindingAdapter("stats", "color")
-fun GradeStatisticsView.setStatistics(stats: GradeCellViewState.GradeStats?, @ColorInt color: Int) {
- stats?.let {
- setStats(stats)
- setAccentColor(color)
- }
-}
diff --git a/apps/student/src/main/java/com/instructure/student/di/AlarmSchedulerModule.kt b/apps/student/src/main/java/com/instructure/student/di/AlarmSchedulerModule.kt
deleted file mode 100644
index ee3ad2f563..0000000000
--- a/apps/student/src/main/java/com/instructure/student/di/AlarmSchedulerModule.kt
+++ /dev/null
@@ -1,38 +0,0 @@
-/*
- * Copyright (C) 2024 - 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.di
-
-import android.content.Context
-import com.instructure.canvasapi2.utils.ApiPrefs
-import com.instructure.pandautils.room.appdatabase.daos.ReminderDao
-import com.instructure.student.features.assignments.reminder.AlarmScheduler
-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 AlarmSchedulerModule {
-
- @Provides
- fun provideAlarmScheduler(@ApplicationContext context: Context, reminderDao: ReminderDao, apiPrefs: ApiPrefs): AlarmScheduler {
- return AlarmScheduler(context, reminderDao, apiPrefs)
- }
-}
\ No newline at end of file
diff --git a/apps/student/src/main/java/com/instructure/student/di/LoginModule.kt b/apps/student/src/main/java/com/instructure/student/di/LoginModule.kt
index 5672e1660e..2cc049cb0d 100644
--- a/apps/student/src/main/java/com/instructure/student/di/LoginModule.kt
+++ b/apps/student/src/main/java/com/instructure/student/di/LoginModule.kt
@@ -20,7 +20,7 @@ import androidx.fragment.app.FragmentActivity
import com.instructure.loginapi.login.LoginNavigation
import com.instructure.loginapi.login.features.acceptableusepolicy.AcceptableUsePolicyRouter
import com.instructure.pandautils.room.offline.DatabaseProvider
-import com.instructure.student.features.assignments.reminder.AlarmScheduler
+import com.instructure.pandautils.features.assignments.details.reminder.AlarmScheduler
import com.instructure.student.features.login.StudentAcceptableUsePolicyRouter
import com.instructure.student.features.login.StudentLoginNavigation
import dagger.Module
diff --git a/apps/student/src/main/java/com/instructure/student/di/feature/AssignmentDetailsModule.kt b/apps/student/src/main/java/com/instructure/student/di/feature/AssignmentDetailsModule.kt
index 36ec1daf56..6ba70af567 100644
--- a/apps/student/src/main/java/com/instructure/student/di/feature/AssignmentDetailsModule.kt
+++ b/apps/student/src/main/java/com/instructure/student/di/feature/AssignmentDetailsModule.kt
@@ -17,21 +17,53 @@
package com.instructure.student.di.feature
-import com.instructure.canvasapi2.apis.*
+import com.instructure.canvasapi2.apis.AssignmentAPI
+import com.instructure.canvasapi2.apis.CourseAPI
+import com.instructure.canvasapi2.apis.QuizAPI
+import com.instructure.canvasapi2.apis.SubmissionAPI
+import com.instructure.pandautils.features.assignments.details.AssignmentDetailsBehaviour
+import com.instructure.pandautils.features.assignments.details.AssignmentDetailsColorProvider
+import com.instructure.pandautils.features.assignments.details.AssignmentDetailsRepository
+import com.instructure.pandautils.features.assignments.details.AssignmentDetailsRouter
+import com.instructure.pandautils.features.assignments.details.AssignmentDetailsSubmissionHandler
+import com.instructure.pandautils.receivers.alarm.AlarmReceiverNotificationHandler
import com.instructure.pandautils.room.appdatabase.daos.ReminderDao
import com.instructure.pandautils.room.offline.daos.QuizDao
import com.instructure.pandautils.room.offline.facade.AssignmentFacade
import com.instructure.pandautils.room.offline.facade.CourseFacade
+import com.instructure.pandautils.utils.ColorKeeper
import com.instructure.pandautils.utils.FeatureFlagProvider
import com.instructure.pandautils.utils.NetworkStateProvider
-import com.instructure.student.features.assignments.details.AssignmentDetailsRepository
+import com.instructure.student.features.assignments.details.StudentAssignmentDetailsBehaviour
+import com.instructure.student.features.assignments.details.StudentAssignmentDetailsColorProvider
+import com.instructure.student.features.assignments.details.StudentAssignmentDetailsRepository
+import com.instructure.student.features.assignments.details.StudentAssignmentDetailsRouter
+import com.instructure.student.features.assignments.details.StudentAssignmentDetailsSubmissionHandler
import com.instructure.student.features.assignments.details.datasource.AssignmentDetailsLocalDataSource
import com.instructure.student.features.assignments.details.datasource.AssignmentDetailsNetworkDataSource
+import com.instructure.student.features.assignments.details.receiver.StudentAlarmReceiverNotificationHandler
+import com.instructure.student.mobius.common.ui.SubmissionHelper
+import com.instructure.student.room.StudentDb
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
+import dagger.hilt.android.components.FragmentComponent
import dagger.hilt.android.components.ViewModelComponent
+import dagger.hilt.components.SingletonComponent
+@Module
+@InstallIn(FragmentComponent::class)
+class AssignmentDetailsFragmentModule {
+ @Provides
+ fun provideAssignmentDetailsRouter(): AssignmentDetailsRouter {
+ return StudentAssignmentDetailsRouter()
+ }
+
+ @Provides
+ fun provideAssignmentDetailsBehaviour(router: AssignmentDetailsRouter): AssignmentDetailsBehaviour {
+ return StudentAssignmentDetailsBehaviour(router)
+ }
+}
@Module
@InstallIn(ViewModelComponent::class)
class AssignmentDetailsModule {
@@ -56,13 +88,32 @@ class AssignmentDetailsModule {
}
@Provides
- fun provideCourseBrowserRepository(
+ fun provideAssignmentDetailsRepository(
networkStateProvider: NetworkStateProvider,
localDataSource: AssignmentDetailsLocalDataSource,
networkDataSource: AssignmentDetailsNetworkDataSource,
featureFlagProvider: FeatureFlagProvider,
reminderDao: ReminderDao
): AssignmentDetailsRepository {
- return AssignmentDetailsRepository(localDataSource, networkDataSource, networkStateProvider, featureFlagProvider, reminderDao)
+ return StudentAssignmentDetailsRepository(localDataSource, networkDataSource, networkStateProvider, featureFlagProvider, reminderDao)
+ }
+
+ @Provides
+ fun provideAssignmentDetailsSubmissionHandler(submissionHandler: SubmissionHelper, studentDb: StudentDb): AssignmentDetailsSubmissionHandler {
+ return StudentAssignmentDetailsSubmissionHandler(submissionHandler, studentDb)
+ }
+
+ @Provides
+ fun provideAssignmentDetailsColorProvider(colorKeeper: ColorKeeper): AssignmentDetailsColorProvider {
+ return StudentAssignmentDetailsColorProvider(colorKeeper)
}
}
+
+@Module
+@InstallIn(SingletonComponent::class)
+class AssignmentDetailsSingletonModule {
+ @Provides
+ fun provideAssignmentDetailsNotificationHandler(): AlarmReceiverNotificationHandler {
+ return StudentAlarmReceiverNotificationHandler()
+ }
+}
\ No newline at end of file
diff --git a/apps/student/src/main/java/com/instructure/student/events/RationedBusEvent.kt b/apps/student/src/main/java/com/instructure/student/events/RationedBusEvent.kt
index 99fa0e7e5c..125073df61 100644
--- a/apps/student/src/main/java/com/instructure/student/events/RationedBusEvent.kt
+++ b/apps/student/src/main/java/com/instructure/student/events/RationedBusEvent.kt
@@ -17,7 +17,11 @@
@file:Suppress("unused")
package com.instructure.student.events
-import com.instructure.canvasapi2.models.*
+import com.instructure.canvasapi2.models.DiscussionTopicHeader
+import com.instructure.canvasapi2.models.ModuleObject
+import com.instructure.canvasapi2.models.Page
+import com.instructure.canvasapi2.models.Recipient
+import com.instructure.canvasapi2.models.User
import org.greenrobot.eventbus.EventBus
/**
@@ -115,9 +119,6 @@ fun RationedBusEvent<*>.post() = EventBus.getDefault().post(this)
/** A RationedBusEvent for a User. @see [RationedBusEvent] */
class UserUpdatedEvent(user: User, skipId: String? = null) : RationedBusEvent(user, skipId)
-/** A RationedBusEvent for a Conversation. @see [RationedBusEvent] */
-class ConversationUpdatedEvent(conversation: Conversation, skipId: String? = null) : RationedBusEvent(conversation, skipId)
-
/** A RationedBusEvent adding a new message to the MessageThreadFragment. @see [RationedBusEvent] */
class MessageAddedEvent(shouldUpdate: Boolean, skipId: String? = null) : RationedBusEvent(shouldUpdate, skipId)
diff --git a/apps/student/src/main/java/com/instructure/student/features/assignments/details/AssignmentDetailsRepository.kt b/apps/student/src/main/java/com/instructure/student/features/assignments/details/AssignmentDetailsRepository.kt
index 475df251da..45ab5f7a31 100644
--- a/apps/student/src/main/java/com/instructure/student/features/assignments/details/AssignmentDetailsRepository.kt
+++ b/apps/student/src/main/java/com/instructure/student/features/assignments/details/AssignmentDetailsRepository.kt
@@ -22,6 +22,7 @@ import com.instructure.canvasapi2.models.Assignment
import com.instructure.canvasapi2.models.Course
import com.instructure.canvasapi2.models.LTITool
import com.instructure.canvasapi2.models.Quiz
+import com.instructure.pandautils.features.assignments.details.AssignmentDetailsRepository
import com.instructure.pandautils.repository.Repository
import com.instructure.pandautils.room.appdatabase.daos.ReminderDao
import com.instructure.pandautils.room.appdatabase.entities.ReminderEntity
@@ -31,43 +32,43 @@ import com.instructure.student.features.assignments.details.datasource.Assignmen
import com.instructure.student.features.assignments.details.datasource.AssignmentDetailsLocalDataSource
import com.instructure.student.features.assignments.details.datasource.AssignmentDetailsNetworkDataSource
-class AssignmentDetailsRepository(
+class StudentAssignmentDetailsRepository(
localDataSource: AssignmentDetailsLocalDataSource,
networkDataSource: AssignmentDetailsNetworkDataSource,
networkStateProvider: NetworkStateProvider,
featureFlagProvider: FeatureFlagProvider,
private val reminderDao: ReminderDao
-) : Repository(localDataSource, networkDataSource, networkStateProvider, featureFlagProvider) {
+) : Repository(localDataSource, networkDataSource, networkStateProvider, featureFlagProvider), AssignmentDetailsRepository {
- suspend fun getCourseWithGrade(courseId: Long, forceNetwork: Boolean): Course {
+ override suspend fun getCourseWithGrade(courseId: Long, forceNetwork: Boolean): Course {
return dataSource().getCourseWithGrade(courseId, forceNetwork)
}
- suspend fun getAssignment(isObserver: Boolean, assignmentId: Long, courseId: Long, forceNetwork: Boolean): Assignment {
+ override suspend fun getAssignment(isObserver: Boolean, assignmentId: Long, courseId: Long, forceNetwork: Boolean): Assignment {
return dataSource().getAssignment(isObserver, assignmentId, courseId, forceNetwork)
}
- suspend fun getQuiz(courseId: Long, quizId: Long, forceNetwork: Boolean): Quiz {
+ override suspend fun getQuiz(courseId: Long, quizId: Long, forceNetwork: Boolean): Quiz {
return dataSource().getQuiz(courseId, quizId, forceNetwork)
}
- suspend fun getExternalToolLaunchUrl(courseId: Long, externalToolId: Long, assignmentId: Long, forceNetwork: Boolean): LTITool? {
+ override suspend fun getExternalToolLaunchUrl(courseId: Long, externalToolId: Long, assignmentId: Long, forceNetwork: Boolean): LTITool? {
return dataSource().getExternalToolLaunchUrl(courseId, externalToolId, assignmentId, forceNetwork)
}
- suspend fun getLtiFromAuthenticationUrl(url: String, forceNetwork: Boolean): LTITool? {
+ override suspend fun getLtiFromAuthenticationUrl(url: String, forceNetwork: Boolean): LTITool? {
return dataSource().getLtiFromAuthenticationUrl(url, forceNetwork)
}
- fun getRemindersByAssignmentIdLiveData(userId: Long, assignmentId: Long): LiveData> {
+ override fun getRemindersByAssignmentIdLiveData(userId: Long, assignmentId: Long): LiveData> {
return reminderDao.findByAssignmentIdLiveData(userId, assignmentId)
}
- suspend fun deleteReminderById(id: Long) {
+ override suspend fun deleteReminderById(id: Long) {
reminderDao.deleteById(id)
}
- suspend fun addReminder(userId: Long, assignment: Assignment, text: String, time: Long) = reminderDao.insert(
+ override suspend fun addReminder(userId: Long, assignment: Assignment, text: String, time: Long) = reminderDao.insert(
ReminderEntity(
userId = userId,
assignmentId = assignment.id,
diff --git a/apps/student/src/main/java/com/instructure/student/features/assignments/details/StudentAssignmentDetailsBehaviour.kt b/apps/student/src/main/java/com/instructure/student/features/assignments/details/StudentAssignmentDetailsBehaviour.kt
new file mode 100644
index 0000000000..cdb2d9065d
--- /dev/null
+++ b/apps/student/src/main/java/com/instructure/student/features/assignments/details/StudentAssignmentDetailsBehaviour.kt
@@ -0,0 +1,239 @@
+/*
+ * Copyright (C) 2024 - 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.assignments.details
+
+import android.app.Dialog
+import android.view.LayoutInflater
+import android.view.MenuItem
+import android.view.View
+import android.widget.Toast
+import androidx.appcompat.app.AlertDialog
+import androidx.appcompat.widget.Toolbar
+import androidx.fragment.app.FragmentActivity
+import com.instructure.canvasapi2.models.Assignment
+import com.instructure.canvasapi2.models.CanvasContext
+import com.instructure.canvasapi2.models.Course
+import com.instructure.canvasapi2.models.LTITool
+import com.instructure.canvasapi2.utils.APIHelper
+import com.instructure.canvasapi2.utils.Analytics
+import com.instructure.canvasapi2.utils.AnalyticsEventConstants
+import com.instructure.interactions.Navigation
+import com.instructure.interactions.bookmarks.Bookmarker
+import com.instructure.pandautils.databinding.FragmentAssignmentDetailsBinding
+import com.instructure.pandautils.features.assignments.details.AssignmentDetailsBehaviour
+import com.instructure.pandautils.features.assignments.details.AssignmentDetailsRouter
+import com.instructure.pandautils.utils.ThemePrefs
+import com.instructure.pandautils.utils.ViewStyler
+import com.instructure.pandautils.utils.setVisible
+import com.instructure.pandautils.utils.setupAsBackButton
+import com.instructure.pandautils.utils.toast
+import com.instructure.pandautils.views.RecordingMediaType
+import com.instructure.student.R
+import com.instructure.student.databinding.DialogSubmissionPickerBinding
+import com.instructure.student.databinding.DialogSubmissionPickerMediaBinding
+import com.instructure.student.fragment.StudioWebViewFragment
+import com.instructure.student.mobius.assignmentDetails.launchAudio
+import com.instructure.student.router.RouteMatcher
+import com.instructure.student.util.getResourceSelectorUrl
+import java.io.File
+
+class StudentAssignmentDetailsBehaviour (
+ private val router: AssignmentDetailsRouter,
+): AssignmentDetailsBehaviour() {
+ override val dialogColor: Int = ThemePrefs.textButtonColor
+
+ override fun showMediaDialog(
+ activity: FragmentActivity,
+ binding: FragmentAssignmentDetailsBinding?,
+ recordCallback: (File?) -> Unit,
+ startVideoCapture: () -> Unit,
+ onLaunchMediaPicker: () -> Unit,
+ ) {
+ Analytics.logEvent(AnalyticsEventConstants.SUBMIT_MEDIARECORDING_SELECTED)
+ val builder = AlertDialog.Builder(activity)
+ val dialogBinding = DialogSubmissionPickerMediaBinding.inflate(LayoutInflater.from(activity))
+ val dialog = builder.setView(dialogBinding.root).create()
+
+ dialog.setOnShowListener {
+ setupDialogRow(dialog, dialogBinding.submissionEntryAudio, true) {
+ activity.launchAudio({ activity.toast(R.string.permissionDenied) }) {
+ showAudioRecordingView(binding, recordCallback)
+ }
+ }
+ setupDialogRow(dialog, dialogBinding.submissionEntryVideo, true) {
+ startVideoCapture()
+ }
+ setupDialogRow(dialog, dialogBinding.submissionEntryMediaFile, true) {
+ onLaunchMediaPicker()
+ }
+ }
+ dialog.show()
+ }
+
+
+ private fun showAudioRecordingView(binding: FragmentAssignmentDetailsBinding?, recordCallback: (File?) -> Unit) {
+ binding?.floatingRecordingView?.apply {
+ setContentType(RecordingMediaType.Audio)
+ setVisible()
+ stoppedCallback = {}
+ recordingCallback = {
+ recordCallback(it)
+ }
+ }
+ }
+
+ override fun showSubmitDialog(
+ activity: FragmentActivity,
+ binding: FragmentAssignmentDetailsBinding?,
+ recordCallback: (File?) -> Unit,
+ startVideoCapture: () -> Unit,
+ onLaunchMediaPicker: () -> Unit,
+ assignment: Assignment,
+ course: Course,
+ isStudioEnabled: Boolean,
+ studioLTITool: LTITool?
+ ) {
+ val builder = AlertDialog.Builder(activity)
+ val dialogBinding = DialogSubmissionPickerBinding.inflate(LayoutInflater.from(activity))
+ val dialog = builder.setView(dialogBinding.root).create()
+ val submissionTypes = assignment.getSubmissionTypes()
+
+ dialog.setOnShowListener {
+ setupDialogRow(dialog, dialogBinding.submissionEntryText, submissionTypes.contains(
+ Assignment.SubmissionType.ONLINE_TEXT_ENTRY)) {
+ router.navigateToTextEntryScreen(
+ activity,
+ course,
+ assignment.id,
+ assignment.name.orEmpty(),
+ )
+ }
+ setupDialogRow(dialog, dialogBinding.submissionEntryWebsite, submissionTypes.contains(
+ Assignment.SubmissionType.ONLINE_URL)) {
+ router.navigateToUrlSubmissionScreen(
+ activity,
+ course,
+ assignment.id,
+ assignment.name.orEmpty(),
+ null,
+ false
+ )
+ }
+ setupDialogRow(dialog, dialogBinding.submissionEntryFile, submissionTypes.contains(
+ Assignment.SubmissionType.ONLINE_UPLOAD)) {
+ router.navigateToUploadScreen(activity, course, assignment)
+ }
+ setupDialogRow(dialog, dialogBinding.submissionEntryMedia, submissionTypes.contains(
+ Assignment.SubmissionType.MEDIA_RECORDING)) {
+ showMediaDialog(activity, binding, recordCallback, startVideoCapture, onLaunchMediaPicker)
+ }
+ setupDialogRow(
+ dialog,
+ dialogBinding.submissionEntryStudio,
+ isStudioEnabled
+ ) {
+ navigateToStudioScreen(activity, course, assignment, studioLTITool)
+ }
+ setupDialogRow(dialog, dialogBinding.submissionEntryStudentAnnotation, submissionTypes.contains(
+ Assignment.SubmissionType.STUDENT_ANNOTATION)) {
+ assignment.submission?.id?.let{
+ router.navigateToAnnotationSubmissionScreen(
+ activity,
+ course,
+ assignment.annotatableAttachmentId,
+ it,
+ assignment.id,
+ assignment.name.orEmpty())
+ }
+ }
+ }
+ dialog.show()
+ }
+
+ private fun setupDialogRow(dialog: Dialog, view: View, visibility: Boolean, onClick: () -> Unit) {
+ view.setVisible(visibility)
+ view.setOnClickListener {
+ onClick()
+ dialog.cancel()
+ }
+ }
+
+ private fun navigateToStudioScreen(activity: FragmentActivity, canvasContext: CanvasContext, assignment: Assignment, studioLTITool: LTITool?) {
+ Analytics.logEvent(AnalyticsEventConstants.SUBMIT_STUDIO_SELECTED)
+ RouteMatcher.route(
+ activity,
+ StudioWebViewFragment.makeRoute(
+ canvasContext,
+ studioLTITool?.getResourceSelectorUrl(canvasContext, assignment).orEmpty(),
+ studioLTITool?.name.orEmpty(),
+ true,
+ assignment
+ )
+ )
+ }
+
+ override fun applyTheme(
+ activity: FragmentActivity,
+ binding: FragmentAssignmentDetailsBinding?,
+ bookmark: Bookmarker,
+ course: Course?,
+ toolbar: Toolbar
+ ) {
+ binding?.toolbar?.apply {
+ setupAsBackButton {
+ activity.onBackPressed()
+ }
+
+ title = activity.getString(R.string.assignmentDetails)
+ subtitle = course?.name
+
+ setupToolbarMenu(activity, bookmark, toolbar)
+
+ ViewStyler.themeToolbarColored(activity, this, course)
+ }
+ }
+
+ private fun setupToolbarMenu(activity: FragmentActivity, bookmark: Bookmarker, toolbar: Toolbar) {
+ addBookmarkMenuIfAllowed(activity, bookmark, toolbar)
+ addOnMenuItemClickListener(activity, toolbar)
+ }
+
+ private fun addBookmarkMenuIfAllowed(activity: FragmentActivity, bookmark: Bookmarker, toolbar: Toolbar) {
+ val navigation = activity as? Navigation
+ val bookmarkFeatureAllowed = navigation?.canBookmark() ?: false
+ if (bookmarkFeatureAllowed && bookmark.canBookmark && toolbar.menu.findItem(
+ R.id.bookmark) == null) {
+ toolbar.inflateMenu(R.menu.bookmark_menu)
+ }
+ }
+
+ private fun addOnMenuItemClickListener(activity: FragmentActivity, toolbar: Toolbar) {
+ toolbar.setOnMenuItemClickListener { item -> onOptionsItemSelected(activity, item) }
+ }
+
+ override fun onOptionsItemSelected(activity: FragmentActivity, item: MenuItem): Boolean {
+ if (item.itemId == R.id.bookmark) {
+ if (APIHelper.hasNetworkConnection()) {
+ (activity as? Navigation)?.addBookmark()
+ } else {
+ Toast.makeText(activity, activity.getString(com.instructure.pandautils.R.string.notAvailableOffline), Toast.LENGTH_SHORT).show()
+ }
+ return true
+ }
+ return false
+ }
+}
\ No newline at end of file
diff --git a/apps/student/src/main/java/com/instructure/student/features/assignments/details/StudentAssignmentDetailsColorProvider.kt b/apps/student/src/main/java/com/instructure/student/features/assignments/details/StudentAssignmentDetailsColorProvider.kt
new file mode 100644
index 0000000000..bc895043b2
--- /dev/null
+++ b/apps/student/src/main/java/com/instructure/student/features/assignments/details/StudentAssignmentDetailsColorProvider.kt
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2024 - 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.assignments.details
+
+import androidx.annotation.ColorInt
+import com.instructure.canvasapi2.models.Course
+import com.instructure.pandautils.features.assignments.details.AssignmentDetailsColorProvider
+import com.instructure.pandautils.utils.ColorKeeper
+import com.instructure.pandautils.utils.ThemePrefs
+import com.instructure.pandautils.utils.ThemedColor
+
+class StudentAssignmentDetailsColorProvider(
+ private val colorKeeper: ColorKeeper
+): AssignmentDetailsColorProvider() {
+ @ColorInt
+ override val submissionAndRubricLabelColor: Int = ThemePrefs.textButtonColor
+
+ override fun getContentColor(course: Course?): ThemedColor {
+ return colorKeeper.getOrGenerateColor(course)
+ }
+}
\ No newline at end of file
diff --git a/apps/student/src/main/java/com/instructure/student/features/assignments/details/StudentAssignmentDetailsRouter.kt b/apps/student/src/main/java/com/instructure/student/features/assignments/details/StudentAssignmentDetailsRouter.kt
new file mode 100644
index 0000000000..042543792b
--- /dev/null
+++ b/apps/student/src/main/java/com/instructure/student/features/assignments/details/StudentAssignmentDetailsRouter.kt
@@ -0,0 +1,193 @@
+/*
+ * Copyright (C) 2024 - 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.features.assignments.details
+
+import android.net.Uri
+import android.os.Bundle
+import androidx.fragment.app.FragmentActivity
+import com.instructure.canvasapi2.models.Assignment
+import com.instructure.canvasapi2.models.CanvasContext
+import com.instructure.canvasapi2.models.LTITool
+import com.instructure.canvasapi2.models.Quiz
+import com.instructure.canvasapi2.models.RemoteFile
+import com.instructure.canvasapi2.utils.Analytics
+import com.instructure.canvasapi2.utils.AnalyticsEventConstants
+import com.instructure.pandautils.features.assignments.details.AssignmentDetailsRouter
+import com.instructure.pandautils.features.discussion.router.DiscussionRouterFragment
+import com.instructure.student.activity.BaseRouterActivity
+import com.instructure.student.fragment.BasicQuizViewFragment
+import com.instructure.student.fragment.LtiLaunchFragment
+import com.instructure.student.mobius.assignmentDetails.submission.annnotation.AnnotationSubmissionUploadFragment
+import com.instructure.student.mobius.assignmentDetails.submission.file.ui.UploadStatusSubmissionFragment
+import com.instructure.student.mobius.assignmentDetails.submission.picker.PickerSubmissionMode
+import com.instructure.student.mobius.assignmentDetails.submission.picker.ui.PickerSubmissionUploadFragment
+import com.instructure.student.mobius.assignmentDetails.submission.text.ui.TextSubmissionUploadFragment
+import com.instructure.student.mobius.assignmentDetails.submission.url.ui.UrlSubmissionUploadFragment
+import com.instructure.student.mobius.assignmentDetails.submissionDetails.ui.SubmissionDetailsRepositoryFragment
+import com.instructure.student.router.RouteMatcher
+
+class StudentAssignmentDetailsRouter: AssignmentDetailsRouter() {
+ override fun navigateToAssignmentUploadPicker(
+ activity: FragmentActivity,
+ canvasContext: CanvasContext,
+ assignment: Assignment,
+ mediaUri: Uri
+ ) {
+ RouteMatcher.route(
+ activity,
+ PickerSubmissionUploadFragment.makeRoute(canvasContext, assignment, mediaUri)
+ )
+ }
+
+ override fun navigateToSubmissionScreen(
+ activity: FragmentActivity,
+ course: CanvasContext,
+ assignmentId: Long,
+ isObserver: Boolean,
+ initialSelectedSubmissionAttempt: Long?
+ ) {
+ RouteMatcher.route(
+ activity,
+ SubmissionDetailsRepositoryFragment.makeRoute(course, assignmentId, isObserver, initialSelectedSubmissionAttempt)
+ )
+ }
+
+ override fun navigateToQuizScreen(
+ activity: FragmentActivity,
+ canvasContext: CanvasContext,
+ quiz: Quiz,
+ url: String
+ ) {
+ RouteMatcher.route(activity, BasicQuizViewFragment.makeRoute(canvasContext, quiz, url))
+ }
+
+ override fun navigateToDiscussionScreen(
+ activity: FragmentActivity,
+ canvasContext: CanvasContext,
+ discussionTopicHeaderId: Long,
+ isAnnouncement: Boolean
+ ) {
+ RouteMatcher.route(activity, DiscussionRouterFragment.makeRoute(canvasContext, discussionTopicHeaderId, isAnnouncement))
+ }
+
+ override fun navigateToUploadScreen(
+ activity: FragmentActivity,
+ canvasContext: CanvasContext,
+ assignment: Assignment,
+ attemptId: Long?
+ ) {
+ Analytics.logEvent(AnalyticsEventConstants.SUBMIT_FILEUPLOAD_SELECTED)
+ RouteMatcher.route(
+ activity,
+ PickerSubmissionUploadFragment.makeRoute(canvasContext, assignment, PickerSubmissionMode.FileSubmission)
+ )
+ }
+
+ override fun navigateToTextEntryScreen(
+ activity: FragmentActivity,
+ course: CanvasContext,
+ assignmentId: Long,
+ assignmentName: String?,
+ initialText: String?,
+ isFailure: Boolean
+ ) {
+ Analytics.logEvent(AnalyticsEventConstants.SUBMIT_TEXTENTRY_SELECTED)
+ RouteMatcher.route(
+ activity,
+ TextSubmissionUploadFragment.makeRoute(course, assignmentId, assignmentName, initialText, isFailure)
+ )
+ }
+
+ override fun navigateToUrlSubmissionScreen(
+ activity: FragmentActivity,
+ course: CanvasContext,
+ assignmentId: Long,
+ assignmentName: String?,
+ initialUrl: String?,
+ isFailure: Boolean
+ ) {
+ Analytics.logEvent(AnalyticsEventConstants.SUBMIT_ONLINEURL_SELECTED)
+ RouteMatcher.route(
+ activity,
+ UrlSubmissionUploadFragment.makeRoute(course, assignmentId, assignmentName, initialUrl, isFailure)
+ )
+ }
+
+ override fun navigateToAnnotationSubmissionScreen(
+ activity: FragmentActivity,
+ canvasContext: CanvasContext,
+ annotatableAttachmentId: Long,
+ submissionId: Long,
+ assignmentId: Long,
+ assignmentName: String
+ ) {
+ Analytics.logEvent(AnalyticsEventConstants.SUBMIT_STUDENT_ANNOTATION_SELECTED)
+ RouteMatcher.route(
+ activity,
+ AnnotationSubmissionUploadFragment.makeRoute(
+ canvasContext,
+ annotatableAttachmentId,
+ submissionId,
+ assignmentId,
+ assignmentName
+ )
+ )
+ }
+
+ override fun navigateToLtiLaunchScreen(
+ activity: FragmentActivity,
+ canvasContext: CanvasContext,
+ url: String,
+ title: String?,
+ sessionLessLaunch: Boolean,
+ isAssignmentLTI: Boolean,
+ ltiTool: LTITool?
+ ) {
+ RouteMatcher.route(
+ activity,
+ LtiLaunchFragment.makeRoute(
+ canvasContext,
+ url,
+ title,
+ sessionLessLaunch = sessionLessLaunch,
+ isAssignmentLTI = isAssignmentLTI,
+ ltiTool = ltiTool
+ )
+ )
+ }
+
+ override fun navigateToUploadStatusScreen(activity: FragmentActivity, submissionId: Long) {
+ RouteMatcher.route(activity, UploadStatusSubmissionFragment.makeRoute(submissionId))
+ }
+
+ override fun navigateToDiscussionAttachmentScreen(activity: FragmentActivity, canvasContext: CanvasContext, attachment: RemoteFile) {
+ (activity as? BaseRouterActivity)?.openMedia(
+ canvasContext,
+ attachment.contentType.orEmpty(),
+ attachment.url.orEmpty(),
+ attachment.fileName.orEmpty()
+ )
+ }
+
+ override fun navigateToUrl(
+ activity: FragmentActivity,
+ url: String,
+ domain: String,
+ extras: Bundle?
+ ) {
+ RouteMatcher.routeUrl(activity, url, domain, extras)
+ }
+}
\ No newline at end of file
diff --git a/apps/student/src/main/java/com/instructure/student/features/assignments/details/StudentAssignmentDetailsSubmissionHandler.kt b/apps/student/src/main/java/com/instructure/student/features/assignments/details/StudentAssignmentDetailsSubmissionHandler.kt
new file mode 100644
index 0000000000..4e63c4f653
--- /dev/null
+++ b/apps/student/src/main/java/com/instructure/student/features/assignments/details/StudentAssignmentDetailsSubmissionHandler.kt
@@ -0,0 +1,155 @@
+/*
+ * Copyright (C) 2024 - 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.features.assignments.details
+
+import android.content.Context
+import android.content.res.Resources
+import android.net.Uri
+import androidx.fragment.app.FragmentActivity
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.Observer
+import com.instructure.canvasapi2.models.Assignment
+import com.instructure.canvasapi2.models.Course
+import com.instructure.canvasapi2.models.LTITool
+import com.instructure.pandautils.BR
+import com.instructure.pandautils.features.assignmentdetails.AssignmentDetailsAttemptItemViewModel
+import com.instructure.pandautils.features.assignmentdetails.AssignmentDetailsAttemptViewData
+import com.instructure.pandautils.features.assignments.details.AssignmentDetailsSubmissionHandler
+import com.instructure.pandautils.features.assignments.details.AssignmentDetailsViewData
+import com.instructure.pandautils.utils.toFormattedString
+import com.instructure.pandautils.utils.toast
+import com.instructure.student.R
+import com.instructure.student.mobius.assignmentDetails.getVideoUri
+import com.instructure.student.mobius.assignmentDetails.uploadAudioRecording
+import com.instructure.student.mobius.common.ui.SubmissionHelper
+import com.instructure.student.room.StudentDb
+import com.instructure.student.room.entities.CreateSubmissionEntity
+import com.instructure.student.util.getStudioLTITool
+import java.io.File
+import java.util.Date
+
+class StudentAssignmentDetailsSubmissionHandler(
+ private val submissionHelper: SubmissionHelper,
+ private val studentDb: StudentDb
+) : AssignmentDetailsSubmissionHandler {
+ override var isUploading: Boolean = false
+ override var lastSubmissionAssignmentId: Long? = null
+ override var lastSubmissionSubmissionType: String? = null
+ override var lastSubmissionIsDraft: Boolean = false
+ override var lastSubmissionEntry: String? = null
+
+ private var submissionLiveData: LiveData>? = null
+
+ private var submissionObserver: Observer>? = null
+
+ override fun addAssignmentSubmissionObserver(
+ assignmentId: Long,
+ userId: Long,
+ resources: Resources,
+ data: MutableLiveData,
+ refreshAssignment: () -> Unit,
+ ) {
+ submissionLiveData = studentDb.submissionDao().findSubmissionsByAssignmentIdLiveData(assignmentId, userId)
+
+ setupObserver(resources, data, refreshAssignment)
+
+ submissionObserver?.let { observer ->
+ submissionLiveData?.observeForever(observer)
+ }
+ }
+
+ override fun removeAssignmentSubmissionObserver() {
+ submissionObserver?.let { observer ->
+ submissionLiveData?.removeObserver(observer)
+ }
+ }
+
+ override fun uploadAudioSubmission(context: Context?, course: Course?, assignment: Assignment?, file: File?) {
+ if (context != null && file != null && assignment != null && course != null) {
+ uploadAudioRecording(submissionHelper, file, assignment, course)
+ } else {
+ context?.let {
+ context.toast(context.getString(R.string.audioRecordingError))
+ }
+ }
+ }
+
+ override fun getVideoUri(fragment: FragmentActivity): Uri? = fragment.getVideoUri()
+
+ override suspend fun getStudioLTITool(assignment: Assignment, courseId: Long?): LTITool? {
+ return if (assignment.getSubmissionTypes().contains(Assignment.SubmissionType.ONLINE_UPLOAD)) {
+ courseId?.getStudioLTITool()?.dataOrNull
+ } else null
+ }
+
+ private fun setupObserver(
+ resources: Resources,
+ data: MutableLiveData,
+ refreshAssignment: () -> Unit,
+ ) {
+ submissionObserver = Observer> { submissions ->
+ val submission = submissions.lastOrNull()
+ lastSubmissionAssignmentId = submission?.assignmentId
+ lastSubmissionSubmissionType = submission?.submissionType
+ lastSubmissionIsDraft = submission?.isDraft ?: false
+ lastSubmissionEntry = submission?.submissionEntry
+
+ val attempts = data.value?.attempts
+ submission?.let { dbSubmission ->
+ val isDraft = dbSubmission.isDraft
+ data.value?.hasDraft = isDraft
+ data.value?.notifyPropertyChanged(BR.hasDraft)
+
+ val dateString = (dbSubmission.lastActivityDate?.toInstant()?.toEpochMilli()?.let { Date(it) } ?: Date()).toFormattedString()
+ if (!isDraft && !isUploading) {
+ isUploading = true
+ data.value?.attempts = attempts?.toMutableList()?.apply {
+ add(
+ 0, AssignmentDetailsAttemptItemViewModel(
+ AssignmentDetailsAttemptViewData(
+ resources.getString(R.string.attempt, attempts.size + 1),
+ dateString,
+ isUploading = true
+ )
+ )
+ )
+ }.orEmpty()
+ data.value?.notifyPropertyChanged(BR.attempts)
+ }
+ if (isUploading && submission.errorFlag) {
+ data.value?.attempts = attempts?.toMutableList()?.apply {
+ if (isNotEmpty()) removeFirst()
+ add(0, AssignmentDetailsAttemptItemViewModel(
+ AssignmentDetailsAttemptViewData(
+ resources.getString(R.string.attempt, attempts.size),
+ dateString,
+ isFailed = true
+ )
+ )
+ )
+ }.orEmpty()
+ data.value?.notifyPropertyChanged(BR.attempts)
+ }
+ } ?: run {
+ if (isUploading) {
+ isUploading = false
+ refreshAssignment()
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/apps/student/src/main/java/com/instructure/student/features/assignments/details/receiver/StudentAlarmReceiverNotificationHandler.kt b/apps/student/src/main/java/com/instructure/student/features/assignments/details/receiver/StudentAlarmReceiverNotificationHandler.kt
new file mode 100644
index 0000000000..1a13b8072a
--- /dev/null
+++ b/apps/student/src/main/java/com/instructure/student/features/assignments/details/receiver/StudentAlarmReceiverNotificationHandler.kt
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2024 - 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.features.assignments.details.receiver
+
+import android.app.NotificationChannel
+import android.app.NotificationManager
+import android.app.PendingIntent
+import android.content.Context
+import android.content.Intent
+import androidx.core.app.NotificationCompat
+import com.instructure.pandautils.models.PushNotification
+import com.instructure.pandautils.receivers.alarm.AlarmReceiver
+import com.instructure.pandautils.receivers.alarm.AlarmReceiverNotificationHandler
+import com.instructure.pandautils.utils.Const
+import com.instructure.student.R
+import com.instructure.student.activity.NavigationActivity
+
+class StudentAlarmReceiverNotificationHandler: AlarmReceiverNotificationHandler {
+ override fun showNotification(context: Context, assignmentId: Long, assignmentPath: String, assignmentName: String, dueIn: String) {
+ val intent = Intent(context, NavigationActivity.startActivityClass).apply {
+ putExtra(Const.LOCAL_NOTIFICATION, true)
+ putExtra(PushNotification.HTML_URL, assignmentPath)
+ }
+
+ val pendingIntent = PendingIntent.getActivity(
+ context, 0, intent,
+ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
+ )
+
+ val builder = NotificationCompat.Builder(context, AlarmReceiver.CHANNEL_ID)
+ .setSmallIcon(R.drawable.ic_notification_canvas_logo)
+ .setContentTitle(context.getString(R.string.reminderNotificationTitle))
+ .setContentText(context.getString(R.string.reminderNotificationDescription, dueIn, assignmentName))
+ .setAutoCancel(true)
+ .setContentIntent(pendingIntent)
+ .setPriority(NotificationCompat.PRIORITY_DEFAULT)
+
+ val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
+ notificationManager.notify(assignmentId.toInt(), builder.build())
+ }
+
+ override fun createNotificationChannel(context: Context) {
+ val channel = NotificationChannel(
+ AlarmReceiver.CHANNEL_ID,
+ context.getString(R.string.reminderNotificationChannelName),
+ NotificationManager.IMPORTANCE_DEFAULT
+ ).apply {
+ description = context.getString(R.string.reminderNotificationChannelDescription)
+ }
+
+ val notificationManager: NotificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
+ notificationManager.createNotificationChannel(channel)
+ }
+}
\ No newline at end of file
diff --git a/apps/student/src/main/java/com/instructure/student/features/assignments/list/AssignmentListFragment.kt b/apps/student/src/main/java/com/instructure/student/features/assignments/list/AssignmentListFragment.kt
index cac1ea6e68..f77c7fa268 100644
--- a/apps/student/src/main/java/com/instructure/student/features/assignments/list/AssignmentListFragment.kt
+++ b/apps/student/src/main/java/com/instructure/student/features/assignments/list/AssignmentListFragment.kt
@@ -59,7 +59,7 @@ import com.instructure.pandautils.utils.withArgs
import com.instructure.student.R
import com.instructure.student.adapter.TermSpinnerAdapter
import com.instructure.student.databinding.AssignmentListLayoutBinding
-import com.instructure.student.features.assignments.details.AssignmentDetailsFragment
+import com.instructure.pandautils.features.assignments.details.AssignmentDetailsFragment
import com.instructure.student.features.assignments.list.adapter.AssignmentListByDateRecyclerAdapter
import com.instructure.student.features.assignments.list.adapter.AssignmentListByTypeRecyclerAdapter
import com.instructure.student.features.assignments.list.adapter.AssignmentListFilter
diff --git a/apps/student/src/main/java/com/instructure/student/features/calendar/StudentCalendarRepository.kt b/apps/student/src/main/java/com/instructure/student/features/calendar/StudentCalendarRepository.kt
index d288929a5d..32f6d25237 100644
--- a/apps/student/src/main/java/com/instructure/student/features/calendar/StudentCalendarRepository.kt
+++ b/apps/student/src/main/java/com/instructure/student/features/calendar/StudentCalendarRepository.kt
@@ -38,7 +38,7 @@ class StudentCalendarRepository(
private val groupsApi: GroupAPI.GroupInterface,
private val apiPrefs: ApiPrefs,
private val calendarFilterDao: CalendarFilterDao
-) : CalendarRepository {
+) : CalendarRepository() {
override suspend fun getPlannerItems(
startDate: String,
diff --git a/apps/student/src/main/java/com/instructure/student/features/calendar/StudentCalendarRouter.kt b/apps/student/src/main/java/com/instructure/student/features/calendar/StudentCalendarRouter.kt
index da47b081eb..55257f42cc 100644
--- a/apps/student/src/main/java/com/instructure/student/features/calendar/StudentCalendarRouter.kt
+++ b/apps/student/src/main/java/com/instructure/student/features/calendar/StudentCalendarRouter.kt
@@ -29,7 +29,7 @@ import com.instructure.pandautils.features.calendartodo.createupdate.CreateUpdat
import com.instructure.pandautils.features.calendartodo.details.ToDoFragment
import com.instructure.pandautils.features.discussion.router.DiscussionRouterFragment
import com.instructure.student.activity.NavigationActivity
-import com.instructure.student.features.assignments.details.AssignmentDetailsFragment
+import com.instructure.pandautils.features.assignments.details.AssignmentDetailsFragment
import com.instructure.student.fragment.BasicQuizViewFragment
import com.instructure.student.router.RouteMatcher
diff --git a/apps/student/src/main/java/com/instructure/student/features/elementary/course/ElementaryCourseViewModel.kt b/apps/student/src/main/java/com/instructure/student/features/elementary/course/ElementaryCourseViewModel.kt
index a7d7536165..9ab9f32403 100644
--- a/apps/student/src/main/java/com/instructure/student/features/elementary/course/ElementaryCourseViewModel.kt
+++ b/apps/student/src/main/java/com/instructure/student/features/elementary/course/ElementaryCourseViewModel.kt
@@ -22,6 +22,7 @@ import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
+import com.google.firebase.crashlytics.FirebaseCrashlytics
import com.instructure.canvasapi2.managers.CourseManager
import com.instructure.canvasapi2.managers.OAuthManager
import com.instructure.canvasapi2.managers.TabManager
@@ -44,7 +45,8 @@ class ElementaryCourseViewModel @Inject constructor(
private val resources: Resources,
private val apiPrefs: ApiPrefs,
private val oauthManager: OAuthManager,
- private val courseManager: CourseManager
+ private val courseManager: CourseManager,
+ private val firebaseCrashlytics: FirebaseCrashlytics
) : ViewModel() {
val state: LiveData
@@ -84,7 +86,7 @@ class ElementaryCourseViewModel @Inject constructor(
}
} catch (e: Exception) {
_state.postValue(ViewState.Error(resources.getString(R.string.error_loading_course_details)))
- Logger.e("Failed to load tabs")
+ firebaseCrashlytics.recordException(e)
}
}
}
@@ -157,14 +159,7 @@ class ElementaryCourseViewModel @Inject constructor(
else -> it.htmlUrl ?: ""
}
- val authenticatedUrl = if (apiPrefs.isStudentView) {
- apiPrefs.user?.let {
- oauthManager.getAuthenticatedSessionMasqueradingAsync(url, apiPrefs.user!!.id)
- .await().dataOrNull?.sessionUrl
- } ?: url
- } else {
- oauthManager.getAuthenticatedSessionAsync(url).await().dataOrNull?.sessionUrl
- }
+ val authenticatedUrl = oauthManager.getAuthenticatedSessionAsync(url).await().dataOrNull?.sessionUrl
ElementaryCourseTab(it.tabId, drawable, it.label, authenticatedUrl ?: url)
}
diff --git a/apps/student/src/main/java/com/instructure/student/features/files/list/FileListFragment.kt b/apps/student/src/main/java/com/instructure/student/features/files/list/FileListFragment.kt
index 3362b53079..52ea6555a6 100644
--- a/apps/student/src/main/java/com/instructure/student/features/files/list/FileListFragment.kt
+++ b/apps/student/src/main/java/com/instructure/student/features/files/list/FileListFragment.kt
@@ -20,6 +20,8 @@ package com.instructure.student.features.files.list
import android.content.DialogInterface
import android.content.res.Configuration
import android.os.Bundle
+import android.os.Handler
+import android.os.Looper
import android.view.LayoutInflater
import android.view.MenuItem
import android.view.View
@@ -310,12 +312,15 @@ class FileListFragment : ParentFragment(), Bookmarkable, FileUploadDialogParent
private fun themeToolbar() = with(binding) {
// We style the toolbar white for user files
- if (canvasContext.type == CanvasContext.Type.USER) {
- ViewStyler.themeProgressBar(fileLoadingProgressBar, ThemePrefs.primaryTextColor)
- ViewStyler.themeToolbarColored(requireActivity(), toolbar, ThemePrefs.primaryColor, ThemePrefs.primaryTextColor)
- } else {
- ViewStyler.themeProgressBar(fileLoadingProgressBar, requireContext().getColor(R.color.white))
- ViewStyler.themeToolbarColored(requireActivity(), toolbar, canvasContext)
+ Handler(Looper.getMainLooper()).post {
+ if (!isAdded) return@post
+ if (canvasContext.type == CanvasContext.Type.USER) {
+ ViewStyler.themeProgressBar(fileLoadingProgressBar, ThemePrefs.primaryTextColor)
+ ViewStyler.themeToolbarColored(requireActivity(), toolbar, ThemePrefs.primaryColor, ThemePrefs.primaryTextColor)
+ } else {
+ ViewStyler.themeProgressBar(fileLoadingProgressBar, requireContext().getColor(R.color.textLightest))
+ ViewStyler.themeToolbarColored(requireActivity(), toolbar, canvasContext)
+ }
}
}
diff --git a/apps/student/src/main/java/com/instructure/student/features/grades/GradesListFragment.kt b/apps/student/src/main/java/com/instructure/student/features/grades/GradesListFragment.kt
index 3eb279335f..c87a1b80ef 100644
--- a/apps/student/src/main/java/com/instructure/student/features/grades/GradesListFragment.kt
+++ b/apps/student/src/main/java/com/instructure/student/features/grades/GradesListFragment.kt
@@ -65,7 +65,7 @@ import com.instructure.student.R
import com.instructure.student.adapter.TermSpinnerAdapter
import com.instructure.student.databinding.FragmentCourseGradesBinding
import com.instructure.student.dialog.WhatIfDialogStyled
-import com.instructure.student.features.assignments.details.AssignmentDetailsFragment
+import com.instructure.pandautils.features.assignments.details.AssignmentDetailsFragment
import com.instructure.student.fragment.ParentFragment
import com.instructure.student.interfaces.AdapterToFragmentCallback
import com.instructure.student.router.RouteMatcher
diff --git a/apps/student/src/main/java/com/instructure/student/features/grades/GradesListRepository.kt b/apps/student/src/main/java/com/instructure/student/features/grades/GradesListRepository.kt
index 75d6c9088a..dfc4555751 100644
--- a/apps/student/src/main/java/com/instructure/student/features/grades/GradesListRepository.kt
+++ b/apps/student/src/main/java/com/instructure/student/features/grades/GradesListRepository.kt
@@ -17,10 +17,15 @@
package com.instructure.student.features.grades
-import com.instructure.canvasapi2.models.*
+import com.instructure.canvasapi2.models.AssignmentGroup
+import com.instructure.canvasapi2.models.Course
+import com.instructure.canvasapi2.models.Enrollment
+import com.instructure.canvasapi2.models.GradingPeriod
+import com.instructure.canvasapi2.models.Submission
import com.instructure.pandautils.repository.Repository
import com.instructure.pandautils.utils.FeatureFlagProvider
import com.instructure.pandautils.utils.NetworkStateProvider
+import com.instructure.pandautils.utils.filterHiddenAssignments
import com.instructure.student.features.grades.datasource.GradesListDataSource
import com.instructure.student.features.grades.datasource.GradesListLocalDataSource
import com.instructure.student.features.grades.datasource.GradesListNetworkDataSource
@@ -46,7 +51,7 @@ class GradesListRepository(
scopeToStudent: Boolean,
forceNetwork: Boolean
): List {
- return dataSource().getAssignmentGroupsWithAssignmentsForGradingPeriod(courseId, gradingPeriodId, scopeToStudent, forceNetwork)
+ return dataSource().getAssignmentGroupsWithAssignmentsForGradingPeriod(courseId, gradingPeriodId, scopeToStudent, forceNetwork).filterHiddenAssignments()
}
suspend fun getSubmissionsForMultipleAssignments(
@@ -79,6 +84,6 @@ class GradesListRepository(
courseId: Long,
forceNetwork: Boolean,
): List {
- return dataSource().getAssignmentGroupsWithAssignments(courseId, forceNetwork)
+ return dataSource().getAssignmentGroupsWithAssignments(courseId, forceNetwork).filterHiddenAssignments()
}
}
diff --git a/apps/student/src/main/java/com/instructure/student/features/inbox/list/StudentInboxRouter.kt b/apps/student/src/main/java/com/instructure/student/features/inbox/list/StudentInboxRouter.kt
index 02684732cf..97bacbd93c 100644
--- a/apps/student/src/main/java/com/instructure/student/features/inbox/list/StudentInboxRouter.kt
+++ b/apps/student/src/main/java/com/instructure/student/features/inbox/list/StudentInboxRouter.kt
@@ -24,8 +24,9 @@ import com.instructure.canvasapi2.models.Conversation
import com.instructure.pandautils.features.inbox.list.InboxFragment
import com.instructure.pandautils.features.inbox.list.InboxRouter
import com.instructure.pandautils.features.inbox.utils.InboxComposeOptions
+import com.instructure.pandautils.utils.ConversationUpdatedEvent
+import com.instructure.pandautils.utils.remove
import com.instructure.student.activity.NavigationActivity
-import com.instructure.student.events.ConversationUpdatedEvent
import com.instructure.student.fragment.InboxComposeMessageFragment
import com.instructure.student.fragment.InboxConversationFragment
import com.instructure.student.router.RouteMatcher
@@ -44,7 +45,7 @@ class StudentInboxRouter(private val activity: FragmentActivity, private val fra
}
}
- override fun routeToNewMessage() {
+ override fun routeToNewMessage(activity: FragmentActivity) {
val route = InboxComposeMessageFragment.makeRoute()
RouteMatcher.route(activity, route)
}
@@ -63,6 +64,7 @@ class StudentInboxRouter(private val activity: FragmentActivity, private val fra
fun onUpdateConversation(event: ConversationUpdatedEvent) {
event.get {
if (fragment is InboxFragment) {
+ event.remove()
fragment.conversationUpdated()
}
}
diff --git a/apps/student/src/main/java/com/instructure/student/features/login/StudentAcceptableUsePolicyRouter.kt b/apps/student/src/main/java/com/instructure/student/features/login/StudentAcceptableUsePolicyRouter.kt
index 1884f27bfd..4a47830da2 100644
--- a/apps/student/src/main/java/com/instructure/student/features/login/StudentAcceptableUsePolicyRouter.kt
+++ b/apps/student/src/main/java/com/instructure/student/features/login/StudentAcceptableUsePolicyRouter.kt
@@ -27,7 +27,7 @@ import com.instructure.pandautils.services.PushNotificationRegistrationWorker
import com.instructure.student.R
import com.instructure.student.activity.InternalWebViewActivity
import com.instructure.student.activity.NavigationActivity
-import com.instructure.student.features.assignments.reminder.AlarmScheduler
+import com.instructure.pandautils.features.assignments.details.reminder.AlarmScheduler
import com.instructure.student.tasks.StudentLogoutTask
class StudentAcceptableUsePolicyRouter(
diff --git a/apps/student/src/main/java/com/instructure/student/features/login/StudentLoginNavigation.kt b/apps/student/src/main/java/com/instructure/student/features/login/StudentLoginNavigation.kt
index a11e91a18a..18fa125489 100644
--- a/apps/student/src/main/java/com/instructure/student/features/login/StudentLoginNavigation.kt
+++ b/apps/student/src/main/java/com/instructure/student/features/login/StudentLoginNavigation.kt
@@ -25,7 +25,7 @@ import com.instructure.loginapi.login.tasks.LogoutTask
import com.instructure.pandautils.room.offline.DatabaseProvider
import com.instructure.pandautils.services.PushNotificationRegistrationWorker
import com.instructure.student.activity.NavigationActivity
-import com.instructure.student.features.assignments.reminder.AlarmScheduler
+import com.instructure.pandautils.features.assignments.details.reminder.AlarmScheduler
import com.instructure.student.tasks.StudentLogoutTask
class StudentLoginNavigation(
diff --git a/apps/student/src/main/java/com/instructure/student/features/modules/progression/CourseModuleProgressionFragment.kt b/apps/student/src/main/java/com/instructure/student/features/modules/progression/CourseModuleProgressionFragment.kt
index 9e56603f6f..0ac251eadb 100644
--- a/apps/student/src/main/java/com/instructure/student/features/modules/progression/CourseModuleProgressionFragment.kt
+++ b/apps/student/src/main/java/com/instructure/student/features/modules/progression/CourseModuleProgressionFragment.kt
@@ -70,7 +70,7 @@ import com.instructure.student.R
import com.instructure.student.databinding.CourseModuleProgressionBinding
import com.instructure.student.events.ModuleUpdatedEvent
import com.instructure.student.events.post
-import com.instructure.student.features.assignments.details.AssignmentDetailsFragment
+import com.instructure.pandautils.features.assignments.details.AssignmentDetailsFragment
import com.instructure.student.features.files.details.FileDetailsFragment
import com.instructure.student.features.modules.list.ModuleListFragment
import com.instructure.student.features.modules.util.ModuleProgressionUtility
diff --git a/apps/student/src/main/java/com/instructure/student/features/modules/util/ModuleUtility.kt b/apps/student/src/main/java/com/instructure/student/features/modules/util/ModuleUtility.kt
index aed581b1e8..a3066f832b 100644
--- a/apps/student/src/main/java/com/instructure/student/features/modules/util/ModuleUtility.kt
+++ b/apps/student/src/main/java/com/instructure/student/features/modules/util/ModuleUtility.kt
@@ -29,8 +29,8 @@ import com.instructure.canvasapi2.utils.isLocked
import com.instructure.interactions.router.Route
import com.instructure.pandautils.features.discussion.details.DiscussionDetailsWebViewFragment
import com.instructure.student.R
-import com.instructure.student.features.assignments.details.AssignmentDetailsFragment
-import com.instructure.student.features.assignments.details.AssignmentDetailsFragment.Companion.makeRoute
+import com.instructure.pandautils.features.assignments.details.AssignmentDetailsFragment
+import com.instructure.pandautils.features.assignments.details.AssignmentDetailsFragment.Companion.makeRoute
import com.instructure.student.features.discussion.details.DiscussionDetailsFragment
import com.instructure.student.features.discussion.details.DiscussionDetailsFragment.Companion.makeRoute
import com.instructure.student.features.files.details.FileDetailsFragment
diff --git a/apps/student/src/main/java/com/instructure/student/features/pages/details/PageDetailsFragment.kt b/apps/student/src/main/java/com/instructure/student/features/pages/details/PageDetailsFragment.kt
index fba5a92eac..d1d7df70dc 100644
--- a/apps/student/src/main/java/com/instructure/student/features/pages/details/PageDetailsFragment.kt
+++ b/apps/student/src/main/java/com/instructure/student/features/pages/details/PageDetailsFragment.kt
@@ -26,11 +26,16 @@ import com.instructure.canvasapi2.models.AuthenticatedSession
import com.instructure.canvasapi2.models.CanvasContext
import com.instructure.canvasapi2.models.Course
import com.instructure.canvasapi2.models.Page
-import com.instructure.canvasapi2.utils.*
+import com.instructure.canvasapi2.utils.APIHelper
+import com.instructure.canvasapi2.utils.ApiPrefs
+import com.instructure.canvasapi2.utils.Failure
+import com.instructure.canvasapi2.utils.Logger
import com.instructure.canvasapi2.utils.pageview.BeforePageView
import com.instructure.canvasapi2.utils.pageview.PageView
import com.instructure.canvasapi2.utils.pageview.PageViewUrl
-import com.instructure.canvasapi2.utils.weave.*
+import com.instructure.canvasapi2.utils.weave.awaitApi
+import com.instructure.canvasapi2.utils.weave.catch
+import com.instructure.canvasapi2.utils.weave.tryLaunch
import com.instructure.interactions.bookmarks.Bookmarkable
import com.instructure.interactions.bookmarks.Bookmarker
import com.instructure.interactions.router.Route
@@ -38,7 +43,17 @@ import com.instructure.interactions.router.RouterParams
import com.instructure.loginapi.login.dialog.NoInternetConnectionDialog
import com.instructure.pandautils.analytics.SCREEN_VIEW_PAGE_DETAILS
import com.instructure.pandautils.analytics.ScreenView
-import com.instructure.pandautils.utils.*
+import com.instructure.pandautils.navigation.WebViewRouter
+import com.instructure.pandautils.utils.BooleanArg
+import com.instructure.pandautils.utils.NullableStringArg
+import com.instructure.pandautils.utils.ParcelableArg
+import com.instructure.pandautils.utils.ViewStyler
+import com.instructure.pandautils.utils.getModuleItemId
+import com.instructure.pandautils.utils.loadHtmlWithIframes
+import com.instructure.pandautils.utils.makeBundle
+import com.instructure.pandautils.utils.nonNullArgs
+import com.instructure.pandautils.utils.setupAsBackButton
+import com.instructure.pandautils.utils.withRequireNetwork
import com.instructure.pandautils.views.CanvasWebView
import com.instructure.student.R
import com.instructure.student.events.PageUpdatedEvent
@@ -50,8 +65,8 @@ import com.instructure.student.util.LockInfoHTMLHelper
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.Job
import org.greenrobot.eventbus.Subscribe
-import java.util.*
-import java.util.regex.*
+import java.util.Locale
+import java.util.regex.Pattern
import javax.inject.Inject
@ScreenView(SCREEN_VIEW_PAGE_DETAILS)
@@ -62,6 +77,9 @@ class PageDetailsFragment : InternalWebviewFragment(), Bookmarkable {
@Inject
lateinit var repository: PageDetailsRepository
+ @Inject
+ lateinit var webViewRouter: WebViewRouter
+
private var loadHtmlJob: Job? = null
private var pageName: String? by NullableStringArg(key = PAGE_NAME)
private var page: Page by ParcelableArg(default = Page(), key = PAGE)
@@ -124,9 +142,11 @@ class PageDetailsFragment : InternalWebviewFragment(), Bookmarkable {
if (isUpdated) getCanvasWebView()?.clearHistory()
}
- override fun openMediaFromWebView(mime: String, url: String, filename: String) {
- RouteMatcher.openMedia(activity, url)
- }
+ override fun openMediaFromWebView(mime: String, url: String, filename: String) = webViewRouter.openMedia(url)
+
+ override fun canRouteInternallyDelegate(url: String) = webViewRouter.canRouteInternally(url)
+
+ override fun routeInternallyCallback(url: String) = webViewRouter.routeInternally(url)
}
}
}
diff --git a/apps/student/src/main/java/com/instructure/student/features/quiz/list/QuizListFragment.kt b/apps/student/src/main/java/com/instructure/student/features/quiz/list/QuizListFragment.kt
index 14a8b3b566..acf700f1d5 100644
--- a/apps/student/src/main/java/com/instructure/student/features/quiz/list/QuizListFragment.kt
+++ b/apps/student/src/main/java/com/instructure/student/features/quiz/list/QuizListFragment.kt
@@ -38,7 +38,7 @@ import com.instructure.pandautils.utils.*
import com.instructure.student.R
import com.instructure.student.databinding.PandaRecyclerRefreshLayoutBinding
import com.instructure.student.databinding.QuizListLayoutBinding
-import com.instructure.student.features.assignments.details.AssignmentDetailsFragment
+import com.instructure.pandautils.features.assignments.details.AssignmentDetailsFragment
import com.instructure.student.features.assignments.list.AssignmentListFragment
import com.instructure.student.fragment.BasicQuizViewFragment
import com.instructure.student.fragment.ParentFragment
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 b99538f74c..392aed5002 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
@@ -41,29 +41,49 @@ import androidx.work.WorkQuery
import com.google.firebase.crashlytics.FirebaseCrashlytics
import com.instructure.canvasapi2.managers.CourseNicknameManager
import com.instructure.canvasapi2.managers.UserManager
-import com.instructure.canvasapi2.models.*
+import com.instructure.canvasapi2.models.CanvasColor
+import com.instructure.canvasapi2.models.CanvasContext
+import com.instructure.canvasapi2.models.Course
+import com.instructure.canvasapi2.models.CourseNickname
+import com.instructure.canvasapi2.models.DashboardPositions
+import com.instructure.canvasapi2.models.Group
import com.instructure.canvasapi2.utils.pageview.PageView
import com.instructure.canvasapi2.utils.weave.awaitApi
import com.instructure.canvasapi2.utils.weave.catch
import com.instructure.canvasapi2.utils.weave.tryWeave
import com.instructure.interactions.router.Route
+import com.instructure.pandautils.analytics.OfflineAnalyticsManager
import com.instructure.pandautils.analytics.SCREEN_VIEW_DASHBOARD
import com.instructure.pandautils.analytics.ScreenView
import com.instructure.pandautils.binding.viewBinding
import com.instructure.pandautils.dialogs.ColorPickerDialog
+import com.instructure.pandautils.dialogs.EditCourseNicknameDialog
import com.instructure.pandautils.features.dashboard.DashboardCourseItem
import com.instructure.pandautils.features.dashboard.edit.EditDashboardFragment
import com.instructure.pandautils.features.dashboard.notifications.DashboardNotificationsFragment
import com.instructure.pandautils.features.offline.offlinecontent.OfflineContentFragment
import com.instructure.pandautils.features.offline.sync.AggregateProgressObserver
import com.instructure.pandautils.features.offline.sync.OfflineSyncWorker
-import com.instructure.pandautils.utils.*
+import com.instructure.pandautils.utils.ColorKeeper
+import com.instructure.pandautils.utils.Const
+import com.instructure.pandautils.utils.FeatureFlagProvider
+import com.instructure.pandautils.utils.NetworkStateProvider
+import com.instructure.pandautils.utils.NullableParcelableArg
+import com.instructure.pandautils.utils.Utils
+import com.instructure.pandautils.utils.fadeAnimationWithAction
+import com.instructure.pandautils.utils.isTablet
+import com.instructure.pandautils.utils.makeBundle
+import com.instructure.pandautils.utils.removeAllItemDecorations
+import com.instructure.pandautils.utils.setGone
+import com.instructure.pandautils.utils.setMenu
+import com.instructure.pandautils.utils.setVisible
+import com.instructure.pandautils.utils.toast
+import com.instructure.pandautils.utils.withRequireNetwork
import com.instructure.student.R
import com.instructure.student.adapter.DashboardRecyclerAdapter
import com.instructure.student.databinding.CourseGridRecyclerRefreshLayoutBinding
import com.instructure.student.databinding.FragmentCourseGridBinding
import com.instructure.student.decorations.VerticalGridSpacingDecoration
-import com.instructure.pandautils.dialogs.EditCourseNicknameDialog
import com.instructure.student.events.CoreDataFinishedLoading
import com.instructure.student.events.CourseColorOverlayToggledEvent
import com.instructure.student.events.ShowGradesToggledEvent
@@ -104,6 +124,9 @@ class DashboardFragment : ParentFragment() {
@Inject
lateinit var firebaseCrashlytics: FirebaseCrashlytics
+ @Inject
+ lateinit var offlineAnalyticsManager: OfflineAnalyticsManager
+
private val binding by viewBinding(FragmentCourseGridBinding::bind)
private lateinit var recyclerBinding: CourseGridRecyclerRefreshLayoutBinding
@@ -185,8 +208,11 @@ class DashboardFragment : ParentFragment() {
}
override fun onCourseSelected(course: Course) {
- canvasContext = course
- RouteMatcher.route(requireActivity(), CourseBrowserFragment.makeRoute(course))
+ lifecycleScope.launch {
+ if (!repository.isOnline()) { offlineAnalyticsManager.reportCourseOpenedInOfflineMode() }
+ canvasContext = course
+ RouteMatcher.route(requireActivity(), CourseBrowserFragment.makeRoute(course))
+ }
}
@Suppress("EXPERIMENTAL_FEATURE_WARNING")
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 55e375915c..27fff10688 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
@@ -49,7 +49,6 @@ import com.instructure.student.adapter.NothingSelectedSpinnerAdapter
import com.instructure.student.databinding.FragmentInboxComposeMessageBinding
import com.instructure.student.dialog.UnsavedChangesExitDialog
import com.instructure.student.events.ChooseRecipientsEvent
-import com.instructure.student.events.ConversationUpdatedEvent
import com.instructure.student.events.MessageAddedEvent
import com.instructure.student.router.RouteMatcher
import com.instructure.student.view.AttachmentView
diff --git a/apps/student/src/main/java/com/instructure/student/fragment/InboxConversationFragment.kt b/apps/student/src/main/java/com/instructure/student/fragment/InboxConversationFragment.kt
index d616a8eb74..f1cc98172f 100644
--- a/apps/student/src/main/java/com/instructure/student/fragment/InboxConversationFragment.kt
+++ b/apps/student/src/main/java/com/instructure/student/fragment/InboxConversationFragment.kt
@@ -44,7 +44,6 @@ import com.instructure.student.R
import com.instructure.student.adapter.InboxConversationAdapter
import com.instructure.student.databinding.FragmentInboxConversationBinding
import com.instructure.student.databinding.PandaRecyclerRefreshLayoutBinding
-import com.instructure.student.events.ConversationUpdatedEvent
import com.instructure.student.events.MessageAddedEvent
import com.instructure.student.interfaces.MessageAdapterCallback
import com.instructure.student.router.RouteMatcher
diff --git a/apps/student/src/main/java/com/instructure/student/fragment/InternalWebviewFragment.kt b/apps/student/src/main/java/com/instructure/student/fragment/InternalWebviewFragment.kt
index 6ed6c3cff0..efeacd8422 100644
--- a/apps/student/src/main/java/com/instructure/student/fragment/InternalWebviewFragment.kt
+++ b/apps/student/src/main/java/com/instructure/student/fragment/InternalWebviewFragment.kt
@@ -38,11 +38,29 @@ import com.instructure.canvasapi2.models.LTITool
import com.instructure.canvasapi2.utils.ApiPrefs
import com.instructure.canvasapi2.utils.Logger
import com.instructure.canvasapi2.utils.isValid
-import com.instructure.canvasapi2.utils.weave.*
+import com.instructure.canvasapi2.utils.weave.StatusCallbackError
+import com.instructure.canvasapi2.utils.weave.awaitApi
+import com.instructure.canvasapi2.utils.weave.catch
+import com.instructure.canvasapi2.utils.weave.tryWeave
+import com.instructure.canvasapi2.utils.weave.weave
import com.instructure.interactions.router.Route
import com.instructure.pandautils.binding.viewBinding
import com.instructure.pandautils.features.file.download.FileDownloadWorker
-import com.instructure.pandautils.utils.*
+import com.instructure.pandautils.utils.BooleanArg
+import com.instructure.pandautils.utils.Const
+import com.instructure.pandautils.utils.NullableStringArg
+import com.instructure.pandautils.utils.OnBackStackChangedEvent
+import com.instructure.pandautils.utils.ParcelableArg
+import com.instructure.pandautils.utils.PermissionUtils
+import com.instructure.pandautils.utils.ThemePrefs
+import com.instructure.pandautils.utils.ViewStyler
+import com.instructure.pandautils.utils.argsWithContext
+import com.instructure.pandautils.utils.makeBundle
+import com.instructure.pandautils.utils.setGone
+import com.instructure.pandautils.utils.setVisible
+import com.instructure.pandautils.utils.setupAsBackButton
+import com.instructure.pandautils.utils.toast
+import com.instructure.pandautils.utils.withArgs
import com.instructure.pandautils.views.CanvasWebView
import com.instructure.student.R
import com.instructure.student.databinding.FragmentWebviewBinding
@@ -273,7 +291,11 @@ open class InternalWebviewFragment : ParentFragment() {
override fun applyTheme() = with(binding) {
toolbar.title = title()
toolbar.setupAsBackButton(this@InternalWebviewFragment)
- ViewStyler.themeToolbarColored(requireActivity(), toolbar, canvasContext)
+ if (canvasContext.type != CanvasContext.Type.COURSE && canvasContext.type != CanvasContext.Type.GROUP) {
+ ViewStyler.themeToolbarColored(requireActivity(), toolbar, ThemePrefs.primaryColor, ThemePrefs.primaryTextColor)
+ } else {
+ ViewStyler.themeToolbarColored(requireActivity(), toolbar, canvasContext)
+ }
}
override fun title(): String = title ?: canvasContext.name ?: ""
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 d1adbbfd09..aa6ebc7f38 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
@@ -43,7 +43,7 @@ import com.instructure.student.activity.ParentActivity
import com.instructure.student.adapter.NotificationListRecyclerAdapter
import com.instructure.student.databinding.FragmentListNotificationBinding
import com.instructure.student.databinding.PandaRecyclerRefreshLayoutBinding
-import com.instructure.student.features.assignments.details.AssignmentDetailsFragment
+import com.instructure.pandautils.features.assignments.details.AssignmentDetailsFragment
import com.instructure.student.interfaces.NotificationAdapterToFragmentCallback
import com.instructure.student.mobius.conferences.conference_list.ui.ConferenceListRepositoryFragment
import com.instructure.student.router.RouteMatcher
@@ -274,6 +274,13 @@ class NotificationListFragment : ParentFragment(), Bookmarkable, FragmentManager
}
COLLABORATION -> UnsupportedTabFragment.makeRoute(canvasContext, Tab.COLLABORATIONS_ID)
CONFERENCE -> ConferenceListRepositoryFragment.makeRoute(canvasContext)
+ DISCUSSION_MENTION -> {
+ if (streamItem.htmlUrl.isNotEmpty()) {
+ RouteMatcher.getInternalRoute(streamItem.htmlUrl, ApiPrefs.domain)
+ } else {
+ UnknownItemFragment.makeRoute(canvasContext, streamItem)
+ }
+ }
else -> UnsupportedFeatureFragment.makeRoute(canvasContext, featureName = streamItem.type, url = streamItem.url ?: streamItem.htmlUrl)
}
diff --git a/apps/student/src/main/java/com/instructure/student/fragment/ToDoListFragment.kt b/apps/student/src/main/java/com/instructure/student/fragment/ToDoListFragment.kt
index c33c395596..ce3412b950 100644
--- a/apps/student/src/main/java/com/instructure/student/fragment/ToDoListFragment.kt
+++ b/apps/student/src/main/java/com/instructure/student/fragment/ToDoListFragment.kt
@@ -48,7 +48,7 @@ import com.instructure.student.R
import com.instructure.student.adapter.TodoListRecyclerAdapter
import com.instructure.student.databinding.FragmentListTodoBinding
import com.instructure.student.databinding.PandaRecyclerRefreshLayoutBinding
-import com.instructure.student.features.assignments.details.AssignmentDetailsFragment
+import com.instructure.pandautils.features.assignments.details.AssignmentDetailsFragment
import com.instructure.student.interfaces.NotificationAdapterToFragmentCallback
import com.instructure.student.router.RouteMatcher
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 87d965d053..66dfe1b580 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
@@ -87,8 +87,8 @@ class InboxMessageHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
}
val popup = PopupMenu(v.context, v, Gravity.START)
val menu = popup.menu
- for (action in actions) {
- menu.add(0, action.ordinal, action.ordinal, action.labelResId)
+ actions.forEachIndexed { index, action ->
+ menu.add(0, index, index, action.labelResId)
}
// Add click listener
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 6bf59be4ef..71e039750a 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
@@ -94,7 +94,7 @@ class NotificationViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView)
// Icon
val drawableResId: Int
when (item.getStreamItemType()) {
- StreamItem.Type.DISCUSSION_TOPIC -> {
+ StreamItem.Type.DISCUSSION_TOPIC, StreamItem.Type.DISCUSSION_ENTRY, StreamItem.Type.DISCUSSION_MENTION -> {
drawableResId = R.drawable.ic_discussion
icon.contentDescription = context.getString(R.string.discussionIcon)
}
diff --git a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submission/annnotation/AnnotationSubmissionUploadFragment.kt b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submission/annnotation/AnnotationSubmissionUploadFragment.kt
index 874e628b38..0b407516a8 100644
--- a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submission/annnotation/AnnotationSubmissionUploadFragment.kt
+++ b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submission/annnotation/AnnotationSubmissionUploadFragment.kt
@@ -96,7 +96,8 @@ class AnnotationSubmissionUploadFragment : Fragment() {
private const val SUBMISSION_ID = "submission_id"
fun newInstance(route: Route): AnnotationSubmissionUploadFragment {
- return AnnotationSubmissionUploadFragment().withArgs(route.arguments)
+ return AnnotationSubmissionUploadFragment()
+ .withArgs(route.arguments)
}
fun makeRoute(
diff --git a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submission/file/UploadStatusSubmissionEffectHandler.kt b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submission/file/UploadStatusSubmissionEffectHandler.kt
index ee449d3e53..718d583c50 100644
--- a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submission/file/UploadStatusSubmissionEffectHandler.kt
+++ b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submission/file/UploadStatusSubmissionEffectHandler.kt
@@ -16,7 +16,6 @@
*/
package com.instructure.student.mobius.assignmentDetails.submission.file
-import android.content.Context
import com.instructure.canvasapi2.utils.exhaustive
import com.instructure.student.mobius.assignmentDetails.submission.file.ui.UploadStatusSubmissionView
import com.instructure.student.mobius.common.ui.EffectHandler
diff --git a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submission/file/UploadStatusSubmissionPresenter.kt b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submission/file/UploadStatusSubmissionPresenter.kt
index d617410b4d..25d13a5891 100644
--- a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submission/file/UploadStatusSubmissionPresenter.kt
+++ b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submission/file/UploadStatusSubmissionPresenter.kt
@@ -33,10 +33,18 @@ object UploadStatusSubmissionPresenter :
context: Context
): UploadStatusSubmissionViewState {
return when {
- model.isFailed -> presentFailed(model, context)
+ model.isFailed -> presentFailed(
+ model,
+ context
+ )
model.isLoading -> UploadStatusSubmissionViewState.Loading
- model.files.isEmpty() -> presentSuccess(context)
- else -> presentInProgress(model, context)
+ model.files.isEmpty() -> presentSuccess(
+ context
+ )
+ else -> presentInProgress(
+ model,
+ context
+ )
}
}
@@ -56,7 +64,12 @@ object UploadStatusSubmissionPresenter :
return UploadStatusSubmissionViewState.Failed(
context.getString(R.string.submissionStatusFailedTitle),
context.getString(R.string.submissionUploadFailedMessage),
- presentListItems(model, context, R.drawable.ic_warning, true)
+ presentListItems(
+ model,
+ context,
+ R.drawable.ic_warning,
+ true
+ )
)
}
@@ -80,7 +93,12 @@ object UploadStatusSubmissionPresenter :
size,
NumberHelper.doubleToPercentage(percent),
percent,
- presentListItems(model, context, null, false)
+ presentListItems(
+ model,
+ context,
+ null,
+ false
+ )
)
}
diff --git a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submission/text/TextSubmissionUploadUpdate.kt b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submission/text/TextSubmissionUploadUpdate.kt
index 50772d6442..6b9fd0fd4a 100644
--- a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submission/text/TextSubmissionUploadUpdate.kt
+++ b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submission/text/TextSubmissionUploadUpdate.kt
@@ -20,8 +20,6 @@ import com.instructure.student.mobius.common.ui.UpdateInit
import com.spotify.mobius.Effects.effects
import com.spotify.mobius.First
import com.spotify.mobius.Next
-import java.io.UnsupportedEncodingException
-import java.net.URLEncoder
class TextSubmissionUploadUpdate :
UpdateInit() {
diff --git a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/SubmissionDetailsEffectHandler.kt b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/SubmissionDetailsEffectHandler.kt
index 0cb6b6f633..8190b3b1a8 100644
--- a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/SubmissionDetailsEffectHandler.kt
+++ b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/SubmissionDetailsEffectHandler.kt
@@ -24,11 +24,10 @@ import com.instructure.canvasapi2.utils.exhaustive
import com.instructure.pandautils.utils.orDefault
import com.instructure.student.mobius.assignmentDetails.submissionDetails.drawer.comments.SubmissionCommentsSharedEvent
import com.instructure.student.mobius.assignmentDetails.submissionDetails.ui.SubmissionDetailsView
-import com.instructure.student.mobius.common.ChannelSource
+import com.instructure.student.mobius.common.FlowSource
+import com.instructure.student.mobius.common.trySend
import com.instructure.student.mobius.common.ui.EffectHandler
import com.instructure.student.util.getStudioLTITool
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.ObsoleteCoroutinesApi
import kotlinx.coroutines.launch
import java.io.File
@@ -36,8 +35,6 @@ class SubmissionDetailsEffectHandler(
private val repository: SubmissionDetailsRepository
) : EffectHandler() {
- @ObsoleteCoroutinesApi
- @ExperimentalCoroutinesApi
override fun accept(effect: SubmissionDetailsEffect) {
when (effect) {
is SubmissionDetailsEffect.LoadData -> loadData(effect)
@@ -150,16 +147,14 @@ class SubmissionDetailsEffectHandler(
return dataOrNull?.getSubmissionTypes()?.contains(type).orDefault()
}
- @ObsoleteCoroutinesApi
private fun uploadMediaComment(file: File) {
- ChannelSource.getChannel().trySend(
+ FlowSource.getFlow().trySend(
SubmissionCommentsSharedEvent.SendMediaCommentClicked(file)
)
}
- @ObsoleteCoroutinesApi
private fun mediaDialogClosed() {
- ChannelSource.getChannel().trySend(
+ FlowSource.getFlow().trySend(
SubmissionCommentsSharedEvent.MediaCommentDialogClosed
)
}
diff --git a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/content/emptySubmission/ui/SubmissionDetailsEmptyContentView.kt b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/content/emptySubmission/ui/SubmissionDetailsEmptyContentView.kt
index e0bfd1aed1..a52b0c6da9 100644
--- a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/content/emptySubmission/ui/SubmissionDetailsEmptyContentView.kt
+++ b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/content/emptySubmission/ui/SubmissionDetailsEmptyContentView.kt
@@ -25,7 +25,11 @@ import android.view.ViewGroup
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.FragmentActivity
-import com.instructure.canvasapi2.models.*
+import com.instructure.canvasapi2.models.Assignment
+import com.instructure.canvasapi2.models.CanvasContext
+import com.instructure.canvasapi2.models.Course
+import com.instructure.canvasapi2.models.LTITool
+import com.instructure.canvasapi2.models.Quiz
import com.instructure.canvasapi2.utils.AnalyticsEventConstants
import com.instructure.canvasapi2.utils.ApiPrefs
import com.instructure.pandautils.features.discussion.router.DiscussionRouterFragment
@@ -41,14 +45,14 @@ import com.instructure.student.databinding.FragmentSubmissionDetailsEmptyContent
import com.instructure.student.fragment.BasicQuizViewFragment
import com.instructure.student.fragment.LtiLaunchFragment
import com.instructure.student.fragment.StudioWebViewFragment
+import com.instructure.student.mobius.assignmentDetails.submissionDetails.content.emptySubmission.SubmissionDetailsEmptyContentEvent
+import com.instructure.student.mobius.assignmentDetails.submissionDetails.content.emptySubmission.ui.SubmissionDetailsEmptyContentViewState.Loaded
+import com.instructure.student.mobius.assignmentDetails.submissionDetails.ui.SubmissionTypesVisibilities
import com.instructure.student.mobius.assignmentDetails.submission.annnotation.AnnotationSubmissionUploadFragment
import com.instructure.student.mobius.assignmentDetails.submission.picker.PickerSubmissionMode
import com.instructure.student.mobius.assignmentDetails.submission.picker.ui.PickerSubmissionUploadFragment
import com.instructure.student.mobius.assignmentDetails.submission.text.ui.TextSubmissionUploadFragment
import com.instructure.student.mobius.assignmentDetails.submission.url.ui.UrlSubmissionUploadFragment
-import com.instructure.student.mobius.assignmentDetails.submissionDetails.content.emptySubmission.SubmissionDetailsEmptyContentEvent
-import com.instructure.student.mobius.assignmentDetails.submissionDetails.content.emptySubmission.ui.SubmissionDetailsEmptyContentViewState.Loaded
-import com.instructure.student.mobius.assignmentDetails.submissionDetails.ui.SubmissionTypesVisibilities
import com.instructure.student.mobius.common.ui.MobiusView
import com.instructure.student.router.RouteMatcher
import com.spotify.mobius.functions.Consumer
diff --git a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/drawer/comments/SubmissionCommentsEffectHandler.kt b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/drawer/comments/SubmissionCommentsEffectHandler.kt
index 235a3f6058..464ae4703a 100644
--- a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/drawer/comments/SubmissionCommentsEffectHandler.kt
+++ b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/drawer/comments/SubmissionCommentsEffectHandler.kt
@@ -18,7 +18,6 @@
package com.instructure.student.mobius.assignmentDetails.submissionDetails.drawer.comments
-import android.app.Activity
import android.content.Context
import com.instructure.canvasapi2.models.CanvasContext
import com.instructure.canvasapi2.utils.AnalyticsEventConstants
@@ -28,7 +27,8 @@ import com.instructure.pandautils.utils.getFragmentActivity
import com.instructure.pandautils.utils.requestPermissions
import com.instructure.student.mobius.assignmentDetails.submissionDetails.SubmissionDetailsSharedEvent
import com.instructure.student.mobius.assignmentDetails.submissionDetails.drawer.comments.ui.SubmissionCommentsView
-import com.instructure.student.mobius.common.ChannelSource
+import com.instructure.student.mobius.common.FlowSource
+import com.instructure.student.mobius.common.trySend
import com.instructure.student.mobius.common.ui.EffectHandler
import com.instructure.student.mobius.common.ui.SubmissionHelper
@@ -81,13 +81,13 @@ class SubmissionCommentsEffectHandler(val context: Context, val submissionHelper
}
SubmissionCommentsEffect.ScrollToBottom -> view?.scrollToBottom()
is SubmissionCommentsEffect.BroadcastSubmissionSelected -> {
- ChannelSource.getChannel().trySend(
+ FlowSource.getFlow().trySend(
SubmissionDetailsSharedEvent.SubmissionClicked(effect.submission)
)
Unit
}
is SubmissionCommentsEffect.BroadcastSubmissionAttachmentSelected -> {
- ChannelSource.getChannel().trySend(
+ FlowSource.getFlow().trySend(
SubmissionDetailsSharedEvent.SubmissionAttachmentClicked(
effect.submission,
effect.attachment
@@ -128,13 +128,13 @@ class SubmissionCommentsEffectHandler(val context: Context, val submissionHelper
}
private fun showVideoCommentDialog() {
- ChannelSource.getChannel().trySend(
+ FlowSource.getFlow().trySend(
SubmissionDetailsSharedEvent.VideoRecordingViewLaunched
)
}
private fun showAudioCommentDialog() {
- ChannelSource.getChannel().trySend(
+ FlowSource.getFlow().trySend(
SubmissionDetailsSharedEvent.AudioRecordingViewLaunched
)
}
diff --git a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/drawer/comments/ui/SubmissionCommentsFragment.kt b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/drawer/comments/ui/SubmissionCommentsFragment.kt
index 6057fe7a1f..cfb1be5fb8 100644
--- a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/drawer/comments/ui/SubmissionCommentsFragment.kt
+++ b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/drawer/comments/ui/SubmissionCommentsFragment.kt
@@ -23,7 +23,7 @@ import com.instructure.student.mobius.assignmentDetails.submissionDetails.drawer
import com.instructure.student.mobius.assignmentDetails.submissionDetails.drawer.comments.SubmissionCommentsPresenter
import com.instructure.student.mobius.assignmentDetails.submissionDetails.drawer.comments.SubmissionCommentsSharedEvent
import com.instructure.student.mobius.assignmentDetails.submissionDetails.ui.SubmissionDetailsTabData
-import com.instructure.student.mobius.common.ChannelSource
+import com.instructure.student.mobius.common.FlowSource
import com.instructure.student.mobius.common.LiveDataSource
import com.instructure.student.mobius.common.ui.SubmissionHelper
import com.instructure.student.room.StudentDb
@@ -47,7 +47,7 @@ class SubmissionCommentsFragment : BaseSubmissionCommentsFragment() {
override fun makePresenter() = SubmissionCommentsPresenter(studentDb)
override fun getExternalEventSources() = listOf(
- ChannelSource.getSource {
+ FlowSource.getSource {
when (it) {
is SubmissionCommentsSharedEvent.SendMediaCommentClicked -> SubmissionCommentsEvent.SendMediaCommentClicked(
it.file
@@ -55,7 +55,7 @@ class SubmissionCommentsFragment : BaseSubmissionCommentsFragment() {
is SubmissionCommentsSharedEvent.MediaCommentDialogClosed -> SubmissionCommentsEvent.AddFilesDialogClosed
}
},
- ChannelSource.getSource {
+ FlowSource.getSource {
SubmissionCommentsEvent.SubmissionCommentAdded(it)
},
LiveDataSource.of, SubmissionCommentsEvent>(
diff --git a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/drawer/comments/ui/SubmissionCommentsView.kt b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/drawer/comments/ui/SubmissionCommentsView.kt
index 5f717deef4..9d4941bd1e 100644
--- a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/drawer/comments/ui/SubmissionCommentsView.kt
+++ b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/drawer/comments/ui/SubmissionCommentsView.kt
@@ -32,10 +32,10 @@ import com.instructure.student.R
import com.instructure.student.activity.BaseRouterActivity
import com.instructure.student.databinding.DialogCommentFilePickerBinding
import com.instructure.student.databinding.FragmentSubmissionCommentsBinding
-import com.instructure.student.mobius.assignmentDetails.submission.picker.PickerSubmissionMode
-import com.instructure.student.mobius.assignmentDetails.submission.picker.ui.PickerSubmissionUploadFragment
import com.instructure.student.mobius.assignmentDetails.submissionDetails.drawer.comments.SubmissionCommentsEvent
import com.instructure.student.mobius.assignmentDetails.submissionDetails.drawer.comments.SubmissionCommentsViewState
+import com.instructure.student.mobius.assignmentDetails.submission.picker.PickerSubmissionMode
+import com.instructure.student.mobius.assignmentDetails.submission.picker.ui.PickerSubmissionUploadFragment
import com.instructure.student.mobius.common.ui.MobiusView
import com.instructure.student.room.StudentDb
import com.instructure.student.router.RouteMatcher
diff --git a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/drawer/files/SubmissionFilesEffectHandler.kt b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/drawer/files/SubmissionFilesEffectHandler.kt
index 059a8ad5dc..8c7d12b17b 100644
--- a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/drawer/files/SubmissionFilesEffectHandler.kt
+++ b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/drawer/files/SubmissionFilesEffectHandler.kt
@@ -18,7 +18,8 @@ package com.instructure.student.mobius.assignmentDetails.submissionDetails.drawe
import com.instructure.student.mobius.assignmentDetails.submissionDetails.SubmissionDetailsSharedEvent
import com.instructure.student.mobius.assignmentDetails.submissionDetails.drawer.files.ui.SubmissionFilesView
-import com.instructure.student.mobius.common.ChannelSource
+import com.instructure.student.mobius.common.FlowSource
+import com.instructure.student.mobius.common.trySend
import com.instructure.student.mobius.common.ui.EffectHandler
import kotlinx.coroutines.ObsoleteCoroutinesApi
@@ -27,7 +28,7 @@ class SubmissionFilesEffectHandler : EffectHandler {
- ChannelSource.getChannel().trySend(
+ FlowSource.getFlow().trySend(
SubmissionDetailsSharedEvent.FileSelected(value.file)
)
}
diff --git a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/drawer/rubric/SubmissionRubricPresenter.kt b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/drawer/rubric/SubmissionRubricPresenter.kt
index 07948d713f..b1b19cd23e 100644
--- a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/drawer/rubric/SubmissionRubricPresenter.kt
+++ b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/drawer/rubric/SubmissionRubricPresenter.kt
@@ -24,9 +24,9 @@ import com.instructure.canvasapi2.models.RubricCriterionRating
import com.instructure.canvasapi2.utils.NumberHelper
import com.instructure.canvasapi2.utils.isValid
import com.instructure.canvasapi2.utils.validOrNull
+import com.instructure.pandautils.features.assignments.details.mobius.gradeCell.GradeCellViewState
import com.instructure.pandautils.utils.color
import com.instructure.student.R
-import com.instructure.student.mobius.assignmentDetails.ui.gradeCell.GradeCellViewState
import com.instructure.student.mobius.common.ui.Presenter
object SubmissionRubricPresenter : Presenter {
diff --git a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/drawer/rubric/SubmissionRubricViewState.kt b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/drawer/rubric/SubmissionRubricViewState.kt
index 4a6722a484..346aedb025 100644
--- a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/drawer/rubric/SubmissionRubricViewState.kt
+++ b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/drawer/rubric/SubmissionRubricViewState.kt
@@ -16,7 +16,8 @@
*/
package com.instructure.student.mobius.assignmentDetails.submissionDetails.drawer.rubric
-import com.instructure.student.mobius.assignmentDetails.ui.gradeCell.GradeCellViewState
+import com.instructure.pandautils.features.assignments.details.mobius.gradeCell.GradeCellViewState
+
data class SubmissionRubricViewState(
val listData: List
diff --git a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/ui/SubmissionDetailsRepositoryFragment.kt b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/ui/SubmissionDetailsRepositoryFragment.kt
index 414fc1b543..f2ccef6e4c 100644
--- a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/ui/SubmissionDetailsRepositoryFragment.kt
+++ b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/ui/SubmissionDetailsRepositoryFragment.kt
@@ -28,7 +28,7 @@ import com.instructure.pandautils.utils.withArgs
import com.instructure.student.mobius.assignmentDetails.submissionDetails.SubmissionDetailsEvent
import com.instructure.student.mobius.assignmentDetails.submissionDetails.SubmissionDetailsRepository
import com.instructure.student.mobius.assignmentDetails.submissionDetails.SubmissionDetailsSharedEvent
-import com.instructure.student.mobius.common.ChannelSource
+import com.instructure.student.mobius.common.FlowSource
import com.instructure.student.mobius.common.LiveDataSource
import com.instructure.student.room.StudentDb
import com.instructure.student.room.entities.CreateSubmissionEntity
@@ -52,7 +52,7 @@ class SubmissionDetailsRepositoryFragment : SubmissionDetailsFragment() {
}
override fun getExternalEventSources() = listOf(
- ChannelSource.getSource {
+ FlowSource.getSource {
when (it) {
is SubmissionDetailsSharedEvent.FileSelected -> SubmissionDetailsEvent.AttachmentClicked(it.file)
is SubmissionDetailsSharedEvent.AudioRecordingViewLaunched -> SubmissionDetailsEvent.AudioRecordingClicked
diff --git a/apps/student/src/main/java/com/instructure/student/mobius/common/ChannelSource.kt b/apps/student/src/main/java/com/instructure/student/mobius/common/FlowSource.kt
similarity index 58%
rename from apps/student/src/main/java/com/instructure/student/mobius/common/ChannelSource.kt
rename to apps/student/src/main/java/com/instructure/student/mobius/common/FlowSource.kt
index c8bea100f7..d6c309937b 100644
--- a/apps/student/src/main/java/com/instructure/student/mobius/common/ChannelSource.kt
+++ b/apps/student/src/main/java/com/instructure/student/mobius/common/FlowSource.kt
@@ -22,26 +22,25 @@ import com.spotify.mobius.EventSource
import com.spotify.mobius.disposables.Disposable
import com.spotify.mobius.functions.Consumer
import kotlinx.coroutines.GlobalScope
-import kotlinx.coroutines.channels.BroadcastChannel
-import kotlinx.coroutines.channels.consumeEach
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.launch
/**
- * An EventSource which aids in mapping Channel data of one type to events a target type. To use this class, either
- * create a subclass and override [mapEvent], or call [ChannelSource.getSource] and pass in a function that performs
+ * An EventSource which aids in mapping Flow data of one type to events a target type. To use this class, either
+ * create a subclass and override [mapEvent], or call [FlowSource.getSource] and pass in a function that performs
* event mapping.
*/
-abstract class ChannelSource (private val channel: BroadcastChannel) : EventSource {
+abstract class FlowSource (private val sharedFlow: SharedFlow) : EventSource {
override fun subscribe(eventConsumer: Consumer): Disposable {
- val receiveChannel = channel.openSubscription()
- GlobalScope.launch {
- receiveChannel.consumeEach {
+ val job = GlobalScope.launch {
+ sharedFlow.collect {
val event = mapEvent(it)
event?.let { nunNullEvent -> eventConsumer.accept(nunNullEvent) }
}
}
- return Disposable { receiveChannel.cancel() }
+ return Disposable { job.cancel() }
}
/**
@@ -51,23 +50,19 @@ abstract class ChannelSource (private val channel: BroadcastCh
abstract fun mapEvent(event: T): E?
companion object {
- val channelStore = hashMapOf>()
+ val sharedFlowStore = hashMapOf>()
/**
- * Produces a [BroadcastChannel] of the specified type [T], returning an existing channel if it exists or creating
+ * Produces a [MutableSharedFlow] of the specified type [T], returning an existing channel if it exists or creating
* a new channel if either it does not exist, or it does exist but has been closed. Only one channel per unique
* type [T] will exist at a time.
*/
@Suppress("UNCHECKED_CAST")
- inline fun getChannel(): BroadcastChannel {
+ inline fun getFlow(): MutableSharedFlow {
val className = T::class.java.canonicalName!!
- var channel = channelStore[className]
- if ((channel as? BroadcastChannel)?.isClosedForSend != false) {
- channel?.close()
- channel = BroadcastChannel(100)
- channelStore[className] = channel
- }
- return channel
+ return sharedFlowStore.computeIfAbsent(className) {
+ MutableSharedFlow(replay = 0, extraBufferCapacity = 100)
+ } as MutableSharedFlow
}
/**
@@ -75,13 +70,26 @@ abstract class ChannelSource (private val channel: BroadcastCh
* must be provided to map input events of type [T] to events of type [E]. This function may return a null value
* if there is no valid mapping for the input event or if an output event should not be produced.
*/
- inline fun getSource(crossinline mapper: (T) -> E?): ChannelSource {
- val channel = getChannel()
- return object : ChannelSource(channel) {
+ inline fun getSource(crossinline mapper: (T) -> E?): FlowSource {
+ val flow = getFlow()
+ return object : FlowSource(flow) {
override fun mapEvent(event: T): E? = mapper(event)
}
}
}
+}
+/**
+ * Extension function for MutableSharedFlow that replicates the trySend behavior.
+ * It attempts to emit the event into the flow and returns a result similar to BroadcastChannel's trySend.
+ */
+fun MutableSharedFlow.trySend(value: T): Boolean {
+ return try {
+ // Emit value, with an immediate return of false if buffer is full (replay == 0 and no extraBufferCapacity)
+ this.tryEmit(value)
+ } catch (e: Exception) {
+ // In case of any exception (which is rare in SharedFlow), we return false
+ false
+ }
}
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 4346edbc44..df0a0235d9 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
@@ -26,7 +26,6 @@ import android.content.Context
import android.content.Intent
import android.content.pm.ServiceInfo
import android.os.Build
-import android.os.Bundle
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import com.instructure.canvasapi2.CanvasRestAdapter
@@ -57,7 +56,8 @@ import com.instructure.student.R
import com.instructure.student.activity.NavigationActivity
import com.instructure.student.events.ShowConfettiEvent
import com.instructure.student.mobius.assignmentDetails.submissionDetails.SubmissionDetailsSharedEvent
-import com.instructure.student.mobius.common.ChannelSource
+import com.instructure.student.mobius.common.FlowSource
+import com.instructure.student.mobius.common.trySend
import com.instructure.student.room.StudentDb
import com.instructure.student.room.entities.CreateFileSubmissionEntity
import com.instructure.student.room.entities.CreatePendingSubmissionCommentEntity
@@ -533,9 +533,9 @@ class SubmissionService : IntentService(SubmissionService::class.java.simpleName
}
val newComment = submission.submissionComments.last()
- ChannelSource.getChannel().trySend(newComment)
+ FlowSource.getFlow().trySend(newComment)
- ChannelSource.getChannel().trySend(
+ FlowSource.getFlow().trySend(
SubmissionDetailsSharedEvent.SubmissionCommentsUpdated(submission.submissionComments)
)
diff --git a/apps/student/src/main/java/com/instructure/student/mobius/elementary/importantdates/StudentImportantDatesRouter.kt b/apps/student/src/main/java/com/instructure/student/mobius/elementary/importantdates/StudentImportantDatesRouter.kt
index 484d6c55a0..0482a935d3 100644
--- a/apps/student/src/main/java/com/instructure/student/mobius/elementary/importantdates/StudentImportantDatesRouter.kt
+++ b/apps/student/src/main/java/com/instructure/student/mobius/elementary/importantdates/StudentImportantDatesRouter.kt
@@ -21,7 +21,7 @@ import com.instructure.canvasapi2.models.CanvasContext
import com.instructure.canvasapi2.models.ScheduleItem
import com.instructure.pandautils.features.calendarevent.details.EventFragment
import com.instructure.pandautils.features.elementary.importantdates.ImportantDatesRouter
-import com.instructure.student.features.assignments.details.AssignmentDetailsFragment
+import com.instructure.pandautils.features.assignments.details.AssignmentDetailsFragment
import com.instructure.student.router.RouteMatcher
class StudentImportantDatesRouter(private val activity: FragmentActivity) : ImportantDatesRouter {
diff --git a/apps/student/src/main/java/com/instructure/student/mobius/elementary/schedule/StudentScheduleRouter.kt b/apps/student/src/main/java/com/instructure/student/mobius/elementary/schedule/StudentScheduleRouter.kt
index ed92781f17..637925d34b 100644
--- a/apps/student/src/main/java/com/instructure/student/mobius/elementary/schedule/StudentScheduleRouter.kt
+++ b/apps/student/src/main/java/com/instructure/student/mobius/elementary/schedule/StudentScheduleRouter.kt
@@ -23,7 +23,7 @@ import com.instructure.canvasapi2.models.DiscussionTopicHeader
import com.instructure.pandautils.features.calendarevent.details.EventFragment
import com.instructure.pandautils.features.discussion.router.DiscussionRouterFragment
import com.instructure.pandautils.features.elementary.schedule.ScheduleRouter
-import com.instructure.student.features.assignments.details.AssignmentDetailsFragment
+import com.instructure.pandautils.features.assignments.details.AssignmentDetailsFragment
import com.instructure.student.features.elementary.course.ElementaryCourseFragment
import com.instructure.student.fragment.BasicQuizViewFragment
import com.instructure.student.router.RouteMatcher
diff --git a/apps/student/src/main/java/com/instructure/student/mobius/syllabus/ui/SyllabusFragment.kt b/apps/student/src/main/java/com/instructure/student/mobius/syllabus/ui/SyllabusFragment.kt
index b0b37257e4..5933a5ac11 100644
--- a/apps/student/src/main/java/com/instructure/student/mobius/syllabus/ui/SyllabusFragment.kt
+++ b/apps/student/src/main/java/com/instructure/student/mobius/syllabus/ui/SyllabusFragment.kt
@@ -16,8 +16,6 @@
*/
package com.instructure.student.mobius.syllabus.ui
-import android.view.LayoutInflater
-import android.view.ViewGroup
import com.instructure.canvasapi2.models.Course
import com.instructure.canvasapi2.utils.pageview.PageView
import com.instructure.pandautils.analytics.SCREEN_VIEW_SYLLABUS
@@ -26,7 +24,13 @@ import com.instructure.pandautils.utils.Const
import com.instructure.pandautils.utils.ParcelableArg
import com.instructure.student.databinding.FragmentSyllabusBinding
import com.instructure.student.mobius.common.ui.MobiusFragment
-import com.instructure.student.mobius.syllabus.*
+import com.instructure.student.mobius.syllabus.SyllabusEffect
+import com.instructure.student.mobius.syllabus.SyllabusEffectHandler
+import com.instructure.student.mobius.syllabus.SyllabusEvent
+import com.instructure.student.mobius.syllabus.SyllabusModel
+import com.instructure.student.mobius.syllabus.SyllabusPresenter
+import com.instructure.student.mobius.syllabus.SyllabusRepository
+import com.instructure.student.mobius.syllabus.SyllabusUpdate
@ScreenView(SCREEN_VIEW_SYLLABUS)
@PageView(url = "{canvasContext}/assignments/syllabus")
@@ -38,8 +42,6 @@ abstract class SyllabusFragment : MobiusFragment(
- inflater,
- FragmentSyllabusBinding::inflate,
- parent) {
+class SyllabusView(
+ val canvasContext: CanvasContext,
+ val webViewRouter: WebViewRouter,
+ inflater: LayoutInflater,
+ parent: ViewGroup
+) : MobiusView(
+ inflater,
+ FragmentSyllabusBinding::inflate,
+ parent
+) {
private val adapter: SyllabusTabAdapter
@@ -106,10 +113,7 @@ class SyllabusView(val canvasContext: CanvasContext, inflater: LayoutInflater, p
pager.setCurrentItem(if (state.syllabus == null) 1 else 0, false)
- if (state.syllabus != null) webviewBinding?.syllabusWebViewWrapper?.loadHtml(
- state.syllabus,
- context.getString(com.instructure.pandares.R.string.syllabus)
- )
+ if (state.syllabus != null) renderWebView(state.syllabus)
if (state.eventsState != null) renderEvents(state.eventsState)
@@ -118,6 +122,25 @@ class SyllabusView(val canvasContext: CanvasContext, inflater: LayoutInflater, p
}
}
+ private fun renderWebView(syllabus: String) {
+ webviewBinding?.syllabusWebViewWrapper?.apply {
+ webView.canvasWebViewClientCallback?.let {
+ webView.canvasWebViewClientCallback = object : CanvasWebView.CanvasWebViewClientCallback by it {
+ override fun openMediaFromWebView(mime: String, url: String, filename: String) = webViewRouter.openMedia(url)
+
+ override fun canRouteInternallyDelegate(url: String) = webViewRouter.canRouteInternally(url)
+
+ override fun routeInternallyCallback(url: String) = webViewRouter.routeInternally(url)
+ }
+ }
+
+ loadHtml(
+ syllabus,
+ context.getString(com.instructure.pandares.R.string.syllabus)
+ )
+ }
+ }
+
private fun setupSwipeableChildren(position: Int?) {
if (position == 0) {
binding.swipeRefreshLayout.setSwipeableChildren(R.id.syllabusScrollView)
diff --git a/apps/student/src/main/java/com/instructure/student/navigation/StudentWebViewRouter.kt b/apps/student/src/main/java/com/instructure/student/navigation/StudentWebViewRouter.kt
index 20325f6232..a05b3f115d 100644
--- a/apps/student/src/main/java/com/instructure/student/navigation/StudentWebViewRouter.kt
+++ b/apps/student/src/main/java/com/instructure/student/navigation/StudentWebViewRouter.kt
@@ -16,6 +16,7 @@
*/
package com.instructure.student.navigation
+import android.os.Bundle
import androidx.fragment.app.FragmentActivity
import com.instructure.canvasapi2.models.CanvasContext
import com.instructure.canvasapi2.utils.ApiPrefs
@@ -31,8 +32,12 @@ class StudentWebViewRouter(val activity: FragmentActivity) : WebViewRouter {
return RouteMatcher.canRouteInternally(activity, url, ApiPrefs.domain, routeIfPossible = routeIfPossible, allowUnsupported = false)
}
- override fun routeInternally(url: String) {
- RouteMatcher.canRouteInternally(activity, url, ApiPrefs.domain, routeIfPossible = true, allowUnsupported = false)
+ override fun routeInternally(url: String, extras: Bundle?) {
+ if (extras != null) {
+ RouteMatcher.routeUrl(activity, url, ApiPrefs.domain, extras)
+ } else {
+ RouteMatcher.canRouteInternally(activity, url, ApiPrefs.domain, routeIfPossible = true, allowUnsupported = false)
+ }
}
override fun openMedia(url: String, mime: String, filename: String, canvasContext: CanvasContext?) {
diff --git a/apps/student/src/main/java/com/instructure/student/receivers/AlarmReceiver.kt b/apps/student/src/main/java/com/instructure/student/receivers/AlarmReceiver.kt
deleted file mode 100644
index 0f02fe27f8..0000000000
--- a/apps/student/src/main/java/com/instructure/student/receivers/AlarmReceiver.kt
+++ /dev/null
@@ -1,100 +0,0 @@
-/*
- * Copyright (C) 2024 - 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.receivers
-
-import android.app.NotificationChannel
-import android.app.NotificationManager
-import android.app.PendingIntent
-import android.content.BroadcastReceiver
-import android.content.Context
-import android.content.Intent
-import androidx.core.app.NotificationCompat
-import com.instructure.pandautils.models.PushNotification
-import com.instructure.pandautils.room.appdatabase.daos.ReminderDao
-import com.instructure.pandautils.utils.Const
-import com.instructure.student.R
-import com.instructure.student.activity.NavigationActivity
-import com.instructure.student.util.goAsync
-import dagger.hilt.android.AndroidEntryPoint
-import javax.inject.Inject
-
-@AndroidEntryPoint
-class AlarmReceiver : BroadcastReceiver() {
-
- @Inject
- lateinit var reminderDao: ReminderDao
-
- override fun onReceive(context: Context?, intent: Intent?) {
- if (context != null && intent != null) {
- val assignmentId = intent.getLongExtra(ASSIGNMENT_ID, 0L)
- val assignmentPath = intent.getStringExtra(ASSIGNMENT_PATH) ?: return
- val assignmentName = intent.getStringExtra(ASSIGNMENT_NAME) ?: return
- val dueIn = intent.getStringExtra(DUE_IN) ?: return
-
- createNotificationChannel(context)
- showNotification(context, assignmentId, assignmentPath, assignmentName, dueIn)
- goAsync {
- reminderDao.deletePastReminders(System.currentTimeMillis())
- }
- }
- }
-
- private fun showNotification(context: Context, assignmentId: Long, assignmentPath: String, assignmentName: String, dueIn: String) {
- val intent = Intent(context, NavigationActivity.startActivityClass).apply {
- putExtra(Const.LOCAL_NOTIFICATION, true)
- putExtra(PushNotification.HTML_URL, assignmentPath)
- }
-
- val pendingIntent = PendingIntent.getActivity(
- context, 0, intent,
- PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
- )
-
- val builder = NotificationCompat.Builder(context, CHANNEL_ID)
- .setSmallIcon(R.drawable.ic_notification_canvas_logo)
- .setContentTitle(context.getString(R.string.reminderNotificationTitle))
- .setContentText(context.getString(R.string.reminderNotificationDescription, dueIn, assignmentName))
- .setAutoCancel(true)
- .setContentIntent(pendingIntent)
- .setPriority(NotificationCompat.PRIORITY_DEFAULT)
-
- val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
- notificationManager.notify(assignmentId.toInt(), builder.build())
- }
-
- private fun createNotificationChannel(context: Context) {
- val channel = NotificationChannel(
- CHANNEL_ID,
- context.getString(R.string.reminderNotificationChannelName),
- NotificationManager.IMPORTANCE_DEFAULT
- ).apply {
- description = context.getString(R.string.reminderNotificationChannelDescription)
- }
-
- val notificationManager: NotificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
- notificationManager.createNotificationChannel(channel)
- }
-
- companion object {
- private const val CHANNEL_ID = "REMINDERS_CHANNEL_ID"
- const val ASSIGNMENT_ID = "ASSIGNMENT_ID"
- const val ASSIGNMENT_PATH = "ASSIGNMENT_PATH"
- const val ASSIGNMENT_NAME = "ASSIGNMENT_NAME"
- const val DUE_IN = "DUE_IN"
- }
-}
\ No newline at end of file
diff --git a/apps/student/src/main/java/com/instructure/student/receivers/InitializeReceiver.kt b/apps/student/src/main/java/com/instructure/student/receivers/InitializeReceiver.kt
index 90560f5f20..d5138445d8 100644
--- a/apps/student/src/main/java/com/instructure/student/receivers/InitializeReceiver.kt
+++ b/apps/student/src/main/java/com/instructure/student/receivers/InitializeReceiver.kt
@@ -20,10 +20,10 @@ package com.instructure.student.receivers
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
+import com.instructure.pandautils.features.assignments.details.reminder.AlarmScheduler
import com.instructure.pandautils.receivers.PushExternalReceiver
import com.instructure.student.R
import com.instructure.student.activity.NavigationActivity
-import com.instructure.student.features.assignments.reminder.AlarmScheduler
import com.instructure.student.util.goAsync
import com.instructure.student.widget.WidgetUpdater
import dagger.hilt.android.AndroidEntryPoint
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 34580643e3..089f4ee123 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
@@ -50,7 +50,7 @@ import com.instructure.pandautils.utils.RouteUtils
import com.instructure.pandautils.utils.nonNullArgs
import com.instructure.student.R
import com.instructure.student.activity.*
-import com.instructure.student.features.assignments.details.AssignmentDetailsFragment
+import com.instructure.pandautils.features.assignments.details.AssignmentDetailsFragment
import com.instructure.student.features.assignments.list.AssignmentListFragment
import com.instructure.student.features.coursebrowser.CourseBrowserFragment
import com.instructure.student.features.discussion.details.DiscussionDetailsFragment
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 b2e0efc6fe..77ddbd1324 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
@@ -19,7 +19,7 @@ import com.instructure.pandautils.features.offline.sync.progress.SyncProgressFra
import com.instructure.pandautils.utils.Const
import com.instructure.student.AnnotationComments.AnnotationCommentListFragment
import com.instructure.student.activity.NothingToSeeHereFragment
-import com.instructure.student.features.assignments.details.AssignmentDetailsFragment
+import com.instructure.pandautils.features.assignments.details.AssignmentDetailsFragment
import com.instructure.student.features.assignments.list.AssignmentListFragment
import com.instructure.student.features.coursebrowser.CourseBrowserFragment
import com.instructure.student.features.discussion.details.DiscussionDetailsFragment
diff --git a/apps/student/src/main/java/com/instructure/student/tasks/StudentLogoutTask.kt b/apps/student/src/main/java/com/instructure/student/tasks/StudentLogoutTask.kt
index 673aa1da5b..c59c3905bd 100644
--- a/apps/student/src/main/java/com/instructure/student/tasks/StudentLogoutTask.kt
+++ b/apps/student/src/main/java/com/instructure/student/tasks/StudentLogoutTask.kt
@@ -29,7 +29,7 @@ import com.instructure.pandautils.features.offline.sync.OfflineSyncWorker
import com.instructure.pandautils.room.offline.DatabaseProvider
import com.instructure.pandautils.typeface.TypefaceBehavior
import com.instructure.student.activity.LoginActivity
-import com.instructure.student.features.assignments.reminder.AlarmScheduler
+import com.instructure.pandautils.features.assignments.details.reminder.AlarmScheduler
import com.instructure.student.util.StudentPrefs
import com.instructure.student.widget.WidgetUpdater
import java.io.File
diff --git a/apps/student/src/main/java/com/instructure/student/util/AppManager.kt b/apps/student/src/main/java/com/instructure/student/util/AppManager.kt
index c43f7fb665..b3203e6020 100644
--- a/apps/student/src/main/java/com/instructure/student/util/AppManager.kt
+++ b/apps/student/src/main/java/com/instructure/student/util/AppManager.kt
@@ -23,7 +23,7 @@ import com.instructure.canvasapi2.utils.MasqueradeHelper
import com.instructure.loginapi.login.tasks.LogoutTask
import com.instructure.pandautils.room.offline.DatabaseProvider
import com.instructure.pandautils.typeface.TypefaceBehavior
-import com.instructure.student.features.assignments.reminder.AlarmScheduler
+import com.instructure.pandautils.features.assignments.details.reminder.AlarmScheduler
import com.instructure.student.tasks.StudentLogoutTask
import dagger.hilt.android.HiltAndroidApp
import javax.inject.Inject
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 4c2f0743e6..fe26a2ca9f 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
@@ -38,6 +38,7 @@ import com.instructure.student.service.StudentPageViewService
import com.pspdfkit.PSPDFKit
import com.pspdfkit.exceptions.InvalidPSPDFKitLicenseException
import com.pspdfkit.exceptions.PSPDFKitInitializationFailedException
+import com.pspdfkit.initialization.InitializationOptions
import com.zynksoftware.documentscanner.ui.DocumentScanner
abstract class BaseAppManager : com.instructure.canvasapi2.AppManager(), AnalyticsEventHandling {
@@ -116,7 +117,7 @@ abstract class BaseAppManager : com.instructure.canvasapi2.AppManager(), Analyti
private fun initPSPDFKit() {
try {
- PSPDFKit.initialize(this, BuildConfig.PSPDFKIT_LICENSE_KEY)
+ PSPDFKit.initialize(this, InitializationOptions(licenseKey = BuildConfig.PSPDFKIT_LICENSE_KEY))
} catch (e: PSPDFKitInitializationFailedException) {
Logger.e("Current device is not compatible with PSPDFKIT!")
} catch (e: InvalidPSPDFKitLicenseException) {
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 a012f9f8fc..0c33dc9901 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
@@ -121,7 +121,7 @@ class NotificationViewWidgetService : BaseRemoteViewsService(), Serializable {
private fun getDrawableId(streamItem: StreamItem): Int {
when (streamItem.getStreamItemType()) {
- StreamItem.Type.DISCUSSION_TOPIC -> return R.drawable.ic_discussion
+ StreamItem.Type.DISCUSSION_TOPIC, StreamItem.Type.DISCUSSION_ENTRY, StreamItem.Type.DISCUSSION_MENTION -> return R.drawable.ic_discussion
StreamItem.Type.ANNOUNCEMENT -> return R.drawable.ic_announcement
StreamItem.Type.SUBMISSION -> return R.drawable.ic_assignment
StreamItem.Type.CONVERSATION -> return R.drawable.ic_inbox
diff --git a/apps/student/src/main/res/layout/adapter_rubric_grade.xml b/apps/student/src/main/res/layout/adapter_rubric_grade.xml
index 94f1d75ff2..a952c9d53c 100644
--- a/apps/student/src/main/res/layout/adapter_rubric_grade.xml
+++ b/apps/student/src/main/res/layout/adapter_rubric_grade.xml
@@ -20,7 +20,7 @@
android:layout_height="wrap_content"
android:orientation="vertical">
-
+
+
+
+
+
+
+
+
260dp
12dp
16dp
- 8dp
- 0dp
diff --git a/apps/student/src/main/res/values/dimens.xml b/apps/student/src/main/res/values/dimens.xml
index e16e413389..07a5867b87 100644
--- a/apps/student/src/main/res/values/dimens.xml
+++ b/apps/student/src/main/res/values/dimens.xml
@@ -70,7 +70,4 @@
10sp
10sp
- 16dp
- 8dp
-
diff --git a/apps/student/src/test/java/com/instructure/student/features/assignmentlist/AssignmentListRepositoryTest.kt b/apps/student/src/test/java/com/instructure/student/features/assignmentlist/AssignmentListRepositoryTest.kt
index d328763653..842e3f59ea 100644
--- a/apps/student/src/test/java/com/instructure/student/features/assignmentlist/AssignmentListRepositoryTest.kt
+++ b/apps/student/src/test/java/com/instructure/student/features/assignmentlist/AssignmentListRepositoryTest.kt
@@ -20,7 +20,6 @@ package com.instructure.student.features.assignmentlist
import com.instructure.canvasapi2.models.AssignmentGroup
import com.instructure.canvasapi2.models.Course
import com.instructure.canvasapi2.models.GradingPeriod
-import com.instructure.pandautils.utils.FEATURE_FLAG_OFFLINE
import com.instructure.pandautils.utils.FeatureFlagProvider
import com.instructure.pandautils.utils.NetworkStateProvider
import com.instructure.student.features.assignments.list.AssignmentListRepository
@@ -30,14 +29,12 @@ import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.every
import io.mockk.mockk
-import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.Assert
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Test
-@ExperimentalCoroutinesApi
class AssignmentListRepositoryTest {
private val networkDataSource: AssignmentListNetworkDataSource = mockk(relaxed = true)
diff --git a/apps/student/src/test/java/com/instructure/student/features/assignmentlist/datasource/AssignmentListLocalDataSourceTest.kt b/apps/student/src/test/java/com/instructure/student/features/assignmentlist/datasource/AssignmentListLocalDataSourceTest.kt
index 8baf7cfae7..f608f3aa0c 100644
--- a/apps/student/src/test/java/com/instructure/student/features/assignmentlist/datasource/AssignmentListLocalDataSourceTest.kt
+++ b/apps/student/src/test/java/com/instructure/student/features/assignmentlist/datasource/AssignmentListLocalDataSourceTest.kt
@@ -26,12 +26,10 @@ import com.instructure.pandautils.room.offline.facade.CourseFacade
import com.instructure.student.features.assignments.list.datasource.AssignmentListLocalDataSource
import io.mockk.coEvery
import io.mockk.mockk
-import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertEquals
import org.junit.Test
-@ExperimentalCoroutinesApi
class AssignmentListLocalDataSourceTest {
private val assignmentFacade: AssignmentFacade = mockk(relaxed = true)
diff --git a/apps/student/src/test/java/com/instructure/student/features/assignmentlist/datasource/AssignmentListNetworkDataSourceTest.kt b/apps/student/src/test/java/com/instructure/student/features/assignmentlist/datasource/AssignmentListNetworkDataSourceTest.kt
index 3c132a2709..0229a86b6a 100644
--- a/apps/student/src/test/java/com/instructure/student/features/assignmentlist/datasource/AssignmentListNetworkDataSourceTest.kt
+++ b/apps/student/src/test/java/com/instructure/student/features/assignmentlist/datasource/AssignmentListNetworkDataSourceTest.kt
@@ -27,12 +27,10 @@ import com.instructure.canvasapi2.utils.DataResult
import com.instructure.student.features.assignments.list.datasource.AssignmentListNetworkDataSource
import io.mockk.coEvery
import io.mockk.mockk
-import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.Assert
import org.junit.Test
-@ExperimentalCoroutinesApi
class AssignmentListNetworkDataSourceTest {
private val assignmentApi: AssignmentAPI.AssignmentInterface = mockk(relaxed = true)
diff --git a/apps/student/src/test/java/com/instructure/student/features/assignments/details/StudentAssignmentDetailsColorProviderTest.kt b/apps/student/src/test/java/com/instructure/student/features/assignments/details/StudentAssignmentDetailsColorProviderTest.kt
new file mode 100644
index 0000000000..6a87ecfc30
--- /dev/null
+++ b/apps/student/src/test/java/com/instructure/student/features/assignments/details/StudentAssignmentDetailsColorProviderTest.kt
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2024 - 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.assignments.details
+
+import com.instructure.canvasapi2.models.Course
+import com.instructure.pandautils.utils.ColorKeeper
+import com.instructure.pandautils.utils.ThemePrefs
+import com.instructure.pandautils.utils.ThemedColor
+import io.mockk.every
+import io.mockk.mockk
+import io.mockk.mockkObject
+import io.mockk.unmockkAll
+import junit.framework.TestCase.assertEquals
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+
+class StudentAssignmentDetailsColorProviderTest {
+
+ @Before
+ fun setUp() {
+ mockkObject(ThemePrefs)
+ every { ThemePrefs.textButtonColor } returns 1
+
+ mockkObject(ColorKeeper)
+ every { ColorKeeper.getOrGenerateColor(any()) } returns ThemedColor(0)
+ }
+
+ @After
+ fun tearDown() {
+ unmockkAll()
+ }
+
+ @Test
+ fun `submissionAndRubricLabelColor should return ThemePrefs textButtonColor`() {
+ val colorKeeper: ColorKeeper = mockk(relaxed = true)
+ val colorProvider = StudentAssignmentDetailsColorProvider(colorKeeper)
+
+ assertEquals(ThemePrefs.textButtonColor, colorProvider.submissionAndRubricLabelColor)
+ }
+
+ @Test
+ fun `getContentColor should return colorKeeper getOrGenerateColor`() {
+ val colorKeeper: ColorKeeper = mockk(relaxed = true)
+ val colorProvider = StudentAssignmentDetailsColorProvider(colorKeeper)
+
+ val course = mockk()
+ val expected = ThemedColor(0)
+ val result = colorProvider.getContentColor(course)
+ assertEquals(expected, result)
+ }
+}
\ No newline at end of file
diff --git a/apps/student/src/test/java/com/instructure/student/features/assignments/details/StudentAssignmentDetailsSubmissionHandlerTest.kt b/apps/student/src/test/java/com/instructure/student/features/assignments/details/StudentAssignmentDetailsSubmissionHandlerTest.kt
new file mode 100644
index 0000000000..feb1ca2e3f
--- /dev/null
+++ b/apps/student/src/test/java/com/instructure/student/features/assignments/details/StudentAssignmentDetailsSubmissionHandlerTest.kt
@@ -0,0 +1,155 @@
+/*
+ * Copyright (C) 2024 - 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.features.assignments.details
+
+import android.util.Log
+import androidx.arch.core.executor.testing.InstantTaskExecutorRule
+import androidx.lifecycle.MutableLiveData
+import com.instructure.canvasapi2.models.CanvasContext
+import com.instructure.canvasapi2.models.Course
+import com.instructure.canvasapi2.utils.ContextKeeper
+import com.instructure.pandautils.features.assignments.details.AssignmentDetailsViewData
+import com.instructure.pandautils.utils.ColorKeeper
+import com.instructure.pandautils.utils.ThemePrefs
+import com.instructure.student.mobius.common.ui.SubmissionHelper
+import com.instructure.student.room.StudentDb
+import com.instructure.student.room.entities.CreateSubmissionEntity
+import io.mockk.every
+import io.mockk.mockk
+import io.mockk.mockkStatic
+import io.mockk.unmockkAll
+import junit.framework.TestCase.assertEquals
+import junit.framework.TestCase.assertFalse
+import junit.framework.TestCase.assertTrue
+import org.junit.After
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+
+class StudentAssignmentDetailsSubmissionHandlerTest {
+ @get:Rule
+ var instantExecutorRule = InstantTaskExecutorRule()
+
+ private val submissionHelper: SubmissionHelper = mockk(relaxed = true)
+ private val studentDb: StudentDb = mockk(relaxed = true)
+
+ private lateinit var submissionHandler: StudentAssignmentDetailsSubmissionHandler
+
+ @Before
+ fun setUp() {
+ mockkStatic(Log::class)
+ every { Log.d(any(), any()) } returns 0
+ every { Log.e(any(), any()) } returns 0
+
+ ContextKeeper.appContext = mockk(relaxed = true)
+ }
+
+ @After
+ fun tearDown() {
+ unmockkAll()
+ }
+
+ @Test
+ fun `Test initial values`() {
+ submissionHandler = StudentAssignmentDetailsSubmissionHandler(submissionHelper, studentDb)
+ assertEquals(false, submissionHandler.isUploading)
+ assertEquals(false, submissionHandler.lastSubmissionIsDraft)
+ assertEquals(null, submissionHandler.lastSubmissionEntry)
+ assertEquals(null, submissionHandler.lastSubmissionAssignmentId)
+ assertEquals(null, submissionHandler.lastSubmissionSubmissionType)
+ }
+
+ @Test
+ fun `Upload fail`() {
+ submissionHandler = StudentAssignmentDetailsSubmissionHandler(submissionHelper, studentDb)
+
+ val data = MutableLiveData(AssignmentDetailsViewData(
+ courseColor = ColorKeeper.getOrGenerateColor(Course()),
+ submissionAndRubricLabelColor = ThemePrefs.textButtonColor,
+ assignmentName = "Assignment",
+ points = "0",
+ submissionStatusText = "Status",
+ submissionStatusIcon = 1,
+ submissionStatusTint = 1,
+ submissionStatusVisible = true,
+ fullLocked = true,
+ lockedMessage = ""
+ ))
+
+ val liveData = MutableLiveData>(listOf())
+ every {
+ studentDb.submissionDao().findSubmissionsByAssignmentIdLiveData(any(), any())
+ } returns liveData
+
+ submissionHandler.addAssignmentSubmissionObserver(0, 0, mockk(relaxed = true), data, {})
+
+ liveData.postValue(listOf(getDbSubmission()))
+
+ assertTrue(submissionHandler.isUploading)
+
+ liveData.postValue(listOf(getDbSubmission().copy(errorFlag = true)))
+ assertTrue(data.value?.attempts?.first()?.data?.isFailed!!)
+ }
+
+ @Test
+ fun `Upload success`() {
+ submissionHandler = StudentAssignmentDetailsSubmissionHandler(submissionHelper, studentDb)
+
+ val data = MutableLiveData(AssignmentDetailsViewData(
+ courseColor = ColorKeeper.getOrGenerateColor(Course()),
+ submissionAndRubricLabelColor = ThemePrefs.textButtonColor,
+ assignmentName = "Assignment",
+ points = "0",
+ submissionStatusText = "Status",
+ submissionStatusIcon = 1,
+ submissionStatusTint = 1,
+ submissionStatusVisible = true,
+ fullLocked = true,
+ lockedMessage = ""
+ ))
+
+ val liveData = MutableLiveData>(listOf())
+ every {
+ studentDb.submissionDao().findSubmissionsByAssignmentIdLiveData(any(), any())
+ } returns liveData
+
+ submissionHandler.addAssignmentSubmissionObserver(0, 0, mockk(relaxed = true), data, {})
+
+ liveData.postValue(listOf(getDbSubmission()))
+ assertTrue(submissionHandler.isUploading)
+
+ liveData.postValue(emptyList())
+ assertFalse(submissionHandler.isUploading)
+ }
+
+ private fun getDbSubmission() = CreateSubmissionEntity(
+ id = 0,
+ submissionEntry = "",
+ lastActivityDate = null,
+ assignmentName = null,
+ assignmentId = 0,
+ canvasContext = CanvasContext.emptyCourseContext(0),
+ submissionType = "",
+ errorFlag = false,
+ assignmentGroupCategoryId = null,
+ userId = 0,
+ currentFile = 0,
+ fileCount = 0,
+ progress = null,
+ annotatableAttachmentId = null,
+ isDraft = false
+ )
+}
\ No newline at end of file
diff --git a/apps/student/src/test/java/com/instructure/student/features/calendar/StudentCalendarRepositoryTest.kt b/apps/student/src/test/java/com/instructure/student/features/calendar/StudentCalendarRepositoryTest.kt
index 19802f1e59..e943963d23 100644
--- a/apps/student/src/test/java/com/instructure/student/features/calendar/StudentCalendarRepositoryTest.kt
+++ b/apps/student/src/test/java/com/instructure/student/features/calendar/StudentCalendarRepositoryTest.kt
@@ -38,13 +38,11 @@ import com.instructure.pandautils.room.calendar.entities.CalendarFilterEntity
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.mockk
-import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertEquals
import org.junit.Test
import java.util.Date
-@ExperimentalCoroutinesApi
class StudentCalendarRepositoryTest {
private val plannerApi: PlannerAPI.PlannerInterface = mockk(relaxed = true)
diff --git a/apps/student/src/test/java/com/instructure/student/features/dashboard/DashboardLocalDataSourceTest.kt b/apps/student/src/test/java/com/instructure/student/features/dashboard/DashboardLocalDataSourceTest.kt
index a3e79dc0c8..b2c28ed595 100644
--- a/apps/student/src/test/java/com/instructure/student/features/dashboard/DashboardLocalDataSourceTest.kt
+++ b/apps/student/src/test/java/com/instructure/student/features/dashboard/DashboardLocalDataSourceTest.kt
@@ -25,12 +25,10 @@ import com.instructure.pandautils.room.offline.facade.CourseFacade
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.mockk
-import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.Assert
import org.junit.Test
-@ExperimentalCoroutinesApi
class DashboardLocalDataSourceTest {
private val courseFacade: CourseFacade = mockk(relaxed = true)
diff --git a/apps/student/src/test/java/com/instructure/student/features/dashboard/DashboardNetworkDataSourceTest.kt b/apps/student/src/test/java/com/instructure/student/features/dashboard/DashboardNetworkDataSourceTest.kt
index 94ea531de5..838c3084eb 100644
--- a/apps/student/src/test/java/com/instructure/student/features/dashboard/DashboardNetworkDataSourceTest.kt
+++ b/apps/student/src/test/java/com/instructure/student/features/dashboard/DashboardNetworkDataSourceTest.kt
@@ -27,12 +27,10 @@ import com.instructure.canvasapi2.utils.DataResult
import io.mockk.coEvery
import io.mockk.every
import io.mockk.mockk
-import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.Assert
import org.junit.Test
-@ExperimentalCoroutinesApi
class DashboardNetworkDataSourceTest {
private val courseApi: CourseAPI.CoursesInterface = mockk(relaxed = true)
diff --git a/apps/student/src/test/java/com/instructure/student/features/dashboard/DashboardRepositoryTest.kt b/apps/student/src/test/java/com/instructure/student/features/dashboard/DashboardRepositoryTest.kt
index fcf03f3d66..fad9b71a95 100644
--- a/apps/student/src/test/java/com/instructure/student/features/dashboard/DashboardRepositoryTest.kt
+++ b/apps/student/src/test/java/com/instructure/student/features/dashboard/DashboardRepositoryTest.kt
@@ -30,13 +30,11 @@ import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.every
import io.mockk.mockk
-import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.Assert
import org.junit.Before
import org.junit.Test
-@ExperimentalCoroutinesApi
class DashboardRepositoryTest {
private val networkDataSource: DashboardNetworkDataSource = mockk(relaxed = true)
diff --git a/apps/student/src/test/java/com/instructure/student/features/dashboard/edit/StudentEditDashboardRepositoryTest.kt b/apps/student/src/test/java/com/instructure/student/features/dashboard/edit/StudentEditDashboardRepositoryTest.kt
index 6375440fa3..e3071d5cba 100644
--- a/apps/student/src/test/java/com/instructure/student/features/dashboard/edit/StudentEditDashboardRepositoryTest.kt
+++ b/apps/student/src/test/java/com/instructure/student/features/dashboard/edit/StudentEditDashboardRepositoryTest.kt
@@ -26,7 +26,6 @@ import com.instructure.pandautils.room.offline.daos.CourseDao
import com.instructure.pandautils.room.offline.daos.CourseSyncSettingsDao
import com.instructure.pandautils.room.offline.entities.CourseEntity
import com.instructure.pandautils.room.offline.entities.CourseSyncSettingsEntity
-import com.instructure.pandautils.utils.FEATURE_FLAG_OFFLINE
import com.instructure.pandautils.utils.FeatureFlagProvider
import com.instructure.pandautils.utils.NetworkStateProvider
import com.instructure.student.features.dashboard.edit.datasource.StudentEditDashboardLocalDataSource
@@ -34,7 +33,6 @@ import com.instructure.student.features.dashboard.edit.datasource.StudentEditDas
import io.mockk.coEvery
import io.mockk.every
import io.mockk.mockk
-import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.Assert
import org.junit.Assert.assertEquals
@@ -43,7 +41,6 @@ import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
-@ExperimentalCoroutinesApi
class StudentEditDashboardRepositoryTest {
private val networkDataSource: StudentEditDashboardNetworkDataSource = mockk(relaxed = true)
diff --git a/apps/student/src/test/java/com/instructure/student/features/dashboard/edit/datasource/StudentEditDashboardLocalDataSourceTest.kt b/apps/student/src/test/java/com/instructure/student/features/dashboard/edit/datasource/StudentEditDashboardLocalDataSourceTest.kt
index 6671cff396..d41141943e 100644
--- a/apps/student/src/test/java/com/instructure/student/features/dashboard/edit/datasource/StudentEditDashboardLocalDataSourceTest.kt
+++ b/apps/student/src/test/java/com/instructure/student/features/dashboard/edit/datasource/StudentEditDashboardLocalDataSourceTest.kt
@@ -24,12 +24,10 @@ import com.instructure.pandautils.room.offline.entities.EnrollmentState
import com.instructure.pandautils.room.offline.facade.CourseFacade
import io.mockk.coEvery
import io.mockk.mockk
-import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertEquals
import org.junit.Test
-@ExperimentalCoroutinesApi
class StudentEditDashboardLocalDataSourceTest {
private val courseFacade: CourseFacade = mockk(relaxed = true)
diff --git a/apps/student/src/test/java/com/instructure/student/features/dashboard/edit/datasource/StudentEditDashboardNetwoekDataSourceTest.kt b/apps/student/src/test/java/com/instructure/student/features/dashboard/edit/datasource/StudentEditDashboardNetwoekDataSourceTest.kt
index 94af8b3cce..0a8bc2e1de 100644
--- a/apps/student/src/test/java/com/instructure/student/features/dashboard/edit/datasource/StudentEditDashboardNetwoekDataSourceTest.kt
+++ b/apps/student/src/test/java/com/instructure/student/features/dashboard/edit/datasource/StudentEditDashboardNetwoekDataSourceTest.kt
@@ -23,12 +23,10 @@ import com.instructure.canvasapi2.models.Group
import com.instructure.canvasapi2.utils.DataResult
import io.mockk.coEvery
import io.mockk.mockk
-import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertEquals
import org.junit.Test
-@ExperimentalCoroutinesApi
class StudentEditDashboardNetworkDataSourceTest {
private val courseApi: CourseAPI.CoursesInterface = mockk(relaxed = true)
diff --git a/apps/student/src/test/java/com/instructure/student/features/discussion/details/DiscussionDetailsRepositoryTest.kt b/apps/student/src/test/java/com/instructure/student/features/discussion/details/DiscussionDetailsRepositoryTest.kt
index 06e07ceaa4..2e4cfc38f0 100644
--- a/apps/student/src/test/java/com/instructure/student/features/discussion/details/DiscussionDetailsRepositoryTest.kt
+++ b/apps/student/src/test/java/com/instructure/student/features/discussion/details/DiscussionDetailsRepositoryTest.kt
@@ -7,7 +7,6 @@ import com.instructure.canvasapi2.models.DiscussionTopicHeader
import com.instructure.canvasapi2.models.Group
import com.instructure.canvasapi2.utils.DataResult
import com.instructure.canvasapi2.utils.Failure
-import com.instructure.pandautils.utils.FEATURE_FLAG_OFFLINE
import com.instructure.pandautils.utils.FeatureFlagProvider
import com.instructure.pandautils.utils.NetworkStateProvider
import com.instructure.student.features.discussion.details.datasource.DiscussionDetailsLocalDataSource
@@ -17,12 +16,10 @@ import io.mockk.coVerify
import io.mockk.every
import io.mockk.mockk
import junit.framework.TestCase.assertEquals
-import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
-@ExperimentalCoroutinesApi
class DiscussionDetailsRepositoryTest {
private val networkDataSource: DiscussionDetailsNetworkDataSource = mockk(relaxed = true)
diff --git a/apps/student/src/test/java/com/instructure/student/features/discussion/details/datasource/DiscussionDetailsLocalDataSourceTest.kt b/apps/student/src/test/java/com/instructure/student/features/discussion/details/datasource/DiscussionDetailsLocalDataSourceTest.kt
index 17989d2ab9..30190dc411 100644
--- a/apps/student/src/test/java/com/instructure/student/features/discussion/details/datasource/DiscussionDetailsLocalDataSourceTest.kt
+++ b/apps/student/src/test/java/com/instructure/student/features/discussion/details/datasource/DiscussionDetailsLocalDataSourceTest.kt
@@ -12,11 +12,9 @@ import com.instructure.pandautils.room.offline.facade.GroupFacade
import io.mockk.coEvery
import io.mockk.mockk
import junit.framework.TestCase.assertEquals
-import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.Test
-@ExperimentalCoroutinesApi
class DiscussionDetailsLocalDataSourceTest {
private val discussionTopicHeaderFacade: DiscussionTopicHeaderFacade = mockk(relaxed = true)
private val discussionTopicFacade: DiscussionTopicFacade = mockk(relaxed = true)
diff --git a/apps/student/src/test/java/com/instructure/student/features/discussion/details/datasource/DiscussionDetailsNetworkDataSourceTest.kt b/apps/student/src/test/java/com/instructure/student/features/discussion/details/datasource/DiscussionDetailsNetworkDataSourceTest.kt
index e63ffdf702..37ac897a8b 100644
--- a/apps/student/src/test/java/com/instructure/student/features/discussion/details/datasource/DiscussionDetailsNetworkDataSourceTest.kt
+++ b/apps/student/src/test/java/com/instructure/student/features/discussion/details/datasource/DiscussionDetailsNetworkDataSourceTest.kt
@@ -15,11 +15,9 @@ import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.mockk
import junit.framework.TestCase.assertEquals
-import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.Test
-@ExperimentalCoroutinesApi
class DiscussionDetailsNetworkDataSourceTest {
private val discussionApi: DiscussionAPI.DiscussionInterface = mockk(relaxed = true)
private val oAuthApi: OAuthAPI.OAuthInterface = mockk(relaxed = true)
diff --git a/apps/student/src/test/java/com/instructure/student/features/discussion/list/DiscussionListRepositoryTest.kt b/apps/student/src/test/java/com/instructure/student/features/discussion/list/DiscussionListRepositoryTest.kt
index b9274b200f..fd5c4c8d53 100644
--- a/apps/student/src/test/java/com/instructure/student/features/discussion/list/DiscussionListRepositoryTest.kt
+++ b/apps/student/src/test/java/com/instructure/student/features/discussion/list/DiscussionListRepositoryTest.kt
@@ -20,7 +20,6 @@ import com.instructure.canvasapi2.models.CanvasContextPermission
import com.instructure.canvasapi2.models.Course
import com.instructure.canvasapi2.models.DiscussionTopicHeader
import com.instructure.canvasapi2.models.Group
-import com.instructure.pandautils.utils.FEATURE_FLAG_OFFLINE
import com.instructure.pandautils.utils.FeatureFlagProvider
import com.instructure.pandautils.utils.NetworkStateProvider
import com.instructure.student.features.discussion.list.datasource.DiscussionListLocalDataSource
@@ -28,13 +27,11 @@ import com.instructure.student.features.discussion.list.datasource.DiscussionLis
import io.mockk.coEvery
import io.mockk.every
import io.mockk.mockk
-import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.Assert
import org.junit.Before
import org.junit.Test
-@ExperimentalCoroutinesApi
class DiscussionListRepositoryTest {
private val networkDataSource: DiscussionListNetworkDataSource = mockk(relaxed = true)
diff --git a/apps/student/src/test/java/com/instructure/student/features/discussion/list/datasource/DiscussionListLocalDataSourceTest.kt b/apps/student/src/test/java/com/instructure/student/features/discussion/list/datasource/DiscussionListLocalDataSourceTest.kt
index 61c6cdba9e..4dc8f2d632 100644
--- a/apps/student/src/test/java/com/instructure/student/features/discussion/list/datasource/DiscussionListLocalDataSourceTest.kt
+++ b/apps/student/src/test/java/com/instructure/student/features/discussion/list/datasource/DiscussionListLocalDataSourceTest.kt
@@ -22,12 +22,10 @@ import com.instructure.canvasapi2.models.Group
import com.instructure.pandautils.room.offline.facade.DiscussionTopicHeaderFacade
import io.mockk.coEvery
import io.mockk.mockk
-import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.Assert
import org.junit.Test
-@ExperimentalCoroutinesApi
class DiscussionListLocalDataSourceTest {
private val discussionTopicHeaderFacade: DiscussionTopicHeaderFacade = mockk(relaxed = true)
diff --git a/apps/student/src/test/java/com/instructure/student/features/discussion/list/datasource/DiscussionListNetworkDataSourceTest.kt b/apps/student/src/test/java/com/instructure/student/features/discussion/list/datasource/DiscussionListNetworkDataSourceTest.kt
index 8037c306b8..fa10953855 100644
--- a/apps/student/src/test/java/com/instructure/student/features/discussion/list/datasource/DiscussionListNetworkDataSourceTest.kt
+++ b/apps/student/src/test/java/com/instructure/student/features/discussion/list/datasource/DiscussionListNetworkDataSourceTest.kt
@@ -27,12 +27,10 @@ import com.instructure.canvasapi2.models.Group
import com.instructure.canvasapi2.utils.DataResult
import io.mockk.coEvery
import io.mockk.mockk
-import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.Assert
import org.junit.Test
-@ExperimentalCoroutinesApi
class DiscussionListNetworkDataSourceTest {
private val courseApi: CourseAPI.CoursesInterface = mockk(relaxed = true)
diff --git a/apps/student/src/test/java/com/instructure/student/features/discussion/routing/DiscussionRouteHelperStudentRepositoryTest.kt b/apps/student/src/test/java/com/instructure/student/features/discussion/routing/DiscussionRouteHelperStudentRepositoryTest.kt
index 5dc297d392..cd025c6a45 100644
--- a/apps/student/src/test/java/com/instructure/student/features/discussion/routing/DiscussionRouteHelperStudentRepositoryTest.kt
+++ b/apps/student/src/test/java/com/instructure/student/features/discussion/routing/DiscussionRouteHelperStudentRepositoryTest.kt
@@ -9,12 +9,10 @@ import com.instructure.pandautils.utils.NetworkStateProvider
import io.mockk.coEvery
import io.mockk.mockk
import junit.framework.TestCase.assertEquals
-import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
-@ExperimentalCoroutinesApi
class DiscussionRouteHelperStudentRepositoryTest {
private val networkDataSource: DiscussionRouteHelperNetworkDataSource = mockk(relaxed = true)
diff --git a/apps/student/src/test/java/com/instructure/student/features/elementary/course/ElementaryCourseViewModelTest.kt b/apps/student/src/test/java/com/instructure/student/features/elementary/course/ElementaryCourseViewModelTest.kt
index 84d70e0902..8456646677 100644
--- a/apps/student/src/test/java/com/instructure/student/features/elementary/course/ElementaryCourseViewModelTest.kt
+++ b/apps/student/src/test/java/com/instructure/student/features/elementary/course/ElementaryCourseViewModelTest.kt
@@ -14,13 +14,14 @@
* along with this program. If not, see .
*/
-package com.instructure.pandautils.features.elementary.course
+package com.instructure.student.features.elementary.course
import android.content.res.Resources
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LifecycleRegistry
+import com.google.firebase.crashlytics.FirebaseCrashlytics
import com.instructure.canvasapi2.managers.CourseManager
import com.instructure.canvasapi2.managers.OAuthManager
import com.instructure.canvasapi2.managers.TabManager
@@ -33,10 +34,6 @@ import com.instructure.pandautils.R
import com.instructure.pandautils.mvvm.ViewState
import com.instructure.pandautils.utils.ColorKeeper
import com.instructure.pandautils.utils.ThemedColor
-import com.instructure.student.features.elementary.course.ElementaryCourseAction
-import com.instructure.student.features.elementary.course.ElementaryCourseTab
-import com.instructure.student.features.elementary.course.ElementaryCourseViewData
-import com.instructure.student.features.elementary.course.ElementaryCourseViewModel
import io.mockk.coEvery
import io.mockk.every
import io.mockk.mockk
@@ -46,7 +43,7 @@ import io.mockk.unmockkAll
import junit.framework.Assert.assertEquals
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.test.TestCoroutineDispatcher
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.setMain
import org.junit.Before
import org.junit.Rule
@@ -61,13 +58,14 @@ class ElementaryCourseViewModelTest {
private val lifecycleOwner: LifecycleOwner = mockk(relaxed = true)
private val lifecycleRegistry = LifecycleRegistry(lifecycleOwner)
- private val testDispatcher = TestCoroutineDispatcher()
+ private val testDispatcher = UnconfinedTestDispatcher()
private val tabManager: TabManager = mockk(relaxed = true)
private val resources: Resources = mockk(relaxed = true)
private val apiPrefs: ApiPrefs = mockk(relaxed = true)
private val oauthManager: OAuthManager = mockk(relaxed = true)
private val courseManager: CourseManager = mockk(relaxed = true)
+ private val firebaseCrashlytics: FirebaseCrashlytics = mockk(relaxed = true)
private lateinit var viewModel: ElementaryCourseViewModel
@@ -89,7 +87,7 @@ class ElementaryCourseViewModelTest {
}
setupStrings()
- viewModel = ElementaryCourseViewModel(tabManager, resources, apiPrefs, oauthManager, courseManager)
+ viewModel = ElementaryCourseViewModel(tabManager, resources, apiPrefs, oauthManager, courseManager, firebaseCrashlytics)
mockkObject(ColorKeeper)
every { ColorKeeper.darkTheme } returns false
diff --git a/apps/student/src/test/java/com/instructure/student/features/file/details/FileDetailsLocalDataSourceTest.kt b/apps/student/src/test/java/com/instructure/student/features/file/details/FileDetailsLocalDataSourceTest.kt
index 792bc1d265..072527337e 100644
--- a/apps/student/src/test/java/com/instructure/student/features/file/details/FileDetailsLocalDataSourceTest.kt
+++ b/apps/student/src/test/java/com/instructure/student/features/file/details/FileDetailsLocalDataSourceTest.kt
@@ -13,14 +13,12 @@ import io.mockk.mockk
import io.mockk.mockkStatic
import io.mockk.unmockkAll
import junit.framework.TestCase.assertEquals
-import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.After
import org.junit.Before
import org.junit.Test
import java.util.Date
-@ExperimentalCoroutinesApi
class FileDetailsLocalDataSourceTest {
private val fileFolderDao: FileFolderDao = mockk(relaxed = true)
diff --git a/apps/student/src/test/java/com/instructure/student/features/file/details/FileDetailsNetworkDataSourceTest.kt b/apps/student/src/test/java/com/instructure/student/features/file/details/FileDetailsNetworkDataSourceTest.kt
index ce9cee3051..265a21500e 100644
--- a/apps/student/src/test/java/com/instructure/student/features/file/details/FileDetailsNetworkDataSourceTest.kt
+++ b/apps/student/src/test/java/com/instructure/student/features/file/details/FileDetailsNetworkDataSourceTest.kt
@@ -13,14 +13,12 @@ import io.mockk.mockk
import io.mockk.mockkStatic
import io.mockk.unmockkAll
import junit.framework.TestCase.assertEquals
-import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import okhttp3.ResponseBody.Companion.toResponseBody
import org.junit.After
import org.junit.Before
import org.junit.Test
-@ExperimentalCoroutinesApi
class FileDetailsNetworkDataSourceTest {
private val moduleApi: ModuleAPI.ModuleInterface = mockk(relaxed = true)
diff --git a/apps/student/src/test/java/com/instructure/student/features/file/details/FileDetailsRepositoryTest.kt b/apps/student/src/test/java/com/instructure/student/features/file/details/FileDetailsRepositoryTest.kt
index 517b2d509b..2f591d9e45 100644
--- a/apps/student/src/test/java/com/instructure/student/features/file/details/FileDetailsRepositoryTest.kt
+++ b/apps/student/src/test/java/com/instructure/student/features/file/details/FileDetailsRepositoryTest.kt
@@ -10,13 +10,11 @@ import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.mockk
import io.mockk.unmockkAll
-import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.After
import org.junit.Before
import org.junit.Test
-@ExperimentalCoroutinesApi
class FileDetailsRepositoryTest {
private val fileDetailsLocalDataSource: FileDetailsLocalDataSource = mockk(relaxed = true)
diff --git a/apps/student/src/test/java/com/instructure/student/features/file/list/FileListLocalDataSourceTest.kt b/apps/student/src/test/java/com/instructure/student/features/file/list/FileListLocalDataSourceTest.kt
index 7d6eeecf51..bad0dbd666 100644
--- a/apps/student/src/test/java/com/instructure/student/features/file/list/FileListLocalDataSourceTest.kt
+++ b/apps/student/src/test/java/com/instructure/student/features/file/list/FileListLocalDataSourceTest.kt
@@ -35,14 +35,12 @@ import io.mockk.mockkStatic
import io.mockk.unmockkAll
import junit.framework.TestCase.assertEquals
import junit.framework.TestCase.assertNull
-import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.After
import org.junit.Before
import org.junit.Test
import java.util.Date
-@ExperimentalCoroutinesApi
class FileListLocalDataSourceTest {
private val fileFolderDao: FileFolderDao = mockk(relaxed = true)
diff --git a/apps/student/src/test/java/com/instructure/student/features/file/list/FileListNetworkDataSourceTest.kt b/apps/student/src/test/java/com/instructure/student/features/file/list/FileListNetworkDataSourceTest.kt
index 575e20b7da..f942c83384 100644
--- a/apps/student/src/test/java/com/instructure/student/features/file/list/FileListNetworkDataSourceTest.kt
+++ b/apps/student/src/test/java/com/instructure/student/features/file/list/FileListNetworkDataSourceTest.kt
@@ -33,13 +33,11 @@ import io.mockk.mockkStatic
import io.mockk.unmockkAll
import junit.framework.TestCase.assertEquals
import junit.framework.TestCase.assertNull
-import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.After
import org.junit.Before
import org.junit.Test
-@ExperimentalCoroutinesApi
class FileListNetworkDataSourceTest {
private val fileFolderApi: FileFolderAPI.FilesFoldersInterface = mockk(relaxed = true)
diff --git a/apps/student/src/test/java/com/instructure/student/features/file/list/FileListRepositoryTest.kt b/apps/student/src/test/java/com/instructure/student/features/file/list/FileListRepositoryTest.kt
index 79b296608f..7c1e940ea6 100644
--- a/apps/student/src/test/java/com/instructure/student/features/file/list/FileListRepositoryTest.kt
+++ b/apps/student/src/test/java/com/instructure/student/features/file/list/FileListRepositoryTest.kt
@@ -31,12 +31,10 @@ import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.mockk
import junit.framework.TestCase.assertEquals
-import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
-@ExperimentalCoroutinesApi
class FileListRepositoryTest {
private val fileListLocalDataSource: FileListLocalDataSource = mockk(relaxed = true)
diff --git a/apps/student/src/test/java/com/instructure/student/features/file/search/FileSearchLocalDataSourceTest.kt b/apps/student/src/test/java/com/instructure/student/features/file/search/FileSearchLocalDataSourceTest.kt
index c9a46e177a..665cdbf6fe 100644
--- a/apps/student/src/test/java/com/instructure/student/features/file/search/FileSearchLocalDataSourceTest.kt
+++ b/apps/student/src/test/java/com/instructure/student/features/file/search/FileSearchLocalDataSourceTest.kt
@@ -31,14 +31,12 @@ import io.mockk.mockk
import io.mockk.mockkStatic
import io.mockk.unmockkAll
import junit.framework.TestCase
-import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.After
import org.junit.Before
import org.junit.Test
import java.util.Date
-@ExperimentalCoroutinesApi
class FileSearchLocalDataSourceTest {
private val fileFolderDao: FileFolderDao = mockk(relaxed = true)
diff --git a/apps/student/src/test/java/com/instructure/student/features/file/search/FileSearchNetworkDataSourceTest.kt b/apps/student/src/test/java/com/instructure/student/features/file/search/FileSearchNetworkDataSourceTest.kt
index a8e68b0870..f7fd674262 100644
--- a/apps/student/src/test/java/com/instructure/student/features/file/search/FileSearchNetworkDataSourceTest.kt
+++ b/apps/student/src/test/java/com/instructure/student/features/file/search/FileSearchNetworkDataSourceTest.kt
@@ -24,12 +24,10 @@ import com.instructure.student.features.files.search.FileSearchNetworkDataSource
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.mockk
-import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertEquals
import org.junit.Test
-@ExperimentalCoroutinesApi
class FileSearchNetworkDataSourceTest {
private val fileFolderApi: FileFolderAPI.FilesFoldersInterface = mockk(relaxed = true)
diff --git a/apps/student/src/test/java/com/instructure/student/features/file/search/FileSearchRepositoryTest.kt b/apps/student/src/test/java/com/instructure/student/features/file/search/FileSearchRepositoryTest.kt
index a6bed1cf59..0843670426 100644
--- a/apps/student/src/test/java/com/instructure/student/features/file/search/FileSearchRepositoryTest.kt
+++ b/apps/student/src/test/java/com/instructure/student/features/file/search/FileSearchRepositoryTest.kt
@@ -25,13 +25,11 @@ import com.instructure.student.features.files.search.FileSearchNetworkDataSource
import com.instructure.student.features.files.search.FileSearchRepository
import io.mockk.coEvery
import io.mockk.mockk
-import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
-@ExperimentalCoroutinesApi
class FileSearchRepositoryTest {
private val fileSearchLocalDataSource: FileSearchLocalDataSource = mockk(relaxed = true)
diff --git a/apps/student/src/test/java/com/instructure/student/features/grades/GradesListRepositoryTest.kt b/apps/student/src/test/java/com/instructure/student/features/grades/GradesListRepositoryTest.kt
index ca0ade55d9..f588fb7a53 100644
--- a/apps/student/src/test/java/com/instructure/student/features/grades/GradesListRepositoryTest.kt
+++ b/apps/student/src/test/java/com/instructure/student/features/grades/GradesListRepositoryTest.kt
@@ -17,8 +17,12 @@
package com.instructure.student.features.grades
-import com.instructure.canvasapi2.models.*
-import com.instructure.pandautils.utils.FEATURE_FLAG_OFFLINE
+import com.instructure.canvasapi2.models.Assignment
+import com.instructure.canvasapi2.models.AssignmentGroup
+import com.instructure.canvasapi2.models.Course
+import com.instructure.canvasapi2.models.Enrollment
+import com.instructure.canvasapi2.models.GradingPeriod
+import com.instructure.canvasapi2.models.Submission
import com.instructure.pandautils.utils.FeatureFlagProvider
import com.instructure.pandautils.utils.NetworkStateProvider
import com.instructure.student.features.grades.datasource.GradesListLocalDataSource
@@ -27,13 +31,11 @@ import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.every
import io.mockk.mockk
-import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Test
-@ExperimentalCoroutinesApi
class GradesListRepositoryTest {
private val networkDataSource: GradesListNetworkDataSource = mockk(relaxed = true)
@@ -138,6 +140,40 @@ class GradesListRepositoryTest {
assertEquals(offlineExpected, result)
}
+ @Test
+ fun `Get assignment groups with assignments for grading period if there are hidden assignments and device is online`() = runTest {
+ val onlineExpected = listOf(AssignmentGroup(id = 1, assignments = listOf(Assignment(1))), AssignmentGroup(id = 2, assignments = listOf(Assignment(3))))
+ val onlineResult = listOf(AssignmentGroup(id = 1, assignments = listOf(Assignment(1), Assignment(2, isHiddenInGradeBook = true))), AssignmentGroup(id = 2, assignments = listOf(Assignment(3), Assignment(4, isHiddenInGradeBook = true))))
+ val offlineExpected = listOf(AssignmentGroup(id = 3, assignments = listOf(Assignment(5))), AssignmentGroup(id = 4, assignments = listOf(Assignment(7))))
+ val offlineResult = listOf(AssignmentGroup(id = 3, assignments = listOf(Assignment(5), Assignment(6, isHiddenInGradeBook = true))), AssignmentGroup(id = 4, assignments = listOf(Assignment(7), Assignment(8, isHiddenInGradeBook = true))))
+
+ every { networkStateProvider.isOnline() } returns true
+ coEvery { networkDataSource.getAssignmentGroupsWithAssignmentsForGradingPeriod(any(), any(), any(), any()) } returns onlineResult
+ coEvery { localDataSource.getAssignmentGroupsWithAssignmentsForGradingPeriod(any(), any(), any(), any()) } returns offlineResult
+
+ val result = repository.getAssignmentGroupsWithAssignmentsForGradingPeriod(1, 1, scopeToStudent = true, forceNetwork = true)
+
+ coVerify { networkDataSource.getAssignmentGroupsWithAssignmentsForGradingPeriod(1, 1, scopeToStudent = true, forceNetwork = true) }
+ assertEquals(onlineExpected, result)
+ }
+
+ @Test
+ fun `Get assignment groups with assignments for grading period if there are hidden assignments and device is offline`() = runTest {
+ val onlineExpected = listOf(AssignmentGroup(id = 1, assignments = listOf(Assignment(1))), AssignmentGroup(id = 2, assignments = listOf(Assignment(3))))
+ val onlineResult = listOf(AssignmentGroup(id = 1, assignments = listOf(Assignment(1), Assignment(2, isHiddenInGradeBook = true))), AssignmentGroup(id = 2, assignments = listOf(Assignment(3), Assignment(4, isHiddenInGradeBook = true))))
+ val offlineExpected = listOf(AssignmentGroup(id = 3, assignments = listOf(Assignment(5))), AssignmentGroup(id = 4, assignments = listOf(Assignment(7))))
+ val offlineResult = listOf(AssignmentGroup(id = 3, assignments = listOf(Assignment(5), Assignment(6, isHiddenInGradeBook = true))), AssignmentGroup(id = 4, assignments = listOf(Assignment(7), Assignment(8, isHiddenInGradeBook = true))))
+
+ every { networkStateProvider.isOnline() } returns false
+ coEvery { networkDataSource.getAssignmentGroupsWithAssignmentsForGradingPeriod(any(), any(), any(), any()) } returns onlineResult
+ coEvery { localDataSource.getAssignmentGroupsWithAssignmentsForGradingPeriod(any(), any(), any(), any()) } returns offlineResult
+
+ val result = repository.getAssignmentGroupsWithAssignmentsForGradingPeriod(1, 1, scopeToStudent = true, forceNetwork = true)
+
+ coVerify { localDataSource.getAssignmentGroupsWithAssignmentsForGradingPeriod(1, 1, scopeToStudent = true, forceNetwork = true) }
+ assertEquals(offlineExpected, result)
+ }
+
@Test
fun `Get submissions for multiple assignments if device is online`() = runTest {
val onlineExpected = listOf(Submission(1))
@@ -287,4 +323,38 @@ class GradesListRepositoryTest {
coVerify { localDataSource.getAssignmentGroupsWithAssignments(1, true) }
assertEquals(offlineExpected, result)
}
+
+ @Test
+ fun `Get assignment groups with assignments if there are hidden assignments and device is online`() = runTest {
+ val onlineExpected = listOf(AssignmentGroup(id = 1, assignments = listOf(Assignment(1))), AssignmentGroup(id = 2, assignments = listOf(Assignment(3))))
+ val onlineResult = listOf(AssignmentGroup(id = 1, assignments = listOf(Assignment(1), Assignment(2, isHiddenInGradeBook = true))), AssignmentGroup(id = 2, assignments = listOf(Assignment(3), Assignment(4, isHiddenInGradeBook = true))))
+ val offlineExpected = listOf(AssignmentGroup(id = 3, assignments = listOf(Assignment(5))), AssignmentGroup(id = 4, assignments = listOf(Assignment(7))))
+ val offlineResult = listOf(AssignmentGroup(id = 3, assignments = listOf(Assignment(5), Assignment(6, isHiddenInGradeBook = true))), AssignmentGroup(id = 4, assignments = listOf(Assignment(7), Assignment(8, isHiddenInGradeBook = true))))
+
+ every { networkStateProvider.isOnline() } returns true
+ coEvery { networkDataSource.getAssignmentGroupsWithAssignments(any(), any()) } returns onlineResult
+ coEvery { localDataSource.getAssignmentGroupsWithAssignments(any(), any()) } returns offlineResult
+
+ val result = repository.getAssignmentGroupsWithAssignments(1, true)
+
+ coVerify { networkDataSource.getAssignmentGroupsWithAssignments(1, true) }
+ assertEquals(onlineExpected, result)
+ }
+
+ @Test
+ fun `Get assignment groups with assignments if there are hidden assignments and device is offline`() = runTest {
+ val onlineExpected = listOf(AssignmentGroup(id = 1, assignments = listOf(Assignment(1))), AssignmentGroup(id = 2, assignments = listOf(Assignment(3))))
+ val onlineResult = listOf(AssignmentGroup(id = 1, assignments = listOf(Assignment(1), Assignment(2, isHiddenInGradeBook = true))), AssignmentGroup(id = 2, assignments = listOf(Assignment(3), Assignment(4, isHiddenInGradeBook = true))))
+ val offlineExpected = listOf(AssignmentGroup(id = 3, assignments = listOf(Assignment(5))), AssignmentGroup(id = 4, assignments = listOf(Assignment(7))))
+ val offlineResult = listOf(AssignmentGroup(id = 3, assignments = listOf(Assignment(5), Assignment(6, isHiddenInGradeBook = true))), AssignmentGroup(id = 4, assignments = listOf(Assignment(7), Assignment(8, isHiddenInGradeBook = true))))
+
+ every { networkStateProvider.isOnline() } returns false
+ coEvery { networkDataSource.getAssignmentGroupsWithAssignments(any(), any()) } returns onlineResult
+ coEvery { localDataSource.getAssignmentGroupsWithAssignments(any(), any()) } returns offlineResult
+
+ val result = repository.getAssignmentGroupsWithAssignments(1, true)
+
+ coVerify { localDataSource.getAssignmentGroupsWithAssignments(1, true) }
+ assertEquals(offlineExpected, result)
+ }
}
diff --git a/apps/student/src/test/java/com/instructure/student/features/grades/datasource/GradesListLocalDataSourceTest.kt b/apps/student/src/test/java/com/instructure/student/features/grades/datasource/GradesListLocalDataSourceTest.kt
index d29afbc786..6ca480723b 100644
--- a/apps/student/src/test/java/com/instructure/student/features/grades/datasource/GradesListLocalDataSourceTest.kt
+++ b/apps/student/src/test/java/com/instructure/student/features/grades/datasource/GradesListLocalDataSourceTest.kt
@@ -24,12 +24,10 @@ import com.instructure.pandautils.room.offline.facade.EnrollmentFacade
import com.instructure.pandautils.room.offline.facade.SubmissionFacade
import io.mockk.coEvery
import io.mockk.mockk
-import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertEquals
import org.junit.Test
-@ExperimentalCoroutinesApi
class GradesListLocalDataSourceTest {
private val courseFacade: CourseFacade = mockk(relaxed = true)
diff --git a/apps/student/src/test/java/com/instructure/student/features/grades/datasource/GradesListNetworkDataSourceTest.kt b/apps/student/src/test/java/com/instructure/student/features/grades/datasource/GradesListNetworkDataSourceTest.kt
index b26fe9d0e9..147949a5cd 100644
--- a/apps/student/src/test/java/com/instructure/student/features/grades/datasource/GradesListNetworkDataSourceTest.kt
+++ b/apps/student/src/test/java/com/instructure/student/features/grades/datasource/GradesListNetworkDataSourceTest.kt
@@ -25,12 +25,10 @@ import com.instructure.canvasapi2.models.*
import com.instructure.canvasapi2.utils.DataResult
import io.mockk.coEvery
import io.mockk.mockk
-import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertEquals
import org.junit.Test
-@ExperimentalCoroutinesApi
class GradesListNetworkDataSourceTest {
private val courseApi: CourseAPI.CoursesInterface = mockk(relaxed = true)
diff --git a/apps/student/src/test/java/com/instructure/student/features/inbox/list/StudentInboxRepositoryTest.kt b/apps/student/src/test/java/com/instructure/student/features/inbox/list/StudentInboxRepositoryTest.kt
index ba50a0cf5a..37458eaaac 100644
--- a/apps/student/src/test/java/com/instructure/student/features/inbox/list/StudentInboxRepositoryTest.kt
+++ b/apps/student/src/test/java/com/instructure/student/features/inbox/list/StudentInboxRepositoryTest.kt
@@ -20,20 +20,16 @@ import com.instructure.canvasapi2.apis.EnrollmentAPI
import com.instructure.canvasapi2.apis.GroupAPI
import com.instructure.canvasapi2.apis.InboxApi
import com.instructure.canvasapi2.apis.ProgressAPI
-import com.instructure.canvasapi2.builders.RestParams
import com.instructure.canvasapi2.models.Course
import com.instructure.canvasapi2.models.Enrollment
import com.instructure.canvasapi2.models.Group
import com.instructure.canvasapi2.utils.DataResult
-import com.instructure.pandautils.features.inbox.list.InboxRepository
import io.mockk.coEvery
import io.mockk.mockk
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.test.runBlockingTest
-import org.junit.Assert.*
+import kotlinx.coroutines.test.runTest
+import org.junit.Assert.assertEquals
import org.junit.Test
-@ExperimentalCoroutinesApi
class StudentInboxRepositoryTest {
private val inboxApi: InboxApi.InboxInterface = mockk(relaxed = true)
@@ -45,7 +41,7 @@ class StudentInboxRepositoryTest {
StudentInboxRepository(inboxApi, coursesApi, groupsApi, progressApi)
@Test
- fun `Get contexts returns only valid courses`() = runBlockingTest {
+ fun `Get contexts returns only valid courses`() = runTest {
val courses = listOf(
Course(44, enrollments = mutableListOf(Enrollment(enrollmentState = EnrollmentAPI.STATE_ACTIVE))),
Course(11) // no active enrollment
diff --git a/apps/student/src/test/java/com/instructure/student/features/modules/list/ModuleListRepositoryTest.kt b/apps/student/src/test/java/com/instructure/student/features/modules/list/ModuleListRepositoryTest.kt
index 73bbcd4055..8a71e2e507 100644
--- a/apps/student/src/test/java/com/instructure/student/features/modules/list/ModuleListRepositoryTest.kt
+++ b/apps/student/src/test/java/com/instructure/student/features/modules/list/ModuleListRepositoryTest.kt
@@ -22,20 +22,17 @@ import com.instructure.canvasapi2.models.ModuleItem
import com.instructure.canvasapi2.models.ModuleObject
import com.instructure.canvasapi2.models.Tab
import com.instructure.canvasapi2.utils.DataResult
-import com.instructure.pandautils.utils.FEATURE_FLAG_OFFLINE
import com.instructure.pandautils.utils.FeatureFlagProvider
import com.instructure.pandautils.utils.NetworkStateProvider
import com.instructure.student.features.modules.list.datasource.ModuleListLocalDataSource
import com.instructure.student.features.modules.list.datasource.ModuleListNetworkDataSource
import io.mockk.coEvery
import io.mockk.mockk
-import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.Assert
import org.junit.Before
import org.junit.Test
-@ExperimentalCoroutinesApi
class ModuleListRepositoryTest {
private val networkDataSource: ModuleListNetworkDataSource = mockk(relaxed = true)
diff --git a/apps/student/src/test/java/com/instructure/student/features/modules/list/datasource/ModuleListLocalDataSourceTest.kt b/apps/student/src/test/java/com/instructure/student/features/modules/list/datasource/ModuleListLocalDataSourceTest.kt
index e27b27e40c..480116a8a3 100644
--- a/apps/student/src/test/java/com/instructure/student/features/modules/list/datasource/ModuleListLocalDataSourceTest.kt
+++ b/apps/student/src/test/java/com/instructure/student/features/modules/list/datasource/ModuleListLocalDataSourceTest.kt
@@ -30,13 +30,11 @@ import com.instructure.pandautils.room.offline.entities.TabEntity
import com.instructure.pandautils.room.offline.facade.ModuleFacade
import io.mockk.coEvery
import io.mockk.mockk
-import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.Assert
import org.junit.Assert.assertEquals
import org.junit.Test
-@ExperimentalCoroutinesApi
class ModuleListLocalDataSourceTest {
private val tabDao = mockk(relaxed = true)
diff --git a/apps/student/src/test/java/com/instructure/student/features/modules/list/datasource/ModuleListNetworkDataSourceTest.kt b/apps/student/src/test/java/com/instructure/student/features/modules/list/datasource/ModuleListNetworkDataSourceTest.kt
index 72254ad8ad..e1466f82d8 100644
--- a/apps/student/src/test/java/com/instructure/student/features/modules/list/datasource/ModuleListNetworkDataSourceTest.kt
+++ b/apps/student/src/test/java/com/instructure/student/features/modules/list/datasource/ModuleListNetworkDataSourceTest.kt
@@ -28,12 +28,10 @@ import com.instructure.canvasapi2.utils.DataResult
import com.instructure.canvasapi2.utils.LinkHeaders
import io.mockk.coEvery
import io.mockk.mockk
-import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.Assert
import org.junit.Test
-@ExperimentalCoroutinesApi
class ModuleListNetworkDataSourceTest {
private val moduleApi: ModuleAPI.ModuleInterface = mockk(relaxed = true)
diff --git a/apps/student/src/test/java/com/instructure/student/features/modules/progression/ModuleProgressionRepositoryTest.kt b/apps/student/src/test/java/com/instructure/student/features/modules/progression/ModuleProgressionRepositoryTest.kt
index ef6ef2324d..53204a7d53 100644
--- a/apps/student/src/test/java/com/instructure/student/features/modules/progression/ModuleProgressionRepositoryTest.kt
+++ b/apps/student/src/test/java/com/instructure/student/features/modules/progression/ModuleProgressionRepositoryTest.kt
@@ -25,10 +25,7 @@ import com.instructure.canvasapi2.utils.DataResult
import com.instructure.pandautils.room.offline.daos.CourseSyncSettingsDao
import com.instructure.pandautils.room.offline.daos.LocalFileDao
import com.instructure.pandautils.room.offline.entities.CourseSyncSettingsEntity
-import com.instructure.pandautils.room.offline.entities.FileSyncSettingsEntity
import com.instructure.pandautils.room.offline.entities.LocalFileEntity
-import com.instructure.pandautils.room.offline.model.CourseSyncSettingsWithFiles
-import com.instructure.pandautils.utils.FEATURE_FLAG_OFFLINE
import com.instructure.pandautils.utils.FeatureFlagProvider
import com.instructure.pandautils.utils.NetworkStateProvider
import com.instructure.student.features.modules.progression.datasource.ModuleProgressionLocalDataSource
@@ -36,14 +33,12 @@ import com.instructure.student.features.modules.progression.datasource.ModulePro
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.mockk
-import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.Assert
import org.junit.Before
import org.junit.Test
import java.util.Date
-@ExperimentalCoroutinesApi
class ModuleProgressionRepositoryTest {
private val localDataSource: ModuleProgressionLocalDataSource = mockk()
diff --git a/apps/student/src/test/java/com/instructure/student/features/modules/progression/datasource/ModuleProgressionLocalDataSourceTest.kt b/apps/student/src/test/java/com/instructure/student/features/modules/progression/datasource/ModuleProgressionLocalDataSourceTest.kt
index f9c090b82a..7d65474c31 100644
--- a/apps/student/src/test/java/com/instructure/student/features/modules/progression/datasource/ModuleProgressionLocalDataSourceTest.kt
+++ b/apps/student/src/test/java/com/instructure/student/features/modules/progression/datasource/ModuleProgressionLocalDataSourceTest.kt
@@ -27,12 +27,10 @@ import com.instructure.student.features.modules.progression.ModuleItemAsset
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.mockk
-import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.Assert
import org.junit.Test
-@ExperimentalCoroutinesApi
class ModuleProgressionLocalDataSourceTest {
private val moduleFacade: ModuleFacade = mockk(relaxed = true)
diff --git a/apps/student/src/test/java/com/instructure/student/features/modules/progression/datasource/ModuleProgressionNetworkDataSourceTest.kt b/apps/student/src/test/java/com/instructure/student/features/modules/progression/datasource/ModuleProgressionNetworkDataSourceTest.kt
index 6f957f7fd1..106af042d5 100644
--- a/apps/student/src/test/java/com/instructure/student/features/modules/progression/datasource/ModuleProgressionNetworkDataSourceTest.kt
+++ b/apps/student/src/test/java/com/instructure/student/features/modules/progression/datasource/ModuleProgressionNetworkDataSourceTest.kt
@@ -28,13 +28,11 @@ import com.instructure.canvasapi2.utils.LinkHeaders
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.mockk
-import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import okhttp3.ResponseBody
import org.junit.Assert
import org.junit.Test
-@ExperimentalCoroutinesApi
class ModuleProgressionNetworkDataSourceTest {
private val moduleApi: ModuleAPI.ModuleInterface = mockk()
diff --git a/apps/student/src/test/java/com/instructure/student/features/navigation/NavigationRepositoryTest.kt b/apps/student/src/test/java/com/instructure/student/features/navigation/NavigationRepositoryTest.kt
index 10163cc83a..445bdd028c 100644
--- a/apps/student/src/test/java/com/instructure/student/features/navigation/NavigationRepositoryTest.kt
+++ b/apps/student/src/test/java/com/instructure/student/features/navigation/NavigationRepositoryTest.kt
@@ -20,7 +20,6 @@ package com.instructure.student.features.navigation
import com.instructure.canvasapi2.models.Course
import com.instructure.canvasapi2.models.User
import com.instructure.canvasapi2.utils.DataResult
-import com.instructure.pandautils.utils.FEATURE_FLAG_OFFLINE
import com.instructure.pandautils.utils.FeatureFlagProvider
import com.instructure.pandautils.utils.NetworkStateProvider
import com.instructure.student.features.navigation.datasource.NavigationLocalDataSource
@@ -29,7 +28,6 @@ import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.every
import io.mockk.mockk
-import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
@@ -37,7 +35,6 @@ import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
-@ExperimentalCoroutinesApi
class NavigationRepositoryTest {
private val networkDataSource: NavigationNetworkDataSource = mockk(relaxed = true)
diff --git a/apps/student/src/test/java/com/instructure/student/features/navigation/datasource/GradesListLocalDataSourceTest.kt b/apps/student/src/test/java/com/instructure/student/features/navigation/datasource/GradesListLocalDataSourceTest.kt
index dca80d96cc..abcc370366 100644
--- a/apps/student/src/test/java/com/instructure/student/features/navigation/datasource/GradesListLocalDataSourceTest.kt
+++ b/apps/student/src/test/java/com/instructure/student/features/navigation/datasource/GradesListLocalDataSourceTest.kt
@@ -21,12 +21,10 @@ import com.instructure.canvasapi2.models.Course
import com.instructure.pandautils.room.offline.facade.CourseFacade
import io.mockk.coEvery
import io.mockk.mockk
-import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertEquals
import org.junit.Test
-@ExperimentalCoroutinesApi
class NavigationLocalDataSourceTest {
private val courseFacade: CourseFacade = mockk(relaxed = true)
diff --git a/apps/student/src/test/java/com/instructure/student/features/navigation/datasource/GradesListNetworkDataSourceTest.kt b/apps/student/src/test/java/com/instructure/student/features/navigation/datasource/GradesListNetworkDataSourceTest.kt
index 595d23c157..50de81ed63 100644
--- a/apps/student/src/test/java/com/instructure/student/features/navigation/datasource/GradesListNetworkDataSourceTest.kt
+++ b/apps/student/src/test/java/com/instructure/student/features/navigation/datasource/GradesListNetworkDataSourceTest.kt
@@ -24,13 +24,11 @@ import com.instructure.canvasapi2.models.User
import com.instructure.canvasapi2.utils.DataResult
import io.mockk.coEvery
import io.mockk.mockk
-import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
import org.junit.Test
-@ExperimentalCoroutinesApi
class NavigationNetworkDataSourceTest {
private val courseApi: CourseAPI.CoursesInterface = mockk(relaxed = true)
diff --git a/apps/student/src/test/java/com/instructure/student/features/offline/assignmentdetails/AssignmentDetailsLocalDataSourceTest.kt b/apps/student/src/test/java/com/instructure/student/features/offline/assignmentdetails/AssignmentDetailsLocalDataSourceTest.kt
index de5834686f..ed3ca72d4d 100644
--- a/apps/student/src/test/java/com/instructure/student/features/offline/assignmentdetails/AssignmentDetailsLocalDataSourceTest.kt
+++ b/apps/student/src/test/java/com/instructure/student/features/offline/assignmentdetails/AssignmentDetailsLocalDataSourceTest.kt
@@ -27,12 +27,10 @@ import com.instructure.pandautils.room.offline.facade.CourseFacade
import com.instructure.student.features.assignments.details.datasource.AssignmentDetailsLocalDataSource
import io.mockk.coEvery
import io.mockk.mockk
-import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.Assert
import org.junit.Test
-@ExperimentalCoroutinesApi
class AssignmentDetailsLocalDataSourceTest {
private val courseFacade: CourseFacade = mockk(relaxed = true)
diff --git a/apps/student/src/test/java/com/instructure/student/features/offline/assignmentdetails/AssignmentDetailsNetworkDataSourceTest.kt b/apps/student/src/test/java/com/instructure/student/features/offline/assignmentdetails/AssignmentDetailsNetworkDataSourceTest.kt
index 19c0fda613..0ccefcb73b 100644
--- a/apps/student/src/test/java/com/instructure/student/features/offline/assignmentdetails/AssignmentDetailsNetworkDataSourceTest.kt
+++ b/apps/student/src/test/java/com/instructure/student/features/offline/assignmentdetails/AssignmentDetailsNetworkDataSourceTest.kt
@@ -27,12 +27,10 @@ import com.instructure.canvasapi2.utils.Failure
import com.instructure.student.features.assignments.details.datasource.AssignmentDetailsNetworkDataSource
import io.mockk.coEvery
import io.mockk.mockk
-import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.Assert
import org.junit.Test
-@ExperimentalCoroutinesApi
class AssignmentDetailsNetworkDataSourceTest {
private val coursesInterface: CourseAPI.CoursesInterface = mockk(relaxed = true)
diff --git a/apps/student/src/test/java/com/instructure/student/features/offline/assignmentdetails/AssignmentDetailsRepositoryTest.kt b/apps/student/src/test/java/com/instructure/student/features/offline/assignmentdetails/AssignmentDetailsRepositoryTest.kt
index ef8d127a38..5e1d530b25 100644
--- a/apps/student/src/test/java/com/instructure/student/features/offline/assignmentdetails/AssignmentDetailsRepositoryTest.kt
+++ b/apps/student/src/test/java/com/instructure/student/features/offline/assignmentdetails/AssignmentDetailsRepositoryTest.kt
@@ -26,20 +26,18 @@ import com.instructure.pandautils.room.appdatabase.daos.ReminderDao
import com.instructure.pandautils.room.appdatabase.entities.ReminderEntity
import com.instructure.pandautils.utils.FeatureFlagProvider
import com.instructure.pandautils.utils.NetworkStateProvider
-import com.instructure.student.features.assignments.details.AssignmentDetailsRepository
+import com.instructure.student.features.assignments.details.StudentAssignmentDetailsRepository
import com.instructure.student.features.assignments.details.datasource.AssignmentDetailsLocalDataSource
import com.instructure.student.features.assignments.details.datasource.AssignmentDetailsNetworkDataSource
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.every
import io.mockk.mockk
-import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.Assert
import org.junit.Before
import org.junit.Test
-@ExperimentalCoroutinesApi
class AssignmentDetailsRepositoryTest {
private val networkDataSource: AssignmentDetailsNetworkDataSource = mockk(relaxed = true)
@@ -48,7 +46,7 @@ class AssignmentDetailsRepositoryTest {
private val featureFlagProvider: FeatureFlagProvider = mockk(relaxed = true)
private val reminderDao: ReminderDao = mockk(relaxed = true)
- private val repository = AssignmentDetailsRepository(localDataSource, networkDataSource, networkStateProvider, featureFlagProvider, reminderDao)
+ private val repository = StudentAssignmentDetailsRepository(localDataSource, networkDataSource, networkStateProvider, featureFlagProvider, reminderDao)
@Before
fun setup() = runTest {
diff --git a/apps/student/src/test/java/com/instructure/student/features/offline/coursebrowser/CourseBrowserLocalDataSourceTest.kt b/apps/student/src/test/java/com/instructure/student/features/offline/coursebrowser/CourseBrowserLocalDataSourceTest.kt
index 51022eaac0..d2b18887bf 100644
--- a/apps/student/src/test/java/com/instructure/student/features/offline/coursebrowser/CourseBrowserLocalDataSourceTest.kt
+++ b/apps/student/src/test/java/com/instructure/student/features/offline/coursebrowser/CourseBrowserLocalDataSourceTest.kt
@@ -29,12 +29,10 @@ import com.instructure.pandautils.room.offline.facade.PageFacade
import com.instructure.student.features.coursebrowser.datasource.CourseBrowserLocalDataSource
import io.mockk.coEvery
import io.mockk.mockk
-import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.Assert
import org.junit.Test
-@ExperimentalCoroutinesApi
class CourseBrowserLocalDataSourceTest {
private val tabDao: TabDao = mockk(relaxed = true)
diff --git a/apps/student/src/test/java/com/instructure/student/features/offline/coursebrowser/CourseBrowserNetworkDataSourceTest.kt b/apps/student/src/test/java/com/instructure/student/features/offline/coursebrowser/CourseBrowserNetworkDataSourceTest.kt
index 91423513ce..797b3194aa 100644
--- a/apps/student/src/test/java/com/instructure/student/features/offline/coursebrowser/CourseBrowserNetworkDataSourceTest.kt
+++ b/apps/student/src/test/java/com/instructure/student/features/offline/coursebrowser/CourseBrowserNetworkDataSourceTest.kt
@@ -25,12 +25,10 @@ import com.instructure.canvasapi2.utils.DataResult
import com.instructure.student.features.coursebrowser.datasource.CourseBrowserNetworkDataSource
import io.mockk.coEvery
import io.mockk.mockk
-import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.Assert
import org.junit.Test
-@ExperimentalCoroutinesApi
class CourseBrowserNetworkDataSourceTest {
private val tabApi: TabAPI.TabsInterface = mockk(relaxed = true)
diff --git a/apps/student/src/test/java/com/instructure/student/features/offline/coursebrowser/CourseBrowserRepositoryTest.kt b/apps/student/src/test/java/com/instructure/student/features/offline/coursebrowser/CourseBrowserRepositoryTest.kt
index d735c85a5d..89d1da01d3 100644
--- a/apps/student/src/test/java/com/instructure/student/features/offline/coursebrowser/CourseBrowserRepositoryTest.kt
+++ b/apps/student/src/test/java/com/instructure/student/features/offline/coursebrowser/CourseBrowserRepositoryTest.kt
@@ -19,7 +19,6 @@ package com.instructure.student.features.offline.coursebrowser
import com.instructure.canvasapi2.models.CanvasContext
import com.instructure.canvasapi2.models.Page
import com.instructure.canvasapi2.models.Tab
-import com.instructure.pandautils.utils.FEATURE_FLAG_OFFLINE
import com.instructure.pandautils.utils.FeatureFlagProvider
import com.instructure.pandautils.utils.NetworkStateProvider
import com.instructure.student.features.coursebrowser.CourseBrowserRepository
@@ -29,13 +28,11 @@ import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.every
import io.mockk.mockk
-import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.Assert
import org.junit.Before
import org.junit.Test
-@ExperimentalCoroutinesApi
class CourseBrowserRepositoryTest {
private val networkDataSource: CourseBrowserNetworkDataSource = mockk(relaxed = true)
diff --git a/apps/student/src/test/java/com/instructure/student/features/pages/details/PageDetailsLocalDataSourceTest.kt b/apps/student/src/test/java/com/instructure/student/features/pages/details/PageDetailsLocalDataSourceTest.kt
index 0c0f7a5f73..b6fe60a296 100644
--- a/apps/student/src/test/java/com/instructure/student/features/pages/details/PageDetailsLocalDataSourceTest.kt
+++ b/apps/student/src/test/java/com/instructure/student/features/pages/details/PageDetailsLocalDataSourceTest.kt
@@ -26,12 +26,10 @@ import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.mockk
import junit.framework.TestCase
-import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
-@ExperimentalCoroutinesApi
class PageDetailsLocalDataSourceTest {
private val pageFacade: PageFacade = mockk(relaxed = true)
diff --git a/apps/student/src/test/java/com/instructure/student/features/pages/details/PageDetailsNetworkDataSourceTest.kt b/apps/student/src/test/java/com/instructure/student/features/pages/details/PageDetailsNetworkDataSourceTest.kt
index fe72d85e46..cf0d1d6820 100644
--- a/apps/student/src/test/java/com/instructure/student/features/pages/details/PageDetailsNetworkDataSourceTest.kt
+++ b/apps/student/src/test/java/com/instructure/student/features/pages/details/PageDetailsNetworkDataSourceTest.kt
@@ -27,12 +27,10 @@ import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.mockk
import junit.framework.TestCase
-import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
-@ExperimentalCoroutinesApi
class PageDetailsNetworkDataSourceTest {
private val pageApi: PageAPI.PagesInterface = mockk(relaxed = true)
diff --git a/apps/student/src/test/java/com/instructure/student/features/pages/details/PageDetailsRepositoryTest.kt b/apps/student/src/test/java/com/instructure/student/features/pages/details/PageDetailsRepositoryTest.kt
index c185fddebf..899ac9c29b 100644
--- a/apps/student/src/test/java/com/instructure/student/features/pages/details/PageDetailsRepositoryTest.kt
+++ b/apps/student/src/test/java/com/instructure/student/features/pages/details/PageDetailsRepositoryTest.kt
@@ -20,7 +20,6 @@ package com.instructure.student.features.pages.details
import com.instructure.canvasapi2.models.CanvasContext
import com.instructure.canvasapi2.models.Page
import com.instructure.canvasapi2.utils.DataResult
-import com.instructure.pandautils.utils.FEATURE_FLAG_OFFLINE
import com.instructure.pandautils.utils.FeatureFlagProvider
import com.instructure.pandautils.utils.NetworkStateProvider
import com.instructure.student.features.pages.details.datasource.PageDetailsLocalDataSource
@@ -29,12 +28,10 @@ import io.mockk.coEvery
import io.mockk.every
import io.mockk.mockk
import junit.framework.TestCase
-import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
-@ExperimentalCoroutinesApi
class PageDetailsRepositoryTest {
private val networkDataSource: PageDetailsNetworkDataSource = mockk(relaxed = true)
diff --git a/apps/student/src/test/java/com/instructure/student/features/pages/list/PageListLocalDataSourceTest.kt b/apps/student/src/test/java/com/instructure/student/features/pages/list/PageListLocalDataSourceTest.kt
index 1f8a8e8ac8..4a8c6d63e8 100644
--- a/apps/student/src/test/java/com/instructure/student/features/pages/list/PageListLocalDataSourceTest.kt
+++ b/apps/student/src/test/java/com/instructure/student/features/pages/list/PageListLocalDataSourceTest.kt
@@ -7,11 +7,9 @@ import com.instructure.student.features.pages.list.datasource.PageListLocalDataS
import io.mockk.coEvery
import io.mockk.mockk
import junit.framework.TestCase.assertEquals
-import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.Test
-@ExperimentalCoroutinesApi
class PageListLocalDataSourceTest {
private val pageFacade: PageFacade = mockk(relaxed = true)
diff --git a/apps/student/src/test/java/com/instructure/student/features/pages/list/PageListNetworkDataSourceTest.kt b/apps/student/src/test/java/com/instructure/student/features/pages/list/PageListNetworkDataSourceTest.kt
index ad59542df3..1ed11f9374 100644
--- a/apps/student/src/test/java/com/instructure/student/features/pages/list/PageListNetworkDataSourceTest.kt
+++ b/apps/student/src/test/java/com/instructure/student/features/pages/list/PageListNetworkDataSourceTest.kt
@@ -8,11 +8,9 @@ import com.instructure.student.features.pages.list.datasource.PageListNetworkDat
import io.mockk.coEvery
import io.mockk.mockk
import junit.framework.TestCase.assertEquals
-import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.Test
-@ExperimentalCoroutinesApi
class PageListNetworkDataSourceTest {
private val pageApi: PageAPI.PagesInterface = mockk(relaxed = true)
diff --git a/apps/student/src/test/java/com/instructure/student/features/pages/list/PageListRepositoryTest.kt b/apps/student/src/test/java/com/instructure/student/features/pages/list/PageListRepositoryTest.kt
index 7639a70848..e746a969ca 100644
--- a/apps/student/src/test/java/com/instructure/student/features/pages/list/PageListRepositoryTest.kt
+++ b/apps/student/src/test/java/com/instructure/student/features/pages/list/PageListRepositoryTest.kt
@@ -2,7 +2,6 @@ package com.instructure.student.features.pages.list
import com.instructure.canvasapi2.models.CanvasContext
import com.instructure.canvasapi2.models.Page
-import com.instructure.pandautils.utils.FEATURE_FLAG_OFFLINE
import com.instructure.pandautils.utils.FeatureFlagProvider
import com.instructure.pandautils.utils.NetworkStateProvider
import com.instructure.student.features.pages.list.datasource.PageListLocalDataSource
@@ -11,12 +10,10 @@ import io.mockk.coEvery
import io.mockk.every
import io.mockk.mockk
import junit.framework.TestCase.assertEquals
-import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
-@ExperimentalCoroutinesApi
class PageListRepositoryTest {
private val networkDataSource: PageListNetworkDataSource = mockk(relaxed = true)
diff --git a/apps/student/src/test/java/com/instructure/student/features/people/details/PeopleDetailsRepositoryTest.kt b/apps/student/src/test/java/com/instructure/student/features/people/details/PeopleDetailsRepositoryTest.kt
index 3084926b6d..d01b0fb2d0 100644
--- a/apps/student/src/test/java/com/instructure/student/features/people/details/PeopleDetailsRepositoryTest.kt
+++ b/apps/student/src/test/java/com/instructure/student/features/people/details/PeopleDetailsRepositoryTest.kt
@@ -2,19 +2,16 @@ package com.instructure.student.features.people.details
import com.instructure.canvasapi2.models.CanvasContext
import com.instructure.canvasapi2.models.User
-import com.instructure.pandautils.utils.FEATURE_FLAG_OFFLINE
import com.instructure.pandautils.utils.FeatureFlagProvider
import com.instructure.pandautils.utils.NetworkStateProvider
import io.mockk.coEvery
import io.mockk.every
import io.mockk.mockk
import junit.framework.TestCase
-import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
-@ExperimentalCoroutinesApi
class PeopleDetailsRepositoryTest {
private val networkDataSource: PeopleDetailsNetworkDataSource = mockk(relaxed = true)
private val localDataSource: PeopleDetailsLocalDataSource = mockk(relaxed = true)
diff --git a/apps/student/src/test/java/com/instructure/student/features/people/details/datasource/PeopleDetailsLocalDataSourceTest.kt b/apps/student/src/test/java/com/instructure/student/features/people/details/datasource/PeopleDetailsLocalDataSourceTest.kt
index bb886804ca..3819b5b10c 100644
--- a/apps/student/src/test/java/com/instructure/student/features/people/details/datasource/PeopleDetailsLocalDataSourceTest.kt
+++ b/apps/student/src/test/java/com/instructure/student/features/people/details/datasource/PeopleDetailsLocalDataSourceTest.kt
@@ -8,11 +8,9 @@ import com.instructure.student.features.people.details.PeopleDetailsLocalDataSou
import io.mockk.coEvery
import io.mockk.mockk
import junit.framework.TestCase
-import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.Test
-@ExperimentalCoroutinesApi
class PeopleDetailsLocalDataSourceTest {
private val userFacade: UserFacade = mockk(relaxed = true)
private val dataSource = PeopleDetailsLocalDataSource(userFacade)
diff --git a/apps/student/src/test/java/com/instructure/student/features/people/list/PeopleListRepositoryTest.kt b/apps/student/src/test/java/com/instructure/student/features/people/list/PeopleListRepositoryTest.kt
index 194221b9c8..a4cecae0b9 100644
--- a/apps/student/src/test/java/com/instructure/student/features/people/list/PeopleListRepositoryTest.kt
+++ b/apps/student/src/test/java/com/instructure/student/features/people/list/PeopleListRepositoryTest.kt
@@ -3,19 +3,16 @@ package com.instructure.student.features.people.list
import com.instructure.canvasapi2.models.CanvasContext
import com.instructure.canvasapi2.models.User
import com.instructure.canvasapi2.utils.DataResult
-import com.instructure.pandautils.utils.FEATURE_FLAG_OFFLINE
import com.instructure.pandautils.utils.FeatureFlagProvider
import com.instructure.pandautils.utils.NetworkStateProvider
import io.mockk.coEvery
import io.mockk.every
import io.mockk.mockk
import junit.framework.TestCase.assertEquals
-import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
-@ExperimentalCoroutinesApi
class PeopleListRepositoryTest {
private val networkDataSource: PeopleListNetworkDataSource = mockk(relaxed = true)
private val localDataSource: PeopleListLocalDataSource = mockk(relaxed = true)
diff --git a/apps/student/src/test/java/com/instructure/student/features/people/list/datasource/PeopleListLocalDataSourceTest.kt b/apps/student/src/test/java/com/instructure/student/features/people/list/datasource/PeopleListLocalDataSourceTest.kt
index d06e2b5b09..4b1ef0d0b7 100644
--- a/apps/student/src/test/java/com/instructure/student/features/people/list/datasource/PeopleListLocalDataSourceTest.kt
+++ b/apps/student/src/test/java/com/instructure/student/features/people/list/datasource/PeopleListLocalDataSourceTest.kt
@@ -8,11 +8,9 @@ import com.instructure.student.features.people.list.PeopleListLocalDataSource
import io.mockk.coEvery
import io.mockk.mockk
import junit.framework.TestCase.assertEquals
-import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.Test
-@ExperimentalCoroutinesApi
class PeopleListLocalDataSourceTest {
private val userFacade: UserFacade = mockk(relaxed = true)
private val dataSource = PeopleListLocalDataSource(userFacade)
diff --git a/apps/student/src/test/java/com/instructure/student/features/people/list/datasource/PeopleListNetworkDataSourceTest.kt b/apps/student/src/test/java/com/instructure/student/features/people/list/datasource/PeopleListNetworkDataSourceTest.kt
index 6cdaa8ecf5..6db8d0aa1c 100644
--- a/apps/student/src/test/java/com/instructure/student/features/people/list/datasource/PeopleListNetworkDataSourceTest.kt
+++ b/apps/student/src/test/java/com/instructure/student/features/people/list/datasource/PeopleListNetworkDataSourceTest.kt
@@ -9,11 +9,9 @@ import com.instructure.student.features.people.list.PeopleListNetworkDataSource
import io.mockk.coEvery
import io.mockk.mockk
import junit.framework.TestCase.assertEquals
-import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.Test
-@ExperimentalCoroutinesApi
class PeopleListNetworkDataSourceTest {
private val userAPI: UserAPI.UsersInterface = mockk(relaxed = true)
private val dataSource = PeopleListNetworkDataSource(userAPI)
diff --git a/apps/student/src/test/java/com/instructure/student/features/quiz/list/QuizListRepositoryTest.kt b/apps/student/src/test/java/com/instructure/student/features/quiz/list/QuizListRepositoryTest.kt
index 82535495db..243fc4a78c 100644
--- a/apps/student/src/test/java/com/instructure/student/features/quiz/list/QuizListRepositoryTest.kt
+++ b/apps/student/src/test/java/com/instructure/student/features/quiz/list/QuizListRepositoryTest.kt
@@ -18,20 +18,17 @@ package com.instructure.student.features.quiz.list
import com.instructure.canvasapi2.models.CourseSettings
import com.instructure.canvasapi2.models.Quiz
-import com.instructure.pandautils.utils.FEATURE_FLAG_OFFLINE
import com.instructure.pandautils.utils.FeatureFlagProvider
import com.instructure.pandautils.utils.NetworkStateProvider
import io.mockk.coEvery
import io.mockk.every
import io.mockk.mockk
import junit.framework.TestCase.assertEquals
-import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.Assert
import org.junit.Before
import org.junit.Test
-@ExperimentalCoroutinesApi
class QuizListRepositoryTest {
private val networkDataSource: QuizListNetworkDataSource = mockk(relaxed = true)
diff --git a/apps/student/src/test/java/com/instructure/student/features/quiz/list/datasource/QuizListLocalDataSourceTest.kt b/apps/student/src/test/java/com/instructure/student/features/quiz/list/datasource/QuizListLocalDataSourceTest.kt
index c737dd80dc..f12acea9c3 100644
--- a/apps/student/src/test/java/com/instructure/student/features/quiz/list/datasource/QuizListLocalDataSourceTest.kt
+++ b/apps/student/src/test/java/com/instructure/student/features/quiz/list/datasource/QuizListLocalDataSourceTest.kt
@@ -26,11 +26,9 @@ import com.instructure.student.features.quiz.list.QuizListLocalDataSource
import io.mockk.coEvery
import io.mockk.mockk
import junit.framework.TestCase.assertEquals
-import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.Test
-@ExperimentalCoroutinesApi
class QuizListLocalDataSourceTest {
private val quizDao: QuizDao = mockk(relaxed = true)
private val courseSettingsDao: CourseSettingsDao = mockk(relaxed = true)
diff --git a/apps/student/src/test/java/com/instructure/student/features/quiz/list/datasource/QuizListNetworkDataSourceTest.kt b/apps/student/src/test/java/com/instructure/student/features/quiz/list/datasource/QuizListNetworkDataSourceTest.kt
index ece2def15f..65169cbd76 100644
--- a/apps/student/src/test/java/com/instructure/student/features/quiz/list/datasource/QuizListNetworkDataSourceTest.kt
+++ b/apps/student/src/test/java/com/instructure/student/features/quiz/list/datasource/QuizListNetworkDataSourceTest.kt
@@ -25,12 +25,10 @@ import com.instructure.student.features.quiz.list.QuizListNetworkDataSource
import io.mockk.coEvery
import io.mockk.mockk
import junit.framework.TestCase.assertEquals
-import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.Assert
import org.junit.Test
-@ExperimentalCoroutinesApi
class QuizListNetworkDataSourceTest {
private val quizApi: QuizAPI.QuizInterface = mockk(relaxed = true)
private val courseApi: CourseAPI.CoursesInterface = mockk(relaxed = true)
diff --git a/apps/student/src/test/java/com/instructure/student/mobius/assignmentDetails/submission/annnotation/AnnotationSubmissionViewModelTest.kt b/apps/student/src/test/java/com/instructure/student/mobius/assignmentDetails/submission/annnotation/AnnotationSubmissionViewModelTest.kt
index 21cdc3f5f8..8a623c2218 100644
--- a/apps/student/src/test/java/com/instructure/student/mobius/assignmentDetails/submission/annnotation/AnnotationSubmissionViewModelTest.kt
+++ b/apps/student/src/test/java/com/instructure/student/mobius/assignmentDetails/submission/annnotation/AnnotationSubmissionViewModelTest.kt
@@ -31,11 +31,15 @@ import io.mockk.every
import io.mockk.mockk
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.test.TestCoroutineDispatcher
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.resetMain
import kotlinx.coroutines.test.setMain
-import org.junit.*
-import org.junit.Assert.*
+import org.junit.After
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNull
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
@ExperimentalCoroutinesApi
class AnnotationSubmissionViewModelTest {
@@ -51,7 +55,7 @@ class AnnotationSubmissionViewModelTest {
private val canvaDocsManager: CanvaDocsManager = mockk(relaxed = true)
private val resources: Resources = mockk(relaxed = true)
- private val testDispatcher = TestCoroutineDispatcher()
+ private val testDispatcher = UnconfinedTestDispatcher()
@Before
fun setUp() {
@@ -65,7 +69,6 @@ class AnnotationSubmissionViewModelTest {
@After
fun tearDown() {
Dispatchers.resetMain()
- testDispatcher.cleanupTestCoroutines()
}
@Test
diff --git a/apps/student/src/test/java/com/instructure/student/test/assignment/details/GradeCellStateTest.kt b/apps/student/src/test/java/com/instructure/student/test/assignment/details/GradeCellStateTest.kt
index addfb8b0c6..9a0491f31e 100644
--- a/apps/student/src/test/java/com/instructure/student/test/assignment/details/GradeCellStateTest.kt
+++ b/apps/student/src/test/java/com/instructure/student/test/assignment/details/GradeCellStateTest.kt
@@ -25,13 +25,16 @@ import com.instructure.canvasapi2.models.AssignmentScoreStatistics
import com.instructure.canvasapi2.models.CanvasContext
import com.instructure.canvasapi2.models.Submission
import com.instructure.canvasapi2.utils.DateHelper
+import com.instructure.pandautils.features.assignments.details.mobius.gradeCell.GradeCellViewState
import com.instructure.pandautils.utils.ColorKeeper
import com.instructure.pandautils.utils.ThemedColor
-import com.instructure.student.mobius.assignmentDetails.ui.gradeCell.GradeCellViewState
import io.mockk.every
import io.mockk.mockkObject
import io.mockk.unmockkAll
-import org.junit.*
+import org.junit.After
+import org.junit.Assert
+import org.junit.Before
+import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
diff --git a/apps/student/src/test/java/com/instructure/student/test/assignment/details/submission/TextSubmissionUploadUpdateTest.kt b/apps/student/src/test/java/com/instructure/student/test/assignment/details/submission/TextSubmissionUploadUpdateTest.kt
index ecaa5ee508..be9b0ba39b 100644
--- a/apps/student/src/test/java/com/instructure/student/test/assignment/details/submission/TextSubmissionUploadUpdateTest.kt
+++ b/apps/student/src/test/java/com/instructure/student/test/assignment/details/submission/TextSubmissionUploadUpdateTest.kt
@@ -31,14 +31,10 @@ import com.spotify.mobius.test.InitSpec.assertThatFirst
import com.spotify.mobius.test.NextMatchers
import com.spotify.mobius.test.UpdateSpec
import com.spotify.mobius.test.UpdateSpec.assertThatNext
-import io.mockk.every
import io.mockk.mockk
-import io.mockk.mockkStatic
import org.junit.Assert
import org.junit.Before
import org.junit.Test
-import java.io.UnsupportedEncodingException
-import java.net.URLEncoder
class TextSubmissionUploadUpdateTest : Assert() {
private val initSpec = InitSpec(TextSubmissionUploadUpdate()::init)
diff --git a/apps/student/src/test/java/com/instructure/student/test/assignment/details/submission/UploadStatusSubmissionPresenterTest.kt b/apps/student/src/test/java/com/instructure/student/test/assignment/details/submission/UploadStatusSubmissionPresenterTest.kt
index 347fb6837a..b15b449e27 100644
--- a/apps/student/src/test/java/com/instructure/student/test/assignment/details/submission/UploadStatusSubmissionPresenterTest.kt
+++ b/apps/student/src/test/java/com/instructure/student/test/assignment/details/submission/UploadStatusSubmissionPresenterTest.kt
@@ -27,9 +27,6 @@ import com.instructure.student.mobius.assignmentDetails.submission.file.ui.Uploa
import com.instructure.student.mobius.assignmentDetails.submission.file.ui.UploadStatusSubmissionViewState
import com.instructure.student.mobius.assignmentDetails.submission.file.ui.UploadVisibilities
import com.instructure.student.room.entities.CreateFileSubmissionEntity
-import com.instructure.student.util.FileUtils
-import io.mockk.every
-import io.mockk.mockkObject
import org.junit.Assert
import org.junit.Before
import org.junit.Test
diff --git a/apps/student/src/test/java/com/instructure/student/test/assignment/details/submissionDetails/SubmissionDetailsEffectHandlerTest.kt b/apps/student/src/test/java/com/instructure/student/test/assignment/details/submissionDetails/SubmissionDetailsEffectHandlerTest.kt
index 25667582e4..d7642fc82b 100644
--- a/apps/student/src/test/java/com/instructure/student/test/assignment/details/submissionDetails/SubmissionDetailsEffectHandlerTest.kt
+++ b/apps/student/src/test/java/com/instructure/student/test/assignment/details/submissionDetails/SubmissionDetailsEffectHandlerTest.kt
@@ -18,38 +18,58 @@ package com.instructure.student.test.assignment.details.submissionDetails
import com.instructure.canvasapi2.managers.CourseManager
import com.instructure.canvasapi2.managers.ExternalToolManager
import com.instructure.canvasapi2.managers.FeaturesManager
-import com.instructure.canvasapi2.models.*
+import com.instructure.canvasapi2.models.Assignment
+import com.instructure.canvasapi2.models.CourseSettings
+import com.instructure.canvasapi2.models.Enrollment
+import com.instructure.canvasapi2.models.ExternalToolAttributes
+import com.instructure.canvasapi2.models.LTITool
+import com.instructure.canvasapi2.models.Quiz
+import com.instructure.canvasapi2.models.Submission
+import com.instructure.canvasapi2.models.User
import com.instructure.canvasapi2.utils.ApiPrefs
import com.instructure.canvasapi2.utils.DataResult
import com.instructure.canvasapi2.utils.Failure
-import com.instructure.student.mobius.assignmentDetails.submissionDetails.*
+import com.instructure.student.mobius.assignmentDetails.submissionDetails.SubmissionDetailsContentType
+import com.instructure.student.mobius.assignmentDetails.submissionDetails.SubmissionDetailsEffect
+import com.instructure.student.mobius.assignmentDetails.submissionDetails.SubmissionDetailsEffectHandler
+import com.instructure.student.mobius.assignmentDetails.submissionDetails.SubmissionDetailsEvent
+import com.instructure.student.mobius.assignmentDetails.submissionDetails.SubmissionDetailsRepository
import com.instructure.student.mobius.assignmentDetails.submissionDetails.drawer.comments.SubmissionCommentsSharedEvent
import com.instructure.student.mobius.assignmentDetails.submissionDetails.ui.SubmissionDetailsView
-import com.instructure.student.mobius.common.ChannelSource
-import com.instructure.student.test.util.receiveOnce
+import com.instructure.student.mobius.common.FlowSource
import com.spotify.mobius.functions.Consumer
-import io.mockk.*
+import io.mockk.coEvery
+import io.mockk.confirmVerified
+import io.mockk.every
+import io.mockk.mockk
+import io.mockk.mockkObject
+import io.mockk.verify
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.asCoroutineDispatcher
+import kotlinx.coroutines.async
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.resetMain
+import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.test.setMain
+import org.junit.After
import org.junit.Assert
import org.junit.Before
import org.junit.Test
import java.io.File
-import java.util.concurrent.Executors
+@OptIn(ExperimentalCoroutinesApi::class)
class SubmissionDetailsEffectHandlerTest : Assert() {
private val view: SubmissionDetailsView = mockk(relaxed = true)
private val repository: SubmissionDetailsRepository = mockk(relaxed = true)
private val effectHandler = SubmissionDetailsEffectHandler(repository).apply { view = this@SubmissionDetailsEffectHandlerTest.view }
private val eventConsumer: Consumer = mockk(relaxed = true)
private val connection = effectHandler.connect(eventConsumer)
-
- @ExperimentalCoroutinesApi
+ private val testDispatcher = UnconfinedTestDispatcher()
+
@Before
fun setup() {
- Dispatchers.setMain(Executors.newSingleThreadExecutor().asCoroutineDispatcher())
+ Dispatchers.setMain(testDispatcher)
mockkObject(FeaturesManager)
mockkObject(CourseManager)
every { FeaturesManager.getEnabledFeaturesForCourseAsync(any(), any()) } returns mockk {
@@ -59,6 +79,11 @@ class SubmissionDetailsEffectHandlerTest : Assert() {
coEvery { await() } returns DataResult.Success(CourseSettings())
}
}
+
+ @After
+ fun tearDown() {
+ Dispatchers.resetMain()
+ }
@Test
fun `Failed loadData results in fail DataLoaded`() {
@@ -439,26 +464,32 @@ class SubmissionDetailsEffectHandlerTest : Assert() {
}
@Test
- fun `UploadMediaComment results in SendMediaCommentClicked shared event`() {
+ fun `UploadMediaComment results in SendMediaCommentClicked shared event`() = runTest(testDispatcher) {
val file = File("test")
- val channel = ChannelSource.getChannel()
+ val flow = FlowSource.getFlow()
val expectedEvent = SubmissionCommentsSharedEvent.SendMediaCommentClicked(file)
- val actualEvent = channel.receiveOnce {
- connection.accept(SubmissionDetailsEffect.UploadMediaComment(file))
+
+ val deferred = async {
+ flow.first()
}
- assertEquals(expectedEvent, actualEvent)
+ connection.accept(SubmissionDetailsEffect.UploadMediaComment(file))
+
+ assertEquals(expectedEvent, deferred.await())
}
@Test
- fun `MediaCommentDialogClosed results in MediaCommentDialogClosed shared event`() {
- val channel = ChannelSource.getChannel()
+ fun `MediaCommentDialogClosed results in MediaCommentDialogClosed shared event`() = runTest(testDispatcher) {
+ val flow = FlowSource.getFlow()
val expectedEvent = SubmissionCommentsSharedEvent.MediaCommentDialogClosed
- val actualEvent = channel.receiveOnce {
- connection.accept(SubmissionDetailsEffect.MediaCommentDialogClosed)
+
+ val deferred = async {
+ flow.first()
}
- assertEquals(expectedEvent, actualEvent)
+ connection.accept(SubmissionDetailsEffect.MediaCommentDialogClosed)
+
+ assertEquals(expectedEvent, deferred.await())
}
}
diff --git a/apps/student/src/test/java/com/instructure/student/test/assignment/details/submissionDetails/SubmissionDetailsEmptyContentEffectHandlerTest.kt b/apps/student/src/test/java/com/instructure/student/test/assignment/details/submissionDetails/SubmissionDetailsEmptyContentEffectHandlerTest.kt
index 02fc3b61fa..5946823550 100644
--- a/apps/student/src/test/java/com/instructure/student/test/assignment/details/submissionDetails/SubmissionDetailsEmptyContentEffectHandlerTest.kt
+++ b/apps/student/src/test/java/com/instructure/student/test/assignment/details/submissionDetails/SubmissionDetailsEmptyContentEffectHandlerTest.kt
@@ -15,14 +15,18 @@
*/
package com.instructure.student.test.assignment.details.submissionDetails
-import android.app.Activity
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.provider.MediaStore
import androidx.core.content.FileProvider
import androidx.fragment.app.FragmentActivity
-import com.instructure.canvasapi2.models.*
+import com.instructure.canvasapi2.models.Assignment
+import com.instructure.canvasapi2.models.Course
+import com.instructure.canvasapi2.models.DiscussionTopicHeader
+import com.instructure.canvasapi2.models.LTITool
+import com.instructure.canvasapi2.models.Quiz
+import com.instructure.canvasapi2.models.User
import com.instructure.canvasapi2.utils.ApiPrefs
import com.instructure.pandautils.utils.FilePrefs
import com.instructure.pandautils.utils.FileUploadUtils
@@ -41,7 +45,15 @@ import com.instructure.student.mobius.common.ui.SubmissionHelper
import com.instructure.student.mobius.common.ui.SubmissionService
import com.spotify.mobius.Connection
import com.spotify.mobius.functions.Consumer
-import io.mockk.*
+import io.mockk.confirmVerified
+import io.mockk.every
+import io.mockk.excludeRecords
+import io.mockk.invoke
+import io.mockk.mockk
+import io.mockk.mockkObject
+import io.mockk.mockkStatic
+import io.mockk.slot
+import io.mockk.verify
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.asCoroutineDispatcher
@@ -202,7 +214,9 @@ class SubmissionDetailsEmptyContentEffectHandlerTest : Assert() {
connection.accept(SubmissionDetailsEmptyContentEffect.ShowSubmitDialogView(assignment, course, false))
verify(timeout = 100) {
- view.showSubmitDialogView(assignment, SubmissionTypesVisibilities())
+ view.showSubmitDialogView(assignment,
+ SubmissionTypesVisibilities()
+ )
}
confirmVerified(view)
@@ -413,7 +427,9 @@ class SubmissionDetailsEmptyContentEffectHandlerTest : Assert() {
connection.accept(SubmissionDetailsEmptyContentEffect.ShowSubmitDialogView(assignment, course, false))
verify(timeout = 100) {
- view.showSubmitDialogView(assignment, SubmissionTypesVisibilities())
+ view.showSubmitDialogView(assignment,
+ SubmissionTypesVisibilities()
+ )
}
confirmVerified(view)
@@ -429,7 +445,10 @@ class SubmissionDetailsEmptyContentEffectHandlerTest : Assert() {
verify(timeout = 100) {
view.showSubmitDialogView(
assignment,
- SubmissionTypesVisibilities(fileUpload = true, studioUpload = true)
+ SubmissionTypesVisibilities(
+ fileUpload = true,
+ studioUpload = true
+ )
)
}
@@ -446,7 +465,11 @@ class SubmissionDetailsEmptyContentEffectHandlerTest : Assert() {
connection.accept(SubmissionDetailsEmptyContentEffect.ShowSubmitDialogView(assignment, course, false))
verify(timeout = 100) {
- view.showSubmitDialogView(assignment, SubmissionTypesVisibilities(fileUpload = true))
+ view.showSubmitDialogView(assignment,
+ SubmissionTypesVisibilities(
+ fileUpload = true
+ )
+ )
}
confirmVerified(view)
@@ -461,7 +484,11 @@ class SubmissionDetailsEmptyContentEffectHandlerTest : Assert() {
connection.accept(SubmissionDetailsEmptyContentEffect.ShowSubmitDialogView(assignment, course, false))
verify(timeout = 100) {
- view.showSubmitDialogView(assignment, SubmissionTypesVisibilities(textEntry = true))
+ view.showSubmitDialogView(assignment,
+ SubmissionTypesVisibilities(
+ textEntry = true
+ )
+ )
}
confirmVerified(view)
@@ -476,7 +503,11 @@ class SubmissionDetailsEmptyContentEffectHandlerTest : Assert() {
connection.accept(SubmissionDetailsEmptyContentEffect.ShowSubmitDialogView(assignment, course, false))
verify(timeout = 100) {
- view.showSubmitDialogView(assignment, SubmissionTypesVisibilities(urlEntry = true))
+ view.showSubmitDialogView(assignment,
+ SubmissionTypesVisibilities(
+ urlEntry = true
+ )
+ )
}
confirmVerified(view)
@@ -492,7 +523,11 @@ class SubmissionDetailsEmptyContentEffectHandlerTest : Assert() {
connection.accept(SubmissionDetailsEmptyContentEffect.ShowSubmitDialogView(assignment, course, false))
verify(timeout = 100) {
- view.showSubmitDialogView(assignment, SubmissionTypesVisibilities(studentAnnotation = true))
+ view.showSubmitDialogView(assignment,
+ SubmissionTypesVisibilities(
+ studentAnnotation = true
+ )
+ )
}
confirmVerified(view)
@@ -509,7 +544,9 @@ class SubmissionDetailsEmptyContentEffectHandlerTest : Assert() {
verify(timeout = 100) {
view.showSubmitDialogView(
assignment,
- SubmissionTypesVisibilities(mediaRecording = true)
+ SubmissionTypesVisibilities(
+ mediaRecording = true
+ )
)
}
@@ -533,7 +570,8 @@ class SubmissionDetailsEmptyContentEffectHandlerTest : Assert() {
fileUpload = true,
mediaRecording = true,
studioUpload = true,
- studentAnnotation = true)
+ studentAnnotation = true
+ )
)
}
diff --git a/apps/student/src/test/java/com/instructure/student/test/assignment/details/submissionDetails/SubmissionDetailsLocalDataSourceTest.kt b/apps/student/src/test/java/com/instructure/student/test/assignment/details/submissionDetails/SubmissionDetailsLocalDataSourceTest.kt
index 95c41a623a..dd62fd1124 100644
--- a/apps/student/src/test/java/com/instructure/student/test/assignment/details/submissionDetails/SubmissionDetailsLocalDataSourceTest.kt
+++ b/apps/student/src/test/java/com/instructure/student/test/assignment/details/submissionDetails/SubmissionDetailsLocalDataSourceTest.kt
@@ -33,13 +33,11 @@ import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.mockk
import junit.framework.TestCase
-import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Test
-@ExperimentalCoroutinesApi
class SubmissionDetailsLocalDataSourceTest {
private val enrollmentFacade: EnrollmentFacade = mockk(relaxed = true)
diff --git a/apps/student/src/test/java/com/instructure/student/test/assignment/details/submissionDetails/SubmissionDetailsNetworkDataSourceTest.kt b/apps/student/src/test/java/com/instructure/student/test/assignment/details/submissionDetails/SubmissionDetailsNetworkDataSourceTest.kt
index 1473a2a8c2..b7a97ed818 100644
--- a/apps/student/src/test/java/com/instructure/student/test/assignment/details/submissionDetails/SubmissionDetailsNetworkDataSourceTest.kt
+++ b/apps/student/src/test/java/com/instructure/student/test/assignment/details/submissionDetails/SubmissionDetailsNetworkDataSourceTest.kt
@@ -26,13 +26,11 @@ import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.mockk
import junit.framework.TestCase
-import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.Assert
import org.junit.Before
import org.junit.Test
-@ExperimentalCoroutinesApi
class SubmissionDetailsNetworkDataSourceTest {
private val enrollmentApi: EnrollmentAPI.EnrollmentInterface = mockk(relaxed = true)
diff --git a/apps/student/src/test/java/com/instructure/student/test/assignment/details/submissionDetails/SubmissionDetailsPresenterTest.kt b/apps/student/src/test/java/com/instructure/student/test/assignment/details/submissionDetails/SubmissionDetailsPresenterTest.kt
index c0baca9b05..dc4af6d727 100644
--- a/apps/student/src/test/java/com/instructure/student/test/assignment/details/submissionDetails/SubmissionDetailsPresenterTest.kt
+++ b/apps/student/src/test/java/com/instructure/student/test/assignment/details/submissionDetails/SubmissionDetailsPresenterTest.kt
@@ -19,7 +19,12 @@ package com.instructure.student.test.assignment.details.submissionDetails
import android.content.Context
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
-import com.instructure.canvasapi2.models.*
+import com.instructure.canvasapi2.models.Assignment
+import com.instructure.canvasapi2.models.Attachment
+import com.instructure.canvasapi2.models.Course
+import com.instructure.canvasapi2.models.RubricCriterion
+import com.instructure.canvasapi2.models.RubricCriterionAssessment
+import com.instructure.canvasapi2.models.Submission
import com.instructure.canvasapi2.utils.DataResult
import com.instructure.canvasapi2.utils.DateHelper
import com.instructure.student.mobius.assignmentDetails.submissionDetails.SubmissionDetailsModel
@@ -30,7 +35,6 @@ import org.junit.Assert
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
-import java.util.*
@RunWith(AndroidJUnit4::class)
class SubmissionDetailsPresenterTest : Assert() {
diff --git a/apps/student/src/test/java/com/instructure/student/test/assignment/details/submissionDetails/SubmissionDetailsRepositoryTest.kt b/apps/student/src/test/java/com/instructure/student/test/assignment/details/submissionDetails/SubmissionDetailsRepositoryTest.kt
index af3a83944b..d216461d88 100644
--- a/apps/student/src/test/java/com/instructure/student/test/assignment/details/submissionDetails/SubmissionDetailsRepositoryTest.kt
+++ b/apps/student/src/test/java/com/instructure/student/test/assignment/details/submissionDetails/SubmissionDetailsRepositoryTest.kt
@@ -17,9 +17,13 @@
package com.instructure.student.test.assignment.details.submissionDetails
-import com.instructure.canvasapi2.models.*
+import com.instructure.canvasapi2.models.Assignment
+import com.instructure.canvasapi2.models.CourseSettings
+import com.instructure.canvasapi2.models.Enrollment
+import com.instructure.canvasapi2.models.LTITool
+import com.instructure.canvasapi2.models.Quiz
+import com.instructure.canvasapi2.models.Submission
import com.instructure.canvasapi2.utils.DataResult
-import com.instructure.pandautils.utils.FEATURE_FLAG_OFFLINE
import com.instructure.pandautils.utils.FeatureFlagProvider
import com.instructure.pandautils.utils.NetworkStateProvider
import com.instructure.student.mobius.assignmentDetails.submissionDetails.SubmissionDetailsRepository
@@ -30,13 +34,11 @@ import io.mockk.coVerify
import io.mockk.every
import io.mockk.mockk
import junit.framework.TestCase
-import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.Assert
import org.junit.Before
import org.junit.Test
-@ExperimentalCoroutinesApi
class SubmissionDetailsRepositoryTest {
private val localDataSource: SubmissionDetailsLocalDataSource = mockk(relaxed = true)
diff --git a/apps/student/src/test/java/com/instructure/student/test/assignment/details/submissionDetails/commentTab/SubmissionCommentsEffectHandlerTest.kt b/apps/student/src/test/java/com/instructure/student/test/assignment/details/submissionDetails/commentTab/SubmissionCommentsEffectHandlerTest.kt
index ebb7d9b9fe..cb61d84553 100644
--- a/apps/student/src/test/java/com/instructure/student/test/assignment/details/submissionDetails/commentTab/SubmissionCommentsEffectHandlerTest.kt
+++ b/apps/student/src/test/java/com/instructure/student/test/assignment/details/submissionDetails/commentTab/SubmissionCommentsEffectHandlerTest.kt
@@ -18,12 +18,12 @@
package com.instructure.student.test.assignment.details.submissionDetails.commentTab
-import android.app.Activity
import androidx.fragment.app.FragmentActivity
import com.instructure.canvasapi2.models.Assignment
import com.instructure.canvasapi2.models.Attachment
import com.instructure.canvasapi2.models.Course
import com.instructure.canvasapi2.models.Submission
+import com.instructure.canvasapi2.utils.Analytics
import com.instructure.pandautils.utils.PermissionUtils
import com.instructure.pandautils.utils.requestPermissions
import com.instructure.student.mobius.assignmentDetails.submissionDetails.SubmissionDetailsSharedEvent
@@ -31,21 +31,34 @@ import com.instructure.student.mobius.assignmentDetails.submissionDetails.drawer
import com.instructure.student.mobius.assignmentDetails.submissionDetails.drawer.comments.SubmissionCommentsEffectHandler
import com.instructure.student.mobius.assignmentDetails.submissionDetails.drawer.comments.SubmissionCommentsEvent
import com.instructure.student.mobius.assignmentDetails.submissionDetails.drawer.comments.ui.SubmissionCommentsView
-import com.instructure.student.mobius.common.ChannelSource
+import com.instructure.student.mobius.common.FlowSource
import com.instructure.student.mobius.common.ui.SubmissionHelper
import com.instructure.student.mobius.common.ui.SubmissionService
-import com.instructure.student.test.util.receiveOnce
import com.spotify.mobius.functions.Consumer
-import io.mockk.*
+import io.mockk.confirmVerified
+import io.mockk.every
+import io.mockk.invoke
+import io.mockk.just
+import io.mockk.mockk
+import io.mockk.mockkObject
+import io.mockk.mockkStatic
+import io.mockk.runs
+import io.mockk.slot
+import io.mockk.unmockkAll
+import io.mockk.verify
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.asCoroutineDispatcher
+import kotlinx.coroutines.async
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.resetMain
+import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.test.setMain
+import org.junit.After
import org.junit.Assert
import org.junit.Before
import org.junit.Test
import java.io.File
-import java.util.concurrent.Executors
@OptIn(ExperimentalCoroutinesApi::class)
class SubmissionCommentsEffectHandlerTest : Assert(){
@@ -56,10 +69,20 @@ class SubmissionCommentsEffectHandlerTest : Assert(){
private val effectHandler = SubmissionCommentsEffectHandler(context, submissionHelper).apply { view = mockView }
private val eventConsumer: Consumer = mockk(relaxed = true)
private val connection = effectHandler.connect(eventConsumer)
+ private val testDispatcher = UnconfinedTestDispatcher()
@Before
fun setup() {
- Dispatchers.setMain(Executors.newSingleThreadExecutor().asCoroutineDispatcher())
+ Dispatchers.setMain(testDispatcher)
+
+ mockkObject(Analytics)
+ every { Analytics.logEvent(any()) } just runs
+ }
+
+ @After
+ fun tearDown() {
+ Dispatchers.resetMain()
+ unmockkAll()
}
@Test
@@ -127,76 +150,82 @@ class SubmissionCommentsEffectHandlerTest : Assert(){
}
@Test
- fun `ShowAudioRecordingView effect with permission results in AudioRecordingViewLaunched shared event`() {
+ fun `ShowAudioRecordingView effect with permission results in AudioRecordingViewLaunched shared event`() = runTest(testDispatcher) {
mockPermissions(true)
- val channel = ChannelSource.getChannel()
+ val flow = FlowSource.getFlow()
val expectedEvent = SubmissionDetailsSharedEvent.AudioRecordingViewLaunched
- val actualEvent = channel.receiveOnce {
- connection.accept(SubmissionCommentsEffect.ShowAudioRecordingView)
+ val deferred = async {
+ flow.first()
}
- assertEquals(expectedEvent, actualEvent)
+ connection.accept(SubmissionCommentsEffect.ShowAudioRecordingView)
+ assertEquals(expectedEvent, deferred.await())
}
@Test
- fun `ShowVideoRecordingView effect with permission results in VideoRecordingViewLaunched shared event`() {
+ fun `ShowVideoRecordingView effect with permission results in VideoRecordingViewLaunched shared event`() = runTest(testDispatcher) {
mockPermissions(true)
- val channel = ChannelSource.getChannel()
+ val flow = FlowSource.getFlow()
val expectedEvent = SubmissionDetailsSharedEvent.VideoRecordingViewLaunched
- val actualEvent = channel.receiveOnce {
- connection.accept(SubmissionCommentsEffect.ShowVideoRecordingView)
+ val deferred = async {
+ flow.first()
}
- assertEquals(expectedEvent, actualEvent)
+ connection.accept(SubmissionCommentsEffect.ShowVideoRecordingView)
+ assertEquals(expectedEvent, deferred.await())
}
@Test
- fun `ShowAudioRecordingView effect without permissions results in AudioRecordingViewLaunched shared event`() {
+ fun `ShowAudioRecordingView effect without permissions results in AudioRecordingViewLaunched shared event`() = runTest(testDispatcher) {
mockPermissions(hasPermission = true, permissionGranted = true)
- val channel = ChannelSource.getChannel()
+ val flow = FlowSource.getFlow()
val expectedEvent = SubmissionDetailsSharedEvent.AudioRecordingViewLaunched
- val actualEvent = channel.receiveOnce {
- connection.accept(SubmissionCommentsEffect.ShowAudioRecordingView)
+ val deferred = async {
+ flow.first()
}
- assertEquals(expectedEvent, actualEvent)
+ connection.accept(SubmissionCommentsEffect.ShowAudioRecordingView)
+ assertEquals(expectedEvent, deferred.await())
}
@Test
- fun `ShowVideoRecordingView effect without permission results in VideoRecordingViewLaunched shared event`() {
+ fun `ShowVideoRecordingView effect without permission results in VideoRecordingViewLaunched shared event`() = runTest(testDispatcher) {
mockPermissions(hasPermission = true, permissionGranted = true)
- val channel = ChannelSource.getChannel()
+ val flow = FlowSource.getFlow()
val expectedEvent = SubmissionDetailsSharedEvent.VideoRecordingViewLaunched
- val actualEvent = channel.receiveOnce {
- connection.accept(SubmissionCommentsEffect.ShowVideoRecordingView)
+ val deferred = async {
+ flow.first()
}
- assertEquals(expectedEvent, actualEvent)
+ connection.accept(SubmissionCommentsEffect.ShowVideoRecordingView)
+ assertEquals(expectedEvent, deferred.await())
}
@Test
- fun `ShowAudioRecordingView effect with permission check results in AudioRecordingViewLaunched shared event`() {
+ fun `ShowAudioRecordingView effect with permission check results in AudioRecordingViewLaunched shared event`() = runTest(testDispatcher) {
mockPermissions(hasPermission = false, permissionGranted = true)
- val channel = ChannelSource.getChannel()
+ val flow = FlowSource.getFlow()
val expectedEvent = SubmissionDetailsSharedEvent.AudioRecordingViewLaunched
- val actualEvent = channel.receiveOnce {
- connection.accept(SubmissionCommentsEffect.ShowAudioRecordingView)
+ val deferred = async {
+ flow.first()
}
- assertEquals(expectedEvent, actualEvent)
+ connection.accept(SubmissionCommentsEffect.ShowAudioRecordingView)
+ assertEquals(expectedEvent, deferred.await())
}
@Test
- fun `ShowVideoRecordingView effect with permission check results in VideoRecordingViewLaunched shared event`() {
+ fun `ShowVideoRecordingView effect with permission check results in VideoRecordingViewLaunched shared event`() = runTest(testDispatcher) {
mockPermissions(hasPermission = false, permissionGranted = true)
- val channel = ChannelSource.getChannel()
+ val flow = FlowSource.getFlow()
val expectedEvent = SubmissionDetailsSharedEvent.VideoRecordingViewLaunched
- val actualEvent = channel.receiveOnce {
- connection.accept(SubmissionCommentsEffect.ShowVideoRecordingView)
+ val deferred = async {
+ flow.first()
}
- assertEquals(expectedEvent, actualEvent)
+ connection.accept(SubmissionCommentsEffect.ShowVideoRecordingView)
+ assertEquals(expectedEvent, deferred.await())
}
@Test
@@ -316,26 +345,28 @@ class SubmissionCommentsEffectHandlerTest : Assert(){
}
@Test
- fun `BroadcastSubmissionSelected effect sends SubmissionClicked shared event`() {
- val channel = ChannelSource.getChannel()
+ fun `BroadcastSubmissionSelected effect sends SubmissionClicked shared event`() = runTest(testDispatcher) {
+ val flow = FlowSource.getFlow()
val submission = Submission(123L)
val expectedEvent = SubmissionDetailsSharedEvent.SubmissionClicked(submission)
- val actualEvent = channel.receiveOnce {
- connection.accept(SubmissionCommentsEffect.BroadcastSubmissionSelected(submission))
+ val deferred = async {
+ flow.first()
}
- assertEquals(expectedEvent, actualEvent)
+ connection.accept(SubmissionCommentsEffect.BroadcastSubmissionSelected(submission))
+ assertEquals(expectedEvent, deferred.await())
}
@Test
- fun `BroadcastSubmissionAttachmentSelected effect sends SubmissionAttachmentClicked shared event`() {
- val channel = ChannelSource.getChannel()
+ fun `BroadcastSubmissionAttachmentSelected effect sends SubmissionAttachmentClicked shared event`() = runTest(testDispatcher) {
+ val flow = FlowSource.getFlow()
val submission = Submission(123L)
val attachment = Attachment(id = 456L, contentType = "test/data")
val expectedEvent = SubmissionDetailsSharedEvent.SubmissionAttachmentClicked(submission, attachment)
- val actualEvent = channel.receiveOnce {
- connection.accept(SubmissionCommentsEffect.BroadcastSubmissionAttachmentSelected(submission, attachment))
+ val deferred = async {
+ flow.first()
}
- assertEquals(expectedEvent, actualEvent)
+ connection.accept(SubmissionCommentsEffect.BroadcastSubmissionAttachmentSelected(submission, attachment))
+ assertEquals(expectedEvent, deferred.await())
}
@Test
diff --git a/apps/student/src/test/java/com/instructure/student/test/assignment/details/submissionDetails/fileTab/SubmissionFilesEffectHandlerTest.kt b/apps/student/src/test/java/com/instructure/student/test/assignment/details/submissionDetails/fileTab/SubmissionFilesEffectHandlerTest.kt
index 99a069c5f9..aeed485bf5 100644
--- a/apps/student/src/test/java/com/instructure/student/test/assignment/details/submissionDetails/fileTab/SubmissionFilesEffectHandlerTest.kt
+++ b/apps/student/src/test/java/com/instructure/student/test/assignment/details/submissionDetails/fileTab/SubmissionFilesEffectHandlerTest.kt
@@ -23,38 +23,52 @@ import com.instructure.student.mobius.assignmentDetails.submissionDetails.drawer
import com.instructure.student.mobius.assignmentDetails.submissionDetails.drawer.files.SubmissionFilesEffectHandler
import com.instructure.student.mobius.assignmentDetails.submissionDetails.drawer.files.SubmissionFilesEvent
import com.instructure.student.mobius.assignmentDetails.submissionDetails.drawer.files.ui.SubmissionFilesView
-import com.instructure.student.mobius.common.ChannelSource
-import com.instructure.student.test.util.receiveOnce
+import com.instructure.student.mobius.common.FlowSource
import com.spotify.mobius.functions.Consumer
import io.mockk.mockk
import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.asCoroutineDispatcher
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.async
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.resetMain
+import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.test.setMain
+import org.junit.After
import org.junit.Assert
import org.junit.Before
import org.junit.Test
-import java.util.concurrent.Executors
+@OptIn(ExperimentalCoroutinesApi::class)
class SubmissionFilesEffectHandlerTest : Assert() {
private val mockView: SubmissionFilesView = mockk(relaxed = true)
private val effectHandler = SubmissionFilesEffectHandler().apply { view = mockView }
private val eventConsumer: Consumer = mockk(relaxed = true)
private val connection = effectHandler.connect(eventConsumer)
+ private val testDispatcher = UnconfinedTestDispatcher()
@Before
fun setup() {
- Dispatchers.setMain(Executors.newSingleThreadExecutor().asCoroutineDispatcher())
+ Dispatchers.setMain(testDispatcher)
+ }
+
+ @After
+ fun tearDown() {
+ Dispatchers.resetMain()
}
@Test
- fun `BroadcastFileSelected effect sends File selected shared event`() {
- val channel = ChannelSource.getChannel()
+ fun `BroadcastFileSelected effect sends File selected shared event`() = runTest(testDispatcher) {
+ val flow = FlowSource.getFlow()
val attachment = Attachment(id = 123L, contentType = "test/data")
val expectedEvent = SubmissionDetailsSharedEvent.FileSelected(attachment)
- val actualEvent = channel.receiveOnce {
- connection.accept(SubmissionFilesEffect.BroadcastFileSelected(attachment))
+
+ val deferred = async {
+ flow.first()
}
- assertEquals(expectedEvent, actualEvent)
+
+ connection.accept(SubmissionFilesEffect.BroadcastFileSelected(attachment))
+ assertEquals(expectedEvent, deferred.await())
}
}
diff --git a/apps/student/src/test/java/com/instructure/student/test/assignment/details/submissionDetails/rubricTab/SubmissionRubricPresenterTest.kt b/apps/student/src/test/java/com/instructure/student/test/assignment/details/submissionDetails/rubricTab/SubmissionRubricPresenterTest.kt
index 776ff2635f..d55e7d1b39 100644
--- a/apps/student/src/test/java/com/instructure/student/test/assignment/details/submissionDetails/rubricTab/SubmissionRubricPresenterTest.kt
+++ b/apps/student/src/test/java/com/instructure/student/test/assignment/details/submissionDetails/rubricTab/SubmissionRubricPresenterTest.kt
@@ -27,13 +27,13 @@ import com.instructure.canvasapi2.models.RubricCriterionRating
import com.instructure.canvasapi2.models.RubricSettings
import com.instructure.canvasapi2.models.Submission
import com.instructure.canvasapi2.utils.DateHelper
+import com.instructure.pandautils.features.assignments.details.mobius.gradeCell.GradeCellViewState
import com.instructure.pandautils.utils.color
import com.instructure.student.mobius.assignmentDetails.submissionDetails.drawer.rubric.RatingData
import com.instructure.student.mobius.assignmentDetails.submissionDetails.drawer.rubric.RubricListData
import com.instructure.student.mobius.assignmentDetails.submissionDetails.drawer.rubric.SubmissionRubricModel
import com.instructure.student.mobius.assignmentDetails.submissionDetails.drawer.rubric.SubmissionRubricPresenter
import com.instructure.student.mobius.assignmentDetails.submissionDetails.drawer.rubric.SubmissionRubricViewState
-import com.instructure.student.mobius.assignmentDetails.ui.gradeCell.GradeCellViewState
import org.junit.Assert
import org.junit.Before
import org.junit.Test
diff --git a/apps/student/src/test/java/com/instructure/student/test/conferences/conference_details/ConferenceDetailsEffectHandlerTest.kt b/apps/student/src/test/java/com/instructure/student/test/conferences/conference_details/ConferenceDetailsEffectHandlerTest.kt
index 625a803c43..dcf82a66f6 100644
--- a/apps/student/src/test/java/com/instructure/student/test/conferences/conference_details/ConferenceDetailsEffectHandlerTest.kt
+++ b/apps/student/src/test/java/com/instructure/student/test/conferences/conference_details/ConferenceDetailsEffectHandlerTest.kt
@@ -16,7 +16,10 @@
*/
package com.instructure.student.test.conferences.conference_details
-import com.instructure.canvasapi2.models.*
+import com.instructure.canvasapi2.models.AuthenticatedSession
+import com.instructure.canvasapi2.models.CanvasContext
+import com.instructure.canvasapi2.models.Conference
+import com.instructure.canvasapi2.models.Course
import com.instructure.canvasapi2.utils.DataResult
import com.instructure.student.mobius.conferences.conference_details.ConferenceDetailsEffect
import com.instructure.student.mobius.conferences.conference_details.ConferenceDetailsEffectHandler
@@ -24,10 +27,19 @@ import com.instructure.student.mobius.conferences.conference_details.ConferenceD
import com.instructure.student.mobius.conferences.conference_details.ConferenceDetailsRepository
import com.instructure.student.mobius.conferences.conference_details.ui.ConferenceDetailsView
import com.spotify.mobius.functions.Consumer
-import io.mockk.*
+import io.mockk.clearAllMocks
+import io.mockk.coEvery
+import io.mockk.coVerify
+import io.mockk.confirmVerified
+import io.mockk.mockk
+import io.mockk.verify
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.test.*
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.advanceUntilIdle
+import kotlinx.coroutines.test.resetMain
+import kotlinx.coroutines.test.runTest
+import kotlinx.coroutines.test.setMain
import org.junit.After
import org.junit.Assert
import org.junit.Before
@@ -35,7 +47,7 @@ import org.junit.Test
@ExperimentalCoroutinesApi
class ConferenceDetailsEffectHandlerTest : Assert() {
- private val testDispatcher = TestCoroutineDispatcher()
+ private val testDispatcher = UnconfinedTestDispatcher()
private val view: ConferenceDetailsView = mockk(relaxed = true)
private val repository: ConferenceDetailsRepository = mockk(relaxed = true)
private val effectHandler = ConferenceDetailsEffectHandler(repository).apply {
@@ -53,14 +65,11 @@ class ConferenceDetailsEffectHandlerTest : Assert() {
@After
fun cleanUp() {
Dispatchers.resetMain()
- testDispatcher.cleanupTestCoroutines()
clearAllMocks()
}
- fun test(block: suspend TestCoroutineScope.() -> Unit) = testDispatcher.runBlockingTest(block)
-
@Test
- fun `ShowRecording calls launchUrl on view and produces ShowRecordingFinished event`() = test {
+ fun `ShowRecording calls launchUrl on view and produces ShowRecordingFinished event`() = runTest {
val recordingId = "recording_123"
val url = "url"
@@ -80,7 +89,7 @@ class ConferenceDetailsEffectHandlerTest : Assert() {
}
@Test
- fun `JoinConference calls launchUrl and produces JoinConferenceFinished event`() = test {
+ fun `JoinConference calls launchUrl and produces JoinConferenceFinished event`() = runTest {
val url = "url"
val authenticate = false
@@ -100,7 +109,7 @@ class ConferenceDetailsEffectHandlerTest : Assert() {
}
@Test
- fun `JoinConference calls API when authenticate is true`() = test {
+ fun `JoinConference calls API when authenticate is true`() = runTest {
val url = "url"
val sessionUrl = "session-url"
val authenticate = true
@@ -128,7 +137,7 @@ class ConferenceDetailsEffectHandlerTest : Assert() {
@Suppress("DeferredResultUnused")
@Test
- fun `RefreshData calls API and produces RefreshFinished event`() = test {
+ fun `RefreshData calls API and produces RefreshFinished event`() = runTest {
val canvasContext: CanvasContext = Course(id = 123L)
val apiResult = DataResult.Success(emptyList())
diff --git a/apps/student/src/test/java/com/instructure/student/test/conferences/conference_details/ConferenceDetailsLocalDataSourceTest.kt b/apps/student/src/test/java/com/instructure/student/test/conferences/conference_details/ConferenceDetailsLocalDataSourceTest.kt
index 3a897a34ed..b446e64af9 100644
--- a/apps/student/src/test/java/com/instructure/student/test/conferences/conference_details/ConferenceDetailsLocalDataSourceTest.kt
+++ b/apps/student/src/test/java/com/instructure/student/test/conferences/conference_details/ConferenceDetailsLocalDataSourceTest.kt
@@ -26,12 +26,10 @@ import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.mockk
import junit.framework.TestCase
-import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
-@ExperimentalCoroutinesApi
class ConferenceDetailsLocalDataSourceTest {
private val conferenceFacade: ConferenceFacade = mockk(relaxed = true)
diff --git a/apps/student/src/test/java/com/instructure/student/test/conferences/conference_details/ConferenceDetailsNetworkDataSourceTest.kt b/apps/student/src/test/java/com/instructure/student/test/conferences/conference_details/ConferenceDetailsNetworkDataSourceTest.kt
index 9f87aeaf4f..3caf97e37a 100644
--- a/apps/student/src/test/java/com/instructure/student/test/conferences/conference_details/ConferenceDetailsNetworkDataSourceTest.kt
+++ b/apps/student/src/test/java/com/instructure/student/test/conferences/conference_details/ConferenceDetailsNetworkDataSourceTest.kt
@@ -31,12 +31,10 @@ import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.mockk
import junit.framework.TestCase
-import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
-@ExperimentalCoroutinesApi
class ConferenceDetailsNetworkDataSourceTest {
private val conferencesApi: ConferencesApi.ConferencesInterface = mockk(relaxed = true)
diff --git a/apps/student/src/test/java/com/instructure/student/test/conferences/conference_details/ConferenceDetailsRepositoryTest.kt b/apps/student/src/test/java/com/instructure/student/test/conferences/conference_details/ConferenceDetailsRepositoryTest.kt
index 8324672d8e..51d3c1393d 100644
--- a/apps/student/src/test/java/com/instructure/student/test/conferences/conference_details/ConferenceDetailsRepositoryTest.kt
+++ b/apps/student/src/test/java/com/instructure/student/test/conferences/conference_details/ConferenceDetailsRepositoryTest.kt
@@ -21,7 +21,6 @@ import com.instructure.canvasapi2.models.AuthenticatedSession
import com.instructure.canvasapi2.models.CanvasContext
import com.instructure.canvasapi2.models.Conference
import com.instructure.canvasapi2.utils.DataResult
-import com.instructure.pandautils.utils.FEATURE_FLAG_OFFLINE
import com.instructure.pandautils.utils.FeatureFlagProvider
import com.instructure.pandautils.utils.NetworkStateProvider
import com.instructure.student.mobius.conferences.conference_details.ConferenceDetailsRepository
@@ -32,12 +31,10 @@ import io.mockk.coVerify
import io.mockk.every
import io.mockk.mockk
import junit.framework.TestCase
-import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
-@ExperimentalCoroutinesApi
class ConferenceDetailsRepositoryTest {
private val localDataSource: ConferenceDetailsLocalDataSource = mockk(relaxed = true)
diff --git a/apps/student/src/test/java/com/instructure/student/test/conferences/conference_list/ConferenceListEffectHandlerTest.kt b/apps/student/src/test/java/com/instructure/student/test/conferences/conference_list/ConferenceListEffectHandlerTest.kt
index ae9b2b0cec..761cde1f55 100644
--- a/apps/student/src/test/java/com/instructure/student/test/conferences/conference_list/ConferenceListEffectHandlerTest.kt
+++ b/apps/student/src/test/java/com/instructure/student/test/conferences/conference_list/ConferenceListEffectHandlerTest.kt
@@ -16,7 +16,10 @@
*/
package com.instructure.student.test.conferences.conference_list
-import com.instructure.canvasapi2.models.*
+import com.instructure.canvasapi2.models.AuthenticatedSession
+import com.instructure.canvasapi2.models.CanvasContext
+import com.instructure.canvasapi2.models.Conference
+import com.instructure.canvasapi2.models.Course
import com.instructure.canvasapi2.utils.DataResult
import com.instructure.student.mobius.conferences.conference_list.ConferenceListEffect
import com.instructure.student.mobius.conferences.conference_list.ConferenceListEffectHandler
@@ -24,10 +27,19 @@ import com.instructure.student.mobius.conferences.conference_list.ConferenceList
import com.instructure.student.mobius.conferences.conference_list.ConferenceListRepository
import com.instructure.student.mobius.conferences.conference_list.ui.ConferenceListView
import com.spotify.mobius.functions.Consumer
-import io.mockk.*
+import io.mockk.clearAllMocks
+import io.mockk.coEvery
+import io.mockk.coVerify
+import io.mockk.confirmVerified
+import io.mockk.mockk
+import io.mockk.verify
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.test.*
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.advanceUntilIdle
+import kotlinx.coroutines.test.resetMain
+import kotlinx.coroutines.test.runTest
+import kotlinx.coroutines.test.setMain
import org.junit.After
import org.junit.Assert
import org.junit.Before
@@ -35,7 +47,7 @@ import org.junit.Test
@ExperimentalCoroutinesApi
class ConferenceListEffectHandlerTest : Assert() {
- private val testDispatcher = TestCoroutineDispatcher()
+ private val testDispatcher = UnconfinedTestDispatcher()
private val view: ConferenceListView = mockk(relaxed = true)
private val repository: ConferenceListRepository = mockk(relaxed = true)
private val effectHandler = ConferenceListEffectHandler(repository).apply {
@@ -53,12 +65,9 @@ class ConferenceListEffectHandlerTest : Assert() {
@After
fun cleanUp() {
Dispatchers.resetMain()
- testDispatcher.cleanupTestCoroutines()
clearAllMocks()
}
- fun test(block: suspend TestCoroutineScope.() -> Unit) = testDispatcher.runBlockingTest(block)
-
@Suppress("DeferredResultUnused")
@Test
fun `LoadData calls API and returns DataLoaded event`() {
@@ -79,7 +88,7 @@ class ConferenceListEffectHandlerTest : Assert() {
}
@Test
- fun `LaunchInBrowser calls API, calls launchUrl and produces LaunchInBrowserFinished`() = test {
+ fun `LaunchInBrowser calls API, calls launchUrl and produces LaunchInBrowserFinished`() = runTest {
val url = "url"
val sessionUrl = "session-url"
diff --git a/apps/student/src/test/java/com/instructure/student/test/conferences/conference_list/ConferenceListLocalDataSourceTest.kt b/apps/student/src/test/java/com/instructure/student/test/conferences/conference_list/ConferenceListLocalDataSourceTest.kt
index 5670a532ff..bf49379bc7 100644
--- a/apps/student/src/test/java/com/instructure/student/test/conferences/conference_list/ConferenceListLocalDataSourceTest.kt
+++ b/apps/student/src/test/java/com/instructure/student/test/conferences/conference_list/ConferenceListLocalDataSourceTest.kt
@@ -26,12 +26,10 @@ import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.mockk
import junit.framework.TestCase
-import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
-@ExperimentalCoroutinesApi
class ConferenceListLocalDataSourceTest {
private val conferenceFacade: ConferenceFacade = mockk(relaxed = true)
diff --git a/apps/student/src/test/java/com/instructure/student/test/conferences/conference_list/ConferenceListNetworkDataSourceTest.kt b/apps/student/src/test/java/com/instructure/student/test/conferences/conference_list/ConferenceListNetworkDataSourceTest.kt
index cfc9cfb2db..68b99272a7 100644
--- a/apps/student/src/test/java/com/instructure/student/test/conferences/conference_list/ConferenceListNetworkDataSourceTest.kt
+++ b/apps/student/src/test/java/com/instructure/student/test/conferences/conference_list/ConferenceListNetworkDataSourceTest.kt
@@ -31,12 +31,10 @@ import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.mockk
import junit.framework.TestCase
-import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
-@ExperimentalCoroutinesApi
class ConferenceListNetworkDataSourceTest {
private val conferencesApi: ConferencesApi.ConferencesInterface = mockk(relaxed = true)
diff --git a/apps/student/src/test/java/com/instructure/student/test/conferences/conference_list/ConferenceListRepositoryTest.kt b/apps/student/src/test/java/com/instructure/student/test/conferences/conference_list/ConferenceListRepositoryTest.kt
index 27678507ff..12a16d0cc4 100644
--- a/apps/student/src/test/java/com/instructure/student/test/conferences/conference_list/ConferenceListRepositoryTest.kt
+++ b/apps/student/src/test/java/com/instructure/student/test/conferences/conference_list/ConferenceListRepositoryTest.kt
@@ -37,7 +37,6 @@ import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
-@ExperimentalCoroutinesApi
class ConferenceListRepositoryTest {
private val localDataSource: ConferenceListLocalDataSource = mockk(relaxed = true)
diff --git a/apps/student/src/test/java/com/instructure/student/test/syllabus/SyllabusLocalDataSourceTest.kt b/apps/student/src/test/java/com/instructure/student/test/syllabus/SyllabusLocalDataSourceTest.kt
index 563ed2a5ee..46fa94252d 100644
--- a/apps/student/src/test/java/com/instructure/student/test/syllabus/SyllabusLocalDataSourceTest.kt
+++ b/apps/student/src/test/java/com/instructure/student/test/syllabus/SyllabusLocalDataSourceTest.kt
@@ -32,12 +32,10 @@ import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.mockk
import junit.framework.TestCase.assertEquals
-import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
-@ExperimentalCoroutinesApi
class SyllabusLocalDataSourceTest {
private val courseSettingsDao: CourseSettingsDao = mockk(relaxed = true)
diff --git a/apps/student/src/test/java/com/instructure/student/test/syllabus/SyllabusNetworkDataSourceTest.kt b/apps/student/src/test/java/com/instructure/student/test/syllabus/SyllabusNetworkDataSourceTest.kt
index 2b98c7787b..6f926884be 100644
--- a/apps/student/src/test/java/com/instructure/student/test/syllabus/SyllabusNetworkDataSourceTest.kt
+++ b/apps/student/src/test/java/com/instructure/student/test/syllabus/SyllabusNetworkDataSourceTest.kt
@@ -32,12 +32,10 @@ import io.mockk.coVerify
import io.mockk.mockk
import junit.framework.TestCase.assertEquals
import junit.framework.TestCase.assertNull
-import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
-@ExperimentalCoroutinesApi
class SyllabusNetworkDataSourceTest {
private val courseApi: CourseAPI.CoursesInterface = mockk(relaxed = true)
diff --git a/apps/student/src/test/java/com/instructure/student/test/syllabus/SyllabusRepositoryTest.kt b/apps/student/src/test/java/com/instructure/student/test/syllabus/SyllabusRepositoryTest.kt
index 34d6b41f47..dbff3a6049 100644
--- a/apps/student/src/test/java/com/instructure/student/test/syllabus/SyllabusRepositoryTest.kt
+++ b/apps/student/src/test/java/com/instructure/student/test/syllabus/SyllabusRepositoryTest.kt
@@ -23,10 +23,8 @@ import com.instructure.canvasapi2.models.Course
import com.instructure.canvasapi2.models.CourseSettings
import com.instructure.canvasapi2.models.ScheduleItem
import com.instructure.canvasapi2.utils.DataResult
-import com.instructure.pandautils.utils.FEATURE_FLAG_OFFLINE
import com.instructure.pandautils.utils.FeatureFlagProvider
import com.instructure.pandautils.utils.NetworkStateProvider
-import com.instructure.student.mobius.conferences.conference_list.ConferenceListRepository
import com.instructure.student.mobius.syllabus.SyllabusRepository
import com.instructure.student.mobius.syllabus.datasource.SyllabusLocalDataSource
import com.instructure.student.mobius.syllabus.datasource.SyllabusNetworkDataSource
@@ -35,12 +33,10 @@ import io.mockk.coVerify
import io.mockk.every
import io.mockk.mockk
import junit.framework.TestCase.assertEquals
-import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
-@ExperimentalCoroutinesApi
class SyllabusRepositoryTest {
private val syllabusLocalDataSource: SyllabusLocalDataSource = mockk(relaxed = true)
diff --git a/apps/student/src/test/java/com/instructure/student/test/util/ModuleUtilityTest.kt b/apps/student/src/test/java/com/instructure/student/test/util/ModuleUtilityTest.kt
index 7381916a36..d4c052a9ea 100644
--- a/apps/student/src/test/java/com/instructure/student/test/util/ModuleUtilityTest.kt
+++ b/apps/student/src/test/java/com/instructure/student/test/util/ModuleUtilityTest.kt
@@ -25,15 +25,15 @@ import com.instructure.canvasapi2.models.Course
import com.instructure.canvasapi2.models.ModuleItem
import com.instructure.canvasapi2.models.ModuleObject
import com.instructure.canvasapi2.models.Tab
+import com.instructure.pandautils.features.assignments.details.AssignmentDetailsFragment
import com.instructure.pandautils.features.discussion.details.DiscussionDetailsWebViewFragment
-import com.instructure.student.features.assignments.details.AssignmentDetailsFragment
import com.instructure.student.features.discussion.details.DiscussionDetailsFragment
import com.instructure.student.features.files.details.FileDetailsFragment
import com.instructure.student.features.modules.progression.ModuleQuizDecider
import com.instructure.student.features.modules.progression.NotAvailableOfflineFragment
import com.instructure.student.features.modules.util.ModuleUtility
import com.instructure.student.features.pages.details.PageDetailsFragment
-import com.instructure.student.fragment.*
+import com.instructure.student.fragment.InternalWebviewFragment
import com.instructure.student.util.Const
import io.mockk.mockk
import junit.framework.TestCase
@@ -141,6 +141,7 @@ class ModuleUtilityTest : TestCase() {
val course = Course()
val expectedBundle = Bundle()
expectedBundle.putParcelable(Const.CANVAS_CONTEXT, course)
+ expectedBundle.putLong(com.instructure.pandautils.utils.Const.COURSE_ID, course.id)
expectedBundle.putLong(Const.ASSIGNMENT_ID, 123456789)
val parentFragment = callGetFragment(moduleItem, course, null)
@@ -161,6 +162,7 @@ class ModuleUtilityTest : TestCase() {
val course = Course()
val expectedBundle = Bundle()
expectedBundle.putParcelable(Const.CANVAS_CONTEXT, course)
+ expectedBundle.putLong(com.instructure.pandautils.utils.Const.COURSE_ID, course.id)
expectedBundle.putLong(Const.ASSIGNMENT_ID, 123456789)
val parentFragment = callGetFragment(moduleItem, course, null, isOnline = false, tabs = setOf(Tab.ASSIGNMENTS_ID))
@@ -197,6 +199,7 @@ class ModuleUtilityTest : TestCase() {
val course = Course()
val expectedBundle = Bundle()
expectedBundle.putParcelable(Const.CANVAS_CONTEXT, course)
+ expectedBundle.putLong(com.instructure.pandautils.utils.Const.COURSE_ID, course.id)
expectedBundle.putLong(Const.ASSIGNMENT_ID, 123450000000006789)
val parentFragment = callGetFragment(moduleItem, course, null)
@@ -217,6 +220,7 @@ class ModuleUtilityTest : TestCase() {
val course = Course()
val expectedBundle = Bundle()
expectedBundle.putParcelable(Const.CANVAS_CONTEXT, course)
+ expectedBundle.putLong(com.instructure.pandautils.utils.Const.COURSE_ID, course.id)
expectedBundle.putLong(Const.ASSIGNMENT_ID, 123450000000006789)
val parentFragment = callGetFragment(moduleItem, course, null)
diff --git a/apps/student/src/test/java/com/instructure/student/test/util/RouterUtilsTest.kt b/apps/student/src/test/java/com/instructure/student/test/util/RouterUtilsTest.kt
index 9fa5ce93f9..a62b1dbb82 100644
--- a/apps/student/src/test/java/com/instructure/student/test/util/RouterUtilsTest.kt
+++ b/apps/student/src/test/java/com/instructure/student/test/util/RouterUtilsTest.kt
@@ -27,7 +27,7 @@ import com.instructure.interactions.router.RouterParams
import com.instructure.pandautils.features.discussion.router.DiscussionRouterFragment
import com.instructure.pandautils.features.inbox.list.InboxFragment
import com.instructure.student.activity.BaseRouterActivity
-import com.instructure.student.features.assignments.details.AssignmentDetailsFragment
+import com.instructure.pandautils.features.assignments.details.AssignmentDetailsFragment
import com.instructure.student.features.assignments.list.AssignmentListFragment
import com.instructure.student.features.discussion.list.DiscussionListFragment
import com.instructure.student.features.grades.GradesListFragment
diff --git a/apps/student/src/test/java/com/instructure/student/test/util/TestUtils.kt b/apps/student/src/test/java/com/instructure/student/test/util/TestUtils.kt
index b3066f1c5c..8e59fcffdf 100644
--- a/apps/student/src/test/java/com/instructure/student/test/util/TestUtils.kt
+++ b/apps/student/src/test/java/com/instructure/student/test/util/TestUtils.kt
@@ -18,17 +18,10 @@
package com.instructure.student.test.util
-import com.instructure.canvasapi2.utils.weave.StatusCallbackError
import com.spotify.mobius.First
import com.spotify.mobius.Next
import com.spotify.mobius.test.FirstMatchers
import com.spotify.mobius.test.NextMatchers
-import kotlinx.coroutines.channels.BroadcastChannel
-import kotlinx.coroutines.runBlocking
-import okhttp3.Protocol
-import okhttp3.Request
-import okhttp3.Response
-import okhttp3.ResponseBody.Companion.toResponseBody
import org.hamcrest.Matcher
import org.hamcrest.Matchers
@@ -39,26 +32,3 @@ fun matchesEffects(vararg effects: F): Matcher> {
fun matchesFirstEffects(vararg effects: F): Matcher> {
return FirstMatchers.hasEffects(Matchers.containsInAnyOrder(*effects))
}
-
-
-fun createError(message: String = "Error", code: Int = 400) = StatusCallbackError(
- null,
- null,
- retrofit2.Response.error(
- "".toResponseBody(null),
- Response.Builder()
- .protocol(Protocol.HTTP_1_1)
- .message(message)
- .code(code)
- .request(Request.Builder().url("http://localhost/").build())
- .build()
- )
-)
-
-inline fun BroadcastChannel.receiveOnce(crossinline block: () -> Unit): T = runBlocking {
- val receiveChannel = openSubscription()
- block()
- val single = receiveChannel.receive()
- receiveChannel.cancel()
- single
-}
diff --git a/apps/teacher/build.gradle b/apps/teacher/build.gradle
index c7fc77f5ba..124a64779f 100644
--- a/apps/teacher/build.gradle
+++ b/apps/teacher/build.gradle
@@ -39,10 +39,9 @@ android {
defaultConfig {
minSdkVersion Versions.MIN_SDK
targetSdkVersion Versions.TARGET_SDK
- versionCode = 71
- versionName = '1.34.0'
+ versionCode = 72
+ versionName = '1.35.0'
vectorDrawables.useSupportLibrary = true
- multiDexEnabled true
testInstrumentationRunner 'com.instructure.teacher.ui.espresso.TeacherHiltTestRunner'
testInstrumentationRunnerArguments disableAnalytics: 'true'
@@ -88,6 +87,11 @@ android {
qa {
dimension "icecream"
buildConfigField "boolean", "IS_TESTING", "true"
+ buildConfigField "String", "PRONOUN_TEACHER_TEST_USER", "\"$pronounTestTeacher\""
+ buildConfigField "String", "PRONOUN_TEACHER_TEST_PASSWORD", "\"$pronounTestTeacherPassword\""
+
+ buildConfigField "String", "PUSH_NOTIFICATIONS_TEACHER_TEST_USER", "\"$pushNotificationsTestTeacher\""
+ buildConfigField "String", "PUSH_NOTIFICATIONS_TEACHER_TEST_PASSWORD", "\"$pushNotificationsTestTeacherPassword\""
}
}
@@ -107,12 +111,6 @@ android {
buildConfigField "String", "HEAP_APP_ID", "\"$heapStagingId\""
- buildConfigField "String", "PRONOUN_TEACHER_TEST_USER", "\"$pronounTestTeacher\""
- buildConfigField "String", "PRONOUN_TEACHER_TEST_PASSWORD", "\"$pronounTestTeacherPassword\""
-
- buildConfigField "String", "PUSH_NOTIFICATIONS_TEACHER_TEST_USER", "\"$pushNotificationsTestTeacher\""
- buildConfigField "String", "PUSH_NOTIFICATIONS_TEACHER_TEST_PASSWORD", "\"$pushNotificationsTestTeacherPassword\""
-
ext {
heapEnabled = true
}
@@ -276,7 +274,7 @@ dependencies {
implementation Libs.ANDROIDX_BROWSER
implementation Libs.ANDROIDX_CARDVIEW
implementation Libs.ANDROIDX_CONSTRAINT_LAYOUT
- implementation Libs.ANDROIDX_DESIGN
+ implementation Libs.MATERIAL_DESIGN
implementation Libs.ANDROIDX_PALETTE
implementation Libs.ANDROIDX_PERCENT
implementation Libs.ANDROIDX_ANNOTATION
@@ -299,7 +297,7 @@ dependencies {
implementation Libs.VIEW_MODEL
implementation Libs.LIVE_DATA
implementation Libs.VIEW_MODE_SAVED_STATE
- implementation Libs.FRAGMENT_KTX
+ implementation Libs.ANDROIDX_FRAGMENT_KTX
kapt Libs.LIFECYCLE_COMPILER
/* DI */
diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/QuizListPageTest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/QuizListPageTest.kt
index f57ccc6c44..2a0ad9c017 100644
--- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/QuizListPageTest.kt
+++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/QuizListPageTest.kt
@@ -55,10 +55,10 @@ class QuizListPageTest : TeacherTest() {
fun searchesQuizzes() {
val quizzes = getToQuizzesPage(quizCount = 3)
val searchQuiz = quizzes[2]
- quizListPage.assertQuizCount(quizzes.size + 1) // +1 to account for header
+ quizListPage.assertQuizCount(quizzes.size)
quizListPage.searchable.clickOnSearchButton()
quizListPage.searchable.typeToSearchBar(searchQuiz.title!!.take(searchQuiz.title!!.length / 2))
- quizListPage.assertQuizCount(2) // header + single search result
+ quizListPage.assertQuizCount(1)
quizListPage.assertHasQuiz(searchQuiz)
}
diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/QuizE2ETest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/QuizE2ETest.kt
index 2864daabc4..a4dac01980 100644
--- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/QuizE2ETest.kt
+++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/QuizE2ETest.kt
@@ -17,6 +17,7 @@
package com.instructure.teacher.ui.e2e
import android.util.Log
+import androidx.test.espresso.Espresso
import com.instructure.canvas.espresso.E2E
import com.instructure.canvas.espresso.FeatureCategory
import com.instructure.canvas.espresso.Priority
@@ -44,7 +45,7 @@ class QuizE2ETest: TeacherTest() {
@E2E
@Test
@TestMetaData(Priority.MANDATORY, FeatureCategory.QUIZZES, TestCategory.E2E)
- fun testQuizE2E() {
+ fun testQuizzesE2E() {
Log.d(PREPARATION_TAG, "Seeding data.")
val data = seedData(students = 1, teachers = 1, courses = 1)
@@ -63,20 +64,28 @@ class QuizE2ETest: TeacherTest() {
Log.d(STEP_TAG,"Assert that there is no quiz displayed on the page.")
quizListPage.assertDisplaysNoQuizzesView()
- Log.d(PREPARATION_TAG,"Seed a quiz for the '${course.name}' course. Also, seed a question into the quiz and publish it.")
- val testQuizList = seedQuizzes(courseId = course.id, withDescription = true, dueAt = 3.days.fromNow.iso8601, teacherToken = teacher.token, published = false)
+ Log.d(PREPARATION_TAG,"Seed two quizzes for the '${course.name}' course. Also, seed a question into both the quizzes and publish them.")
+ val testQuizList = seedQuizzes(courseId = course.id, quizzes = 2, withDescription = true, dueAt = 3.days.fromNow.iso8601, teacherToken = teacher.token, published = false)
seedQuizQuestion(courseId = course.id, quizId = testQuizList.quizList[0].id, teacherToken = teacher.token)
+ seedQuizQuestion(courseId = course.id, quizId = testQuizList.quizList[1].id, teacherToken = teacher.token)
- Log.d(STEP_TAG,"Refresh the page. Assert that the quiz is there and click on the previously seeded quiz: '${testQuizList.quizList[0].title}'.")
+ Log.d(STEP_TAG,"Refresh the page.")
quizListPage.refresh()
- quizListPage.clickQuiz(testQuizList.quizList[0].title)
- Log.d(STEP_TAG,"Assert that '${testQuizList.quizList[0].title}' quiz is 'Not Submitted' and it is unpublished.")
+ Log.d(ASSERTION_TAG, "Assert that both of the quizzes are displayed on the Quiz List Page so the number of quizzes is 2.")
+ quizListPage.assertQuizCount(2)
+
+ val firstQuiz = testQuizList.quizList[0]
+ val secondQuiz = testQuizList.quizList[1]
+ Log.d(ASSERTION_TAG, "Assert that the quiz is there and click on the previously seeded quiz: '${firstQuiz.title}'.")
+ quizListPage.clickQuiz(firstQuiz.title)
+
+ Log.d(STEP_TAG,"Assert that '${firstQuiz.title}' quiz is 'Not Submitted' and it is unpublished.")
quizDetailsPage.assertNotSubmitted()
quizDetailsPage.assertQuizUnpublished()
val newQuizTitle = "This is a new quiz"
- Log.d(STEP_TAG,"Open 'Edit' page and edit the '${testQuizList.quizList[0].title}' quiz's title to: '$newQuizTitle'.")
+ Log.d(STEP_TAG,"Open 'Edit' page and edit the '${firstQuiz.title}' quiz's title to: '$newQuizTitle'.")
quizDetailsPage.openEditPage()
editQuizDetailsPage.editQuizTitle(newQuizTitle)
@@ -92,12 +101,41 @@ class QuizE2ETest: TeacherTest() {
quizDetailsPage.refresh()
quizDetailsPage.assertQuizPublished()
- Log.d(PREPARATION_TAG,"Submit the '${testQuizList.quizList[0].title}' quiz.")
+ Log.d(PREPARATION_TAG,"Submit the '${firstQuiz.title}' quiz.")
seedQuizSubmission(courseId = course.id, quizId = testQuizList.quizList[0].id, studentToken = student.token)
Log.d(STEP_TAG,"Refresh the page. Assert that it needs grading because of the previous submission.")
quizListPage.refresh()
quizDetailsPage.assertNeedsGrading()
+
+ Log.d(STEP_TAG,"Click on Search button and type '$newQuizTitle' to the search input field.")
+ Espresso.pressBack()
+ quizListPage.searchable.clickOnSearchButton()
+ quizListPage.searchable.typeToSearchBar(newQuizTitle)
+
+ Log.d(STEP_TAG,"Assert that only the matching quiz, which is '$newQuizTitle' is displayed on the Quiz List Page.")
+ quizListPage.assertQuizCount(1)
+ quizListPage.assertHasQuiz(newQuizTitle)
+ quizListPage.assertQuizNotDisplayed(secondQuiz.title)
+
+ Log.d(STEP_TAG,"Clear search input field value and assert if both of the quizzes are displayed again on the Quiz List Page.")
+ quizListPage.searchable.clickOnClearSearchButton()
+ quizListPage.assertQuizCount(2)
+ quizListPage.assertHasQuiz(newQuizTitle)
+ quizListPage.assertHasQuiz(secondQuiz.title)
+
+ Log.d(STEP_TAG,"Type a search value to the search input field which does not much with any of the existing quizzes.")
+ quizListPage.searchable.typeToSearchBar("Non existing quiz")
+ Thread.sleep(1000) //We need this wait here to let make sure the search process has finished.
+
+ Log.d(STEP_TAG,"Assert that the empty view is displayed.")
+ quizListPage.assertDisplaysNoQuizzesView()
+
+ Log.d(STEP_TAG,"Clear search input field value and assert if both of the quizzes are displayed on the Quiz List Page.")
+ quizListPage.searchable.clickOnClearSearchButton()
+ quizListPage.assertQuizCount(2)
+ quizListPage.assertHasQuiz(newQuizTitle)
+ quizListPage.assertHasQuiz(secondQuiz.title)
}
}
\ No newline at end of file
diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/QuizListPage.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/QuizListPage.kt
index 1068d36295..5898120932 100644
--- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/QuizListPage.kt
+++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/QuizListPage.kt
@@ -17,6 +17,7 @@
package com.instructure.teacher.ui.pages
import com.instructure.canvasapi2.models.Quiz
+import com.instructure.espresso.DoesNotExistAssertion
import com.instructure.espresso.OnViewWithId
import com.instructure.espresso.RecyclerViewItemCountAssertion
import com.instructure.espresso.Searchable
@@ -25,11 +26,15 @@ 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.plus
+import com.instructure.espresso.page.waitForView
import com.instructure.espresso.page.waitForViewWithText
import com.instructure.espresso.page.withId
+import com.instructure.espresso.page.withText
import com.instructure.espresso.swipeDown
import com.instructure.espresso.waitForCheck
import com.instructure.teacher.R
+import org.hamcrest.Matchers.allOf
/**
* Represents the Quiz List Page.
@@ -82,6 +87,24 @@ class QuizListPage(val searchable: Searchable) : BasePage() {
waitForViewWithText(quiz.title!!).assertDisplayed()
}
+ /**
+ * Asserts the presence of a quiz on the page.
+ *
+ * @param quizTitle The quiz title to check.
+ */
+ fun assertHasQuiz(quizTitle: String) {
+ waitForView(withId(R.id.quizTitle) + withText(quizTitle)).assertDisplayed()
+ }
+
+ /**
+ * Asserts the non-existence of a quiz on the page.
+ *
+ * @param quizTitle The quiz title to check.
+ */
+ fun assertQuizNotDisplayed(quizTitle: String) {
+ onView(allOf(withText(quizTitle) + withId(R.id.quizTitle))).check(DoesNotExistAssertion(5))
+ }
+
/**
* Clicks on a quiz.
*
@@ -106,7 +129,7 @@ class QuizListPage(val searchable: Searchable) : BasePage() {
* @param count The expected count of quizzes.
*/
fun assertQuizCount(count: Int) {
- quizRecyclerView.waitForCheck(RecyclerViewItemCountAssertion(count))
+ quizRecyclerView.waitForCheck(RecyclerViewItemCountAssertion(count + 1)) // +1 needed because we don't want to count the 'Assignment Quizzes' group label.
}
/**
diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/utils/TeacherTest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/utils/TeacherTest.kt
index e7fdca880f..5e9ed54ad6 100644
--- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/utils/TeacherTest.kt
+++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/utils/TeacherTest.kt
@@ -143,7 +143,7 @@ abstract class TeacherTest : CanvasTest() {
val pageListPage = PageListPage(Searchable(R.id.search, R.id.search_src_text, R.id.search_close_btn))
val peopleListPage = PeopleListPage(Searchable(R.id.search, R.id.search_src_text, R.id.search_close_btn))
val quizDetailsPage = QuizDetailsPage(ModuleItemInteractions(R.id.moduleName, R.id.next, R.id.previous))
- val quizListPage = QuizListPage(Searchable(R.id.search, R.id.search_src_text, R.id.clearButton, R.id.backButton))
+ val quizListPage = QuizListPage(Searchable(R.id.search, R.id.search_src_text, R.id.search_close_btn, R.id.backButton))
val quizSubmissionListPage = QuizSubmissionListPage()
val speedGraderCommentsPage = SpeedGraderCommentsPage()
val speedGraderFilesPage = SpeedGraderFilesPage()
diff --git a/apps/teacher/src/main/java/com/instructure/teacher/activities/InitActivity.kt b/apps/teacher/src/main/java/com/instructure/teacher/activities/InitActivity.kt
index 790581e81c..04db1e8df1 100644
--- a/apps/teacher/src/main/java/com/instructure/teacher/activities/InitActivity.kt
+++ b/apps/teacher/src/main/java/com/instructure/teacher/activities/InitActivity.kt
@@ -300,13 +300,15 @@ class InitActivity : BasePresenterActivity {
RouteMatcher.route(this@InitActivity, Route(FileListFragment::class.java, ApiPrefs.user))
}
- R.id.navigationDrawerItem_gauge, R.id.navigationDrawerItem_arc -> {
+ R.id.navigationDrawerItem_gauge, R.id.navigationDrawerItem_arc, R.id.navigationDrawerItem_mastery -> {
val launchDefinition = v.tag as? LaunchDefinition ?: return@weave
- val user = ApiPrefs.user ?: return@weave
- val canvasContext = CanvasContext.currentUserContext(user)
- val title = getString(if (launchDefinition.isGauge) R.string.gauge else R.string.studio)
- val route = LtiLaunchFragment.makeBundle(
- canvasContext = canvasContext,
- url = launchDefinition.placements.globalNavigation.url,
- title = title,
- sessionLessLaunch = true
- )
- RouteMatcher.route(this@InitActivity, Route(LtiLaunchFragment::class.java, canvasContext, route))
+ launchLti(launchDefinition)
}
R.id.navigationDrawerItem_help -> HelpDialogFragment.show(this@InitActivity)
R.id.navigationDrawerItem_changeUser -> TeacherLogoutTask(LogoutTask.Type.SWITCH_USERS).execute()
@@ -401,6 +394,19 @@ class InitActivity : BasePresenterActivity?) = with(navigationDrawerBinding) {
val arcLaunchDefinition = launchDefinitions?.firstOrNull { it.domain == LaunchDefinition.STUDIO_DOMAIN }
val gaugeLaunchDefinition = launchDefinitions?.firstOrNull { it.domain == LaunchDefinition.GAUGE_DOMAIN }
+ val masteryLaunchDefinition = launchDefinitions?.firstOrNull { it.domain == LaunchDefinition.MASTERY_DOMAIN }
navigationDrawerItemArc.setVisible(arcLaunchDefinition != null)
navigationDrawerItemArc.tag = arcLaunchDefinition
navigationDrawerItemGauge.setVisible(gaugeLaunchDefinition != null)
navigationDrawerItemGauge.tag = gaugeLaunchDefinition
+
+ navigationDrawerItemMastery.setVisible(masteryLaunchDefinition != null)
+ navigationDrawerItemMastery.tag = masteryLaunchDefinition
}
override fun onStartMasquerading(domain: String, userId: Long) {
diff --git a/apps/teacher/src/main/java/com/instructure/teacher/binders/MessageBinder.kt b/apps/teacher/src/main/java/com/instructure/teacher/binders/MessageBinder.kt
index 86158403e8..c0fb520e82 100644
--- a/apps/teacher/src/main/java/com/instructure/teacher/binders/MessageBinder.kt
+++ b/apps/teacher/src/main/java/com/instructure/teacher/binders/MessageBinder.kt
@@ -38,7 +38,7 @@ import com.instructure.teacher.holders.MessageHolder
import com.instructure.teacher.interfaces.MessageAdapterCallback
import com.instructure.teacher.utils.linkifyTextView
import java.text.SimpleDateFormat
-import java.util.*
+import java.util.Locale
object MessageBinder {
fun bind(
@@ -93,8 +93,8 @@ object MessageBinder {
}
val popup = PopupMenu(v.context, v, Gravity.START)
val menu = popup.menu
- for (action in actions) {
- menu.add(0, action.ordinal, action.ordinal, action.labelResId)
+ actions.forEachIndexed { index, action ->
+ menu.add(0, index, index, action.labelResId)
}
// Add click listener
diff --git a/apps/teacher/src/main/java/com/instructure/teacher/di/AssignmentDetailsModule.kt b/apps/teacher/src/main/java/com/instructure/teacher/di/AssignmentDetailsModule.kt
new file mode 100644
index 0000000000..f84cc60f27
--- /dev/null
+++ b/apps/teacher/src/main/java/com/instructure/teacher/di/AssignmentDetailsModule.kt
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2024 - 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.teacher.di
+
+import com.instructure.pandautils.features.assignments.details.AssignmentDetailsBehaviour
+import com.instructure.pandautils.features.assignments.details.AssignmentDetailsColorProvider
+import com.instructure.pandautils.features.assignments.details.AssignmentDetailsRepository
+import com.instructure.pandautils.features.assignments.details.AssignmentDetailsRouter
+import com.instructure.pandautils.features.assignments.details.AssignmentDetailsSubmissionHandler
+import com.instructure.pandautils.receivers.alarm.AlarmReceiverNotificationHandler
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.android.components.FragmentComponent
+import dagger.hilt.android.components.ViewModelComponent
+import dagger.hilt.components.SingletonComponent
+
+@Module
+@InstallIn(FragmentComponent::class)
+class AssignmentDetailsFragmentModule() {
+ @Provides
+ fun provideAssignmentDetailsRouter(): AssignmentDetailsRouter {
+ throw NotImplementedError()
+ }
+
+ @Provides
+ fun provideAssignmentDetailsBehaviour(): AssignmentDetailsBehaviour {
+ throw NotImplementedError()
+ }
+}
+
+@Module
+@InstallIn(ViewModelComponent::class)
+class AssignmentDetailsModule {
+ @Provides
+ fun provideAssignmentDetailsRepository(): AssignmentDetailsRepository {
+ throw NotImplementedError()
+ }
+
+ @Provides
+ fun provideAssignmentDetailsSubmissionHandler(): AssignmentDetailsSubmissionHandler {
+ throw NotImplementedError()
+ }
+
+ @Provides
+ fun provideAssignmentDetailsColorProvider(): AssignmentDetailsColorProvider {
+ throw NotImplementedError()
+ }
+}
+
+@Module
+@InstallIn(SingletonComponent::class)
+class AssignmentDetailsSingletonModule {
+ @Provides
+ fun provideAssignmentDetailsNotificationHandler(): AlarmReceiverNotificationHandler {
+ throw NotImplementedError()
+ }
+}
\ No newline at end of file
diff --git a/apps/teacher/src/main/java/com/instructure/teacher/features/calendar/TeacherCalendarRepository.kt b/apps/teacher/src/main/java/com/instructure/teacher/features/calendar/TeacherCalendarRepository.kt
index 800fd7220d..934c54b85f 100644
--- a/apps/teacher/src/main/java/com/instructure/teacher/features/calendar/TeacherCalendarRepository.kt
+++ b/apps/teacher/src/main/java/com/instructure/teacher/features/calendar/TeacherCalendarRepository.kt
@@ -22,14 +22,12 @@ import com.instructure.canvasapi2.apis.FeaturesAPI
import com.instructure.canvasapi2.apis.PlannerAPI
import com.instructure.canvasapi2.builders.RestParams
import com.instructure.canvasapi2.models.CanvasContext
-import com.instructure.canvasapi2.models.Plannable
import com.instructure.canvasapi2.models.PlannableType
import com.instructure.canvasapi2.models.PlannerItem
import com.instructure.canvasapi2.models.toPlannerItems
import com.instructure.canvasapi2.utils.ApiPrefs
import com.instructure.canvasapi2.utils.DataResult
import com.instructure.canvasapi2.utils.depaginate
-import com.instructure.canvasapi2.utils.toDate
import com.instructure.pandautils.features.calendar.CalendarRepository
import com.instructure.pandautils.room.calendar.daos.CalendarFilterDao
import com.instructure.pandautils.room.calendar.entities.CalendarFilterEntity
@@ -45,9 +43,7 @@ class TeacherCalendarRepository(
private val apiPrefs: ApiPrefs,
private val featuresApi: FeaturesAPI.FeaturesInterface,
private val calendarFilterDao: CalendarFilterDao
-) : CalendarRepository {
-
- private var canvasContexts: List = emptyList()
+) : CalendarRepository() {
override suspend fun getPlannerItems(
startDate: String,
@@ -70,7 +66,10 @@ class TeacherCalendarRepository(
restParams
).depaginate {
calendarEventApi.next(it, restParams)
- }.dataOrThrow.toPlannerItems(PlannableType.CALENDAR_EVENT)
+ }.dataOrThrow
+ .filterNot { it.isHidden }
+ .toPlannerItems(PlannableType.CALENDAR_EVENT)
+ .mapContextName()
}
val calendarAssignments = async {
@@ -83,7 +82,10 @@ class TeacherCalendarRepository(
restParams
).depaginate {
calendarEventApi.next(it, restParams)
- }.dataOrThrow.toPlannerItems(PlannableType.ASSIGNMENT)
+ }.dataOrThrow
+ .filterNot { it.isHidden }
+ .toPlannerItems(PlannableType.ASSIGNMENT)
+ .mapContextName()
}
val plannerNotes = async {
@@ -125,32 +127,6 @@ class TeacherCalendarRepository(
}
}
- private fun List.toPlannerItems(): List {
- return mapNotNull { plannable ->
- val contextType = if (plannable.courseId != null) CanvasContext.Type.COURSE.apiString else CanvasContext.Type.USER.apiString
- val contextName = if (plannable.courseId != null) canvasContexts.find { it.id == plannable.courseId }?.name else null
- val plannableDate = plannable.todoDate.toDate()
- if (plannableDate == null) {
- null
- } else {
- PlannerItem(
- courseId = plannable.courseId,
- groupId = plannable.groupId,
- userId = plannable.userId,
- contextType = contextType,
- contextName = contextName,
- plannableType = PlannableType.PLANNER_NOTE,
- plannable = plannable,
- plannableDate = plannableDate,
- htmlUrl = null,
- submissionState = null,
- newActivity = false,
- plannerOverride = null
- )
- }
- }
- }
-
override suspend fun getCalendarFilters(): CalendarFilterEntity? {
return calendarFilterDao.findByUserIdAndDomain(apiPrefs.user?.id.orDefault(), apiPrefs.fullDomain)
}
diff --git a/apps/teacher/src/main/java/com/instructure/teacher/features/inbox/list/TeacherInboxRouter.kt b/apps/teacher/src/main/java/com/instructure/teacher/features/inbox/list/TeacherInboxRouter.kt
index 38927081e4..a216d2bcae 100644
--- a/apps/teacher/src/main/java/com/instructure/teacher/features/inbox/list/TeacherInboxRouter.kt
+++ b/apps/teacher/src/main/java/com/instructure/teacher/features/inbox/list/TeacherInboxRouter.kt
@@ -63,7 +63,7 @@ class TeacherInboxRouter(private val activity: FragmentActivity, private val fra
}
}
- override fun routeToNewMessage() {
+ override fun routeToNewMessage(activity: FragmentActivity) {
val args = AddMessageFragment.createBundle()
RouteMatcher.route(activity, Route(AddMessageFragment::class.java, null, args))
}
diff --git a/apps/teacher/src/main/java/com/instructure/teacher/navigation/TeacherWebViewRouter.kt b/apps/teacher/src/main/java/com/instructure/teacher/navigation/TeacherWebViewRouter.kt
index 29c8bd0c74..3540d69515 100644
--- a/apps/teacher/src/main/java/com/instructure/teacher/navigation/TeacherWebViewRouter.kt
+++ b/apps/teacher/src/main/java/com/instructure/teacher/navigation/TeacherWebViewRouter.kt
@@ -16,6 +16,7 @@
*/
package com.instructure.teacher.navigation
+import android.os.Bundle
import androidx.fragment.app.FragmentActivity
import com.instructure.canvasapi2.models.CanvasContext
import com.instructure.canvasapi2.utils.ApiPrefs
@@ -33,7 +34,7 @@ class TeacherWebViewRouter(val activity: FragmentActivity) : WebViewRouter {
return RouteMatcher.canRouteInternally(activity, url, ApiPrefs.domain, routeIfPossible = routeIfPossible)
}
- override fun routeInternally(url: String) {
+ override fun routeInternally(url: String, extras: Bundle?) {
RouteMatcher.canRouteInternally(activity, url, ApiPrefs.domain, routeIfPossible = true)
}
diff --git a/apps/teacher/src/main/java/com/instructure/teacher/presenters/CourseBrowserPresenter.kt b/apps/teacher/src/main/java/com/instructure/teacher/presenters/CourseBrowserPresenter.kt
index 8009430520..60dafefbf6 100644
--- a/apps/teacher/src/main/java/com/instructure/teacher/presenters/CourseBrowserPresenter.kt
+++ b/apps/teacher/src/main/java/com/instructure/teacher/presenters/CourseBrowserPresenter.kt
@@ -65,7 +65,7 @@ class CourseBrowserPresenter(val canvasContext: CanvasContext, val filter: (Tab,
var attendanceId: Long = 0
launchDefinitions.forEach {
- val ltiDefinitionUrl = it.placements.courseNavigation?.url
+ val ltiDefinitionUrl = it.placements?.courseNavigation?.url
if (ltiDefinitionUrl != null && (
ltiDefinitionUrl.contains(AttendanceAPI.BASE_DOMAIN) ||
ltiDefinitionUrl.contains(AttendanceAPI.BASE_TEST_DOMAIN))) {
diff --git a/apps/teacher/src/main/java/com/instructure/teacher/presenters/InitActivityPresenter.kt b/apps/teacher/src/main/java/com/instructure/teacher/presenters/InitActivityPresenter.kt
index 85c676f33b..66a5d60edc 100644
--- a/apps/teacher/src/main/java/com/instructure/teacher/presenters/InitActivityPresenter.kt
+++ b/apps/teacher/src/main/java/com/instructure/teacher/presenters/InitActivityPresenter.kt
@@ -62,10 +62,9 @@ class InitActivityPresenter : Presenter {
val count = todos.sumOf { it.needsGradingCount }
view?.updateTodoCount(count)
- val launchDefinitions = awaitApi?> { LaunchDefinitionsManager.getLaunchDefinitions(it, false) }
+ val launchDefinitions = awaitApi { LaunchDefinitionsManager.getLaunchDefinitions(it, false) }
launchDefinitions?.let {
- val definitions = launchDefinitions.filter { it.domain == LaunchDefinition.STUDIO_DOMAIN || it.domain == LaunchDefinition.GAUGE_DOMAIN }
- view?.gotLaunchDefinitions(definitions)
+ view?.gotLaunchDefinitions(it)
}
val inboxUnreadCount = awaitApi { UnreadCountManager.getUnreadConversationCount(it, true) }
diff --git a/apps/teacher/src/main/java/com/instructure/teacher/utils/BaseAppManager.kt b/apps/teacher/src/main/java/com/instructure/teacher/utils/BaseAppManager.kt
index f3a6b893e8..f2bd23c660 100644
--- a/apps/teacher/src/main/java/com/instructure/teacher/utils/BaseAppManager.kt
+++ b/apps/teacher/src/main/java/com/instructure/teacher/utils/BaseAppManager.kt
@@ -42,6 +42,7 @@ import com.instructure.teacher.tasks.TeacherLogoutTask
import com.pspdfkit.PSPDFKit
import com.pspdfkit.exceptions.InvalidPSPDFKitLicenseException
import com.pspdfkit.exceptions.PSPDFKitInitializationFailedException
+import com.pspdfkit.initialization.InitializationOptions
abstract class BaseAppManager : com.instructure.canvasapi2.AppManager() {
@@ -73,7 +74,7 @@ abstract class BaseAppManager : com.instructure.canvasapi2.AppManager() {
ColorKeeper.defaultColor = getColorCompat(R.color.textDarkest)
try {
- PSPDFKit.initialize(this, BuildConfig.PSPDFKIT_LICENSE_KEY)
+ PSPDFKit.initialize(this, InitializationOptions(licenseKey = BuildConfig.PSPDFKIT_LICENSE_KEY))
} catch (e: PSPDFKitInitializationFailedException) {
Logger.e("Current device is not compatible with PSPDFKIT!")
} catch (e: InvalidPSPDFKitLicenseException) {
diff --git a/apps/teacher/src/main/res/layout/navigation_drawer.xml b/apps/teacher/src/main/res/layout/navigation_drawer.xml
index de3f459f15..acb4776fbe 100644
--- a/apps/teacher/src/main/res/layout/navigation_drawer.xml
+++ b/apps/teacher/src/main/res/layout/navigation_drawer.xml
@@ -150,6 +150,26 @@
+
+
+
+
+
+
+
+
matchesEffects(vararg effects: F): Matcher> {
fun matchesFirstEffects(vararg effects: F): Matcher> {
return FirstMatchers.hasEffects(Matchers.containsInAnyOrder(*effects))
}
-
-fun createError(message: String = "Error", code: Int = 400) =
- StatusCallbackError(
- null,
- null,
- retrofit2.Response.error(
- "".toResponseBody(null),
- Response.Builder()
- .protocol(Protocol.HTTP_1_1)
- .message(message)
- .code(code)
- .request(Request.Builder().url("http://localhost/").build())
- .build()
- )
- )
diff --git a/automation/espresso/build.gradle b/automation/espresso/build.gradle
index 65d7d3dd98..18a5f54cb2 100644
--- a/automation/espresso/build.gradle
+++ b/automation/espresso/build.gradle
@@ -113,7 +113,7 @@ dependencies {
exclude module: 'support-annotations'
}
- implementation Libs.ANDROIDX_DESIGN
+ implementation Libs.MATERIAL_DESIGN
implementation Libs.ANDROIDX_SWIPE_REFRESH_LAYOUT
implementation Libs.GSON
diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/interaction/CreateUpdateEventInteractionTest.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/interaction/CreateUpdateEventInteractionTest.kt
index be83158f72..a7f9cbaede 100644
--- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/interaction/CreateUpdateEventInteractionTest.kt
+++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/interaction/CreateUpdateEventInteractionTest.kt
@@ -18,6 +18,7 @@
package com.instructure.canvas.espresso.common.interaction
import com.instructure.canvas.espresso.CanvasComposeTest
+import com.instructure.canvas.espresso.Stub
import com.instructure.canvas.espresso.common.pages.compose.CalendarEventCreateEditPage
import com.instructure.canvas.espresso.common.pages.compose.CalendarEventDetailsPage
import com.instructure.canvas.espresso.common.pages.compose.CalendarScreenPage
@@ -220,6 +221,7 @@ abstract class CreateUpdateEventInteractionTest : CanvasComposeTest() {
}
@Test
+ @Stub("This test is flaky, depends on the time of day")
fun assertUpdatedFrom() {
val data = initData()
val user = getLoggedInUser()
@@ -248,7 +250,7 @@ abstract class CreateUpdateEventInteractionTest : CanvasComposeTest() {
calendarEventDetailsPage.assertEventDateContains(expectedTime!!)
}
- @Test
+ @Stub("This test is flaky, depends on the time of day")
fun assertUpdatedTo() {
val data = initData()
val user = getLoggedInUser()
diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/interaction/CreateUpdateToDoInteractionTest.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/interaction/CreateUpdateToDoInteractionTest.kt
index b8cde1f491..dd233e220f 100644
--- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/interaction/CreateUpdateToDoInteractionTest.kt
+++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/interaction/CreateUpdateToDoInteractionTest.kt
@@ -18,6 +18,7 @@
package com.instructure.canvas.espresso.common.interaction
import com.instructure.canvas.espresso.CanvasComposeTest
+import com.instructure.canvas.espresso.Stub
import com.instructure.canvas.espresso.common.pages.compose.CalendarScreenPage
import com.instructure.canvas.espresso.common.pages.compose.CalendarToDoCreateUpdatePage
import com.instructure.canvas.espresso.common.pages.compose.CalendarToDoDetailsPage
@@ -172,6 +173,7 @@ abstract class CreateUpdateToDoInteractionTest : CanvasComposeTest() {
}
@Test
+ @Stub("This test is flaky, depends on the time of day")
fun assertUpdatedTime() {
val data = initData()
val user = getLoggedInUser()
diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/interaction/InboxComposeInteractionTest.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/interaction/InboxComposeInteractionTest.kt
index b9563d43f1..e487fa5ec8 100644
--- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/interaction/InboxComposeInteractionTest.kt
+++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/interaction/InboxComposeInteractionTest.kt
@@ -55,8 +55,7 @@ abstract class InboxComposeInteractionTest: CanvasComposeTest() {
goToInboxCompose(data)
composeTestRule.waitForIdle()
- inboxComposePage.pressCourseSelector()
- selectContextPage.selectContext(getFirstCourse().name)
+ selectContext()
inboxComposePage.pressAddRecipient()
recipientPickerPage.pressLabel("Teachers")
recipientPickerPage.pressLabel(getTeachers().first().name)
@@ -83,8 +82,7 @@ abstract class InboxComposeInteractionTest: CanvasComposeTest() {
goToInboxCompose(data)
composeTestRule.waitForIdle()
- inboxComposePage.pressCourseSelector()
- selectContextPage.selectContext(getFirstCourse().name)
+ selectContext()
inboxComposePage.pressAddRecipient()
recipientPickerPage.pressLabel("Teachers")
recipientPickerPage.pressLabel(getTeachers().first().name)
@@ -120,8 +118,7 @@ abstract class InboxComposeInteractionTest: CanvasComposeTest() {
goToInboxCompose(data)
composeTestRule.waitForIdle()
- inboxComposePage.pressCourseSelector()
- selectContextPage.selectContext(getFirstCourse().name)
+ selectContext()
inboxComposePage.pressAddRecipient()
recipientPickerPage.pressLabel("Teachers")
recipientPickerPage.pressLabel(getTeachers().first().name)
@@ -160,8 +157,7 @@ abstract class InboxComposeInteractionTest: CanvasComposeTest() {
goToInboxCompose(data)
composeTestRule.waitForIdle()
- inboxComposePage.pressCourseSelector()
- selectContextPage.selectContext(getFirstCourse().name)
+ selectContext()
inboxComposePage.pressAddRecipient()
recipientPickerPage.pressLabel("All in ${data.courses.values.first().name}")
recipientPickerPage.pressDone()
@@ -198,8 +194,7 @@ abstract class InboxComposeInteractionTest: CanvasComposeTest() {
goToInboxCompose(data)
composeTestRule.waitForIdle()
- inboxComposePage.pressCourseSelector()
- selectContextPage.selectContext(getFirstCourse().name)
+ selectContext()
inboxComposePage.pressAddRecipient()
recipientPickerPage.pressLabel("Teachers")
recipientPickerPage.pressLabel("All in Teachers")
@@ -230,8 +225,7 @@ abstract class InboxComposeInteractionTest: CanvasComposeTest() {
goToInboxCompose(data)
composeTestRule.waitForIdle()
- inboxComposePage.pressCourseSelector()
- selectContextPage.selectContext(getFirstCourse().name)
+ selectContext()
inboxComposePage.pressAddRecipient()
recipientPickerPage.pressLabel("Teachers")
recipientPickerPage.pressLabel(getTeachers().first().name)
@@ -258,9 +252,7 @@ abstract class InboxComposeInteractionTest: CanvasComposeTest() {
goToInboxCompose(data)
composeTestRule.waitForIdle()
- inboxComposePage.pressCourseSelector()
-
- selectContextPage.selectContext(getFirstCourse().name)
+ selectContext()
inboxComposePage.assertContextSelected(getFirstCourse().name)
}
@@ -280,8 +272,7 @@ abstract class InboxComposeInteractionTest: CanvasComposeTest() {
goToInboxCompose(data)
composeTestRule.waitForIdle()
- inboxComposePage.pressCourseSelector()
- selectContextPage.selectContext(getFirstCourse().name)
+ selectContext()
inboxComposePage.pressAddRecipient()
recipientPickerPage.pressLabel("Teachers")
recipientPickerPage.pressLabel(getTeachers().first().name)
@@ -375,4 +366,9 @@ abstract class InboxComposeInteractionTest: CanvasComposeTest() {
abstract fun getFirstCourse(): Course
abstract fun getSentConversation(): Conversation?
+
+ open fun selectContext() {
+ inboxComposePage.pressCourseSelector()
+ selectContextPage.selectContext(getFirstCourse().name)
+ }
}
\ No newline at end of file
diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/AssignmentDetailsPage.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/pages/AssignmentDetailsPage.kt
similarity index 86%
rename from apps/student/src/androidTest/java/com/instructure/student/ui/pages/AssignmentDetailsPage.kt
rename to automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/pages/AssignmentDetailsPage.kt
index 91b0f464ef..9fba6f8e6a 100644
--- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/AssignmentDetailsPage.kt
+++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/pages/AssignmentDetailsPage.kt
@@ -14,11 +14,10 @@
* limitations under the License.
*
*/
-package com.instructure.student.ui.pages
+package com.instructure.canvas.espresso.common.pages
import android.view.View
import android.widget.ScrollView
-import androidx.appcompat.widget.AppCompatButton
import androidx.test.espresso.Espresso
import androidx.test.espresso.Espresso.onData
import androidx.test.espresso.UiController
@@ -37,14 +36,12 @@ import com.instructure.canvas.espresso.containsTextCaseInsensitive
import com.instructure.canvas.espresso.stringContainsTextCaseInsensitive
import com.instructure.canvas.espresso.waitForMatcherWithSleeps
import com.instructure.canvasapi2.models.Assignment
-import com.instructure.dataseeding.model.SubmissionType
import com.instructure.espresso.ModuleItemInteractions
import com.instructure.espresso.OnViewWithId
import com.instructure.espresso.assertContainsText
import com.instructure.espresso.assertDisplayed
import com.instructure.espresso.assertHasText
import com.instructure.espresso.assertNotDisplayed
-import com.instructure.espresso.clearText
import com.instructure.espresso.click
import com.instructure.espresso.page.BasePage
import com.instructure.espresso.page.onView
@@ -61,7 +58,7 @@ import com.instructure.espresso.swipeDown
import com.instructure.espresso.swipeUp
import com.instructure.espresso.typeText
import com.instructure.espresso.waitForCheck
-import com.instructure.student.R
+import com.instructure.pandautils.R
import org.hamcrest.Matcher
import org.hamcrest.Matchers.allOf
import org.hamcrest.Matchers.anything
@@ -194,15 +191,6 @@ open class AssignmentDetailsPage(val moduleItemInteractions: ModuleItemInteracti
waitForMatcherWithSleeps(withId(R.id.contentWebView), timeout = 30000, pollInterval = 1000).scrollTo()
}
- fun addBookmark(bookmarkName: String) {
- openOverflowMenu()
- Espresso.onView(withText("Add Bookmark")).click()
- Espresso.onView(withId(R.id.bookmarkEditText)).clearText()
- Espresso.onView(withId(R.id.bookmarkEditText)).typeText(bookmarkName)
- if(CanvasTest.isLandscapeDevice()) Espresso.pressBack()
- Espresso.onView(allOf(isAssignableFrom(AppCompatButton::class.java), containsTextCaseInsensitive("Save"))).click()
- }
-
fun openOverflowMenu() {
Espresso.onView(
allOf(
@@ -239,19 +227,6 @@ open class AssignmentDetailsPage(val moduleItemInteractions: ModuleItemInteracti
waitForView(allOf(withId(R.id.attemptDate), withAncestor(withId(R.id.attemptSpinner)))).assertDisplayed()
}
- fun selectSubmissionType(submissionType: SubmissionType) {
- val viewMatcher = when (submissionType) {
- SubmissionType.ONLINE_TEXT_ENTRY -> withId(R.id.submissionEntryText)
- SubmissionType.ONLINE_UPLOAD -> withId(R.id.submissionEntryFile)
- SubmissionType.ONLINE_URL -> withId(R.id.submissionEntryWebsite)
- SubmissionType.MEDIA_RECORDING -> withId(R.id.submissionEntryMedia)
-
- else -> {withId(R.id.submissionEntryText)}
- }
-
- onView(viewMatcher).click()
- }
-
fun assertSubmissionTypeDisplayed(submissionType: String) {
onView(withText(submissionType) + withAncestor(R.id.customPanel)).assertDisplayed()
}
@@ -319,13 +294,6 @@ open class AssignmentDetailsPage(val moduleItemInteractions: ModuleItemInteracti
onView(withText(R.string.done)).click()
}
- //OfflineMethod
- fun assertDetailsNotAvailableOffline() {
- onView(withId(R.id.notAvailableIcon) + withAncestor(R.id.moduleProgressionPage)).assertDisplayed()
- onView(withId(R.id.title) + withText(R.string.notAvailableOfflineScreenTitle) + withParent(R.id.textViews) + withAncestor(R.id.moduleProgressionPage)).assertDisplayed()
- onView(withId(R.id.description) + withText(R.string.notAvailableOfflineDescriptionForTabs) + withParent(R.id.textViews) + withAncestor(R.id.moduleProgressionPage)).assertDisplayed()
- }
-
//OfflineMethod
fun assertSubmitButtonDisabled() {
onView(withId(R.id.submitButton)).check(matches(ViewMatchers.isNotEnabled()))
diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/pages/compose/InboxComposePage.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/pages/compose/InboxComposePage.kt
index a95bc55b8c..7892072d00 100644
--- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/pages/compose/InboxComposePage.kt
+++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/pages/compose/InboxComposePage.kt
@@ -29,6 +29,8 @@ import androidx.compose.ui.test.isDisplayed
import androidx.compose.ui.test.isEnabled
import androidx.compose.ui.test.isNotEnabled
import androidx.compose.ui.test.junit4.ComposeTestRule
+import androidx.compose.ui.test.onAllNodesWithContentDescription
+import androidx.compose.ui.test.onFirst
import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.onNodeWithText
@@ -57,7 +59,7 @@ class InboxComposePage(private val composeTestRule: ComposeTestRule) {
fun assertContextSelected(contextName: String) {
composeTestRule.waitForIdle()
- composeTestRule.onNodeWithText(contextName).assertIsDisplayed()
+ composeTestRule.onNode(hasText(contextName).and(isNotEnabled())).assertIsDisplayed()
}
fun assertRecipientSelected(recipientName: String) {
@@ -166,4 +168,17 @@ class InboxComposePage(private val composeTestRule: ComposeTestRule) {
composeTestRule.waitForIdle()
composeTestRule.onNodeWithTag("switch").performClick()
}
+
+ fun isRecipientsLoading(): Boolean {
+ composeTestRule.waitForIdle()
+ return composeTestRule.onNodeWithTag("Loading").isDisplayed()
+ }
+
+ fun removeAllRecipients() {
+ composeTestRule.waitForIdle()
+ val nodes = composeTestRule.onAllNodesWithContentDescription("Remove Recipient")
+ if (nodes.fetchSemanticsNodes().isNotEmpty()) {
+ nodes.onFirst().performClick()
+ }
+ }
}
\ No newline at end of file
diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/endpoints/AssignmentEndpoints.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/endpoints/AssignmentEndpoints.kt
index 71c00dc613..114413018a 100644
--- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/endpoints/AssignmentEndpoints.kt
+++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/endpoints/AssignmentEndpoints.kt
@@ -25,6 +25,7 @@ import com.instructure.canvasapi2.models.Assignment
import com.instructure.canvasapi2.models.AssignmentGroup
import com.instructure.canvasapi2.models.GradeableStudent
import com.instructure.canvasapi2.models.ObserveeAssignment
+import com.instructure.canvasapi2.models.Submission
import com.instructure.canvasapi2.models.ObserveeAssignmentGroup
import com.instructure.canvasapi2.models.SubmissionSummary
import okio.Buffer
@@ -63,7 +64,11 @@ object AssignmentEndpoint : Endpoint(
val assignment = data.assignments[pathVars.assignmentId]
if (assignment != null) {
- request.successResponse(assignment)
+ if (request.url.queryParameterValues("include[]").contains("observed_users")) {
+ request.successResponse(assignment.toObserveeAssignment())
+ } else {
+ request.successResponse(assignment)
+ }
} else {
request.unauthorizedResponse()
}
@@ -215,4 +220,4 @@ private fun Assignment.toObserveeAssignment() = ObserveeAssignment(
anonymousGrading = anonymousGrading,
allowedAttempts = allowedAttempts,
isStudioEnabled = isStudioEnabled
-)
+)
\ No newline at end of file
diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts
index eda82295b4..f88e2a00dc 100644
--- a/buildSrc/build.gradle.kts
+++ b/buildSrc/build.gradle.kts
@@ -20,13 +20,15 @@ repositories {
}
val agpVersion= "7.4.2"
+val kotlinVersion = "2.0.21"
dependencies {
implementation("com.android.tools.build:gradle:$agpVersion")
implementation("com.android.tools.build:gradle-api:$agpVersion")
implementation("org.javassist:javassist:3.24.1-GA")
- implementation("com.google.code.gson:gson:2.8.8")
- implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.25")
+ implementation("com.google.code.gson:gson:2.10.1")
+ implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion")
+ implementation("org.jetbrains.kotlin:compose-compiler-gradle-plugin:$kotlinVersion")
}
plugins {
diff --git a/buildSrc/src/main/java/GlobalDependencies.kt b/buildSrc/src/main/java/GlobalDependencies.kt
index a87e768c26..59529bd190 100644
--- a/buildSrc/src/main/java/GlobalDependencies.kt
+++ b/buildSrc/src/main/java/GlobalDependencies.kt
@@ -12,35 +12,35 @@ object Versions {
/* Testing */
const val JUNIT = "4.13.2"
- const val ROBOLECTRIC = "4.11.1"
+ const val ROBOLECTRIC = "4.13"
const val JACOCO_ANDROID = "0.1.5"
/* Kotlin */
- const val KOTLIN = "1.9.25"
- const val KOTLIN_COROUTINES = "1.6.4"
- const val KOTLIN_COMPOSE_COMPILER_VERSION = "1.5.15"
+ const val KOTLIN = "2.0.21"
+ const val KOTLIN_COROUTINES = "1.9.0"
/* Google, Play Services */
- const val GOOGLE_SERVICES = "4.3.15"
+ const val GOOGLE_SERVICES = "4.4.2"
/* Others */
const val APOLLO = "2.5.14" // There is already a brand new version, Apollo 3, that requires lots of migration
- const val PSPDFKIT = "8.9.1"
+ const val PSPDFKIT = "2024.3.1"
const val PHOTO_VIEW = "2.3.0"
const val MOBIUS = "1.2.1"
- const val HILT = "2.49"
- const val HILT_ANDROIDX = "1.1.0"
- const val LIFECYCLE = "2.6.2"
- const val FRAGMENT = "1.6.2"
- const val WORK_MANAGER = "2.9.0"
+ const val HILT = "2.52"
+ const val HILT_ANDROIDX = "1.2.0"
+ const val LIFECYCLE = "2.8.6"
+ const val FRAGMENT = "1.8.4"
+ const val WORK_MANAGER = "2.9.1"
const val GLIDE_VERSION = "4.16.0"
- const val RETROFIT = "2.9.0"
+ const val RETROFIT = "2.11.0"
const val OKHTTP = "4.12.0"
const val HEAP = "1.10.6"
const val ROOM = "2.6.1"
const val HAMCREST = "2.2"
- const val NAVIGATION = "2.7.7"
- const val MEDIA3 = "1.3.1"
+ const val NAVIGATION = "2.8.3"
+ const val MEDIA3 = "1.4.1"
+ const val DATASTORE = "1.1.1"
}
object Libs {
@@ -55,38 +55,39 @@ object Libs {
const val APOLLO_ANDROID_SUPPORT = "com.apollographql.apollo:apollo-android-support:${Versions.APOLLO}"
const val APOLLO_HTTP_CACHE = "com.apollographql.apollo:apollo-http-cache:${Versions.APOLLO}"
- /* Support Libs */
- const val ANDROIDX_ANNOTATION = "androidx.annotation:annotation:1.7.0"
- const val ANDROIDX_APPCOMPAT = "androidx.appcompat:appcompat:1.6.1"
- const val ANDROIDX_BROWSER = "androidx.browser:browser:1.7.0"
+ /* Androidx libraries */
+ const val ANDROIDX_ANNOTATION = "androidx.annotation:annotation:1.9.0"
+ const val ANDROIDX_APPCOMPAT = "androidx.appcompat:appcompat:1.7.0"
+ const val ANDROIDX_BROWSER = "androidx.browser:browser:1.8.0"
const val ANDROIDX_CARDVIEW = "androidx.cardview:cardview:1.0.0"
const val ANDROIDX_CONSTRAINT_LAYOUT = "androidx.constraintlayout:constraintlayout:2.1.4"
- const val ANDROIDX_DESIGN = "com.google.android.material:material:1.10.0"
- const val ANDROIDX_EXIF = "androidx.exifinterface:exifinterface:1.3.6"
+ const val ANDROIDX_EXIF = "androidx.exifinterface:exifinterface:1.3.7"
const val ANDROIDX_FRAGMENT = "androidx.fragment:fragment:${Versions.FRAGMENT}"
- const val FRAGMENT_KTX = "androidx.fragment:fragment-ktx:${Versions.FRAGMENT}"
+ const val ANDROIDX_FRAGMENT_KTX = "androidx.fragment:fragment-ktx:${Versions.FRAGMENT}"
const val ANDROIDX_PALETTE = "androidx.palette:palette:1.0.0"
const val ANDROIDX_PERCENT = "androidx.percentlayout:percentlayout:1.0.0"
const val ANDROIDX_RECYCLERVIEW = "androidx.recyclerview:recyclerview:1.3.2"
- const val ANDROIDX_VECTOR = "androidx.vectordrawable:vectordrawable:1.1.0"
+ const val ANDROIDX_VECTOR = "androidx.vectordrawable:vectordrawable:1.2.0"
const val ANDROIDX_SWIPE_REFRESH_LAYOUT = "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0"
const val ANDROIDX_CORE_TESTING = "androidx.arch.core:core-testing:2.2.0"
const val ANDROIDX_WORK_MANAGER = "androidx.work:work-runtime:${Versions.WORK_MANAGER}"
const val ANDROIDX_WORK_MANAGER_KTX = "androidx.work:work-runtime-ktx:${Versions.WORK_MANAGER}"
const val ANDROIDX_WEBKIT = "androidx.webkit:webkit:1.9.0"
const val ANDROIDX_DATABINDING_COMPILER = "androidx.databinding:databinding-compiler:${Versions.ANDROID_GRADLE_TOOLS}" // This is bundled with the gradle plugin so we use the same version
- const val ANDROIDX_COMPOSE_ACTIVITY = "androidx.activity:activity-compose:1.8.2"
+ const val ANDROIDX_COMPOSE_ACTIVITY = "androidx.activity:activity-compose:1.9.0"
+ const val DATASTORE = "androidx.datastore:datastore-preferences:${Versions.DATASTORE}"
/* Firebase */
- const val FIREBASE_BOM = "com.google.firebase:firebase-bom:32.6.0"
+ const val FIREBASE_BOM = "com.google.firebase:firebase-bom:33.4.0"
const val FIREBASE_CRASHLYTICS = "com.google.firebase:firebase-crashlytics"
const val FIREBASE_MESSAGING = "com.google.firebase:firebase-messaging"
const val FIREBASE_CONFIG = "com.google.firebase:firebase-config"
const val FIREBASE_CRASHLYTICS_NDK = "com.google.firebase:firebase-crashlytics-ndk"
- /* Play Services */
+ /* Google Dependencies */
const val PLAY_IN_APP_UPDATES = "com.google.android.play:app-update:2.1.0"
const val FLEXBOX_LAYOUT = "com.google.android.flexbox:flexbox:3.0.0"
+ const val MATERIAL_DESIGN = "com.google.android.material:material:1.12.0"
/* Mobius */
const val MOBIUS_CORE = "com.spotify.mobius:mobius-core:${Versions.MOBIUS}"
@@ -97,11 +98,11 @@ object Libs {
/* Testing */
const val JUNIT = "junit:junit:${Versions.JUNIT}"
const val ROBOLECTRIC = "org.robolectric:robolectric:${Versions.ROBOLECTRIC}"
- const val ANDROIDX_TEST_JUNIT = "androidx.test.ext:junit:1.1.5"
- const val MOCKK = "io.mockk:mockk:1.13.12"
- const val THREETEN_BP = "org.threeten:threetenbp:1.6.8"
- const val UI_AUTOMATOR = "com.android.support.test.uiautomator:uiautomator-v18:2.1.3"
- const val TEST_ORCHESTRATOR = "androidx.test:orchestrator:1.4.2"
+ const val ANDROIDX_TEST_JUNIT = "androidx.test.ext:junit:1.2.1"
+ const val MOCKK = "io.mockk:mockk:1.13.13"
+ const val THREETEN_BP = "org.threeten:threetenbp:1.7.0"
+ const val UI_AUTOMATOR = "androidx.test.uiautomator:uiautomator:2.3.0"
+ const val TEST_ORCHESTRATOR = "androidx.test:orchestrator:1.5.1"
/* Qr Code (zxing) */
const val JOURNEY_ZXING = "com.journeyapps:zxing-android-embedded:4.3.0"
@@ -147,10 +148,10 @@ object Libs {
const val OKHTTP = "com.squareup.okhttp3:okhttp:${Versions.OKHTTP}"
const val OKHTTP_LOGGING = "com.squareup.okhttp3:logging-interceptor:${Versions.OKHTTP}"
const val OKHTTP_URL_CONNECTION = "com.squareup.okhttp3:okhttp-urlconnection:${Versions.OKHTTP}"
- const val OKIO = "com.squareup.okio:okio:3.6.0"
+ const val OKIO = "com.squareup.okio:okio:3.9.1"
/* Other */
- const val LOTTIE = "com.airbnb.android:lottie:6.2.0"
+ const val LOTTIE = "com.airbnb.android:lottie:6.5.2"
const val SLIDING_UP_PANEL = "com.sothree.slidinguppanel:library:3.3.1"
const val DISK_LRU_CACHE = "com.jakewharton:disklrucache:2.0.2"
const val EVENTBUS = "org.greenrobot:eventbus:3.3.1"
@@ -158,7 +159,7 @@ object Libs {
const val PROCESS_PHOENIX = "com.jakewharton:process-phoenix:2.1.2"
const val PAPERDB = "io.github.pilgr:paperdb:2.7.2"
const val KEYBOARD_VISIBILITY_LISTENER = "net.yslibrary.keyboardvisibilityevent:keyboardvisibilityevent:2.2.1"
- const val APACHE_COMMONS_TEXT = "org.apache.commons:commons-text:1.10.0"
+ const val APACHE_COMMONS_TEXT = "org.apache.commons:commons-text:1.12.0"
const val CAMERA_VIEW = "com.otaliastudios:cameraview:2.7.2"
const val HEAP = "com.heapanalytics.android:heap-android-client:${Versions.HEAP}"
@@ -188,6 +189,7 @@ object Libs {
}
object Plugins {
+ // We should upgrade this to 3.0.0, but it requires AGP 8.1+, which we are currently not supporting due to the ProjectTransformers
const val FIREBASE_CRASHLYTICS = "com.google.firebase:firebase-crashlytics-gradle:2.9.2"
const val ANDROID_GRADLE_TOOLS = "com.android.tools.build:gradle:${Versions.ANDROID_GRADLE_TOOLS}"
const val APOLLO = "com.apollographql.apollo:apollo-gradle-plugin:${Versions.APOLLO}"
diff --git a/docs/git-workflow/README.md b/docs/git-workflow/README.md
index 5871b7efc8..60e847081c 100644
--- a/docs/git-workflow/README.md
+++ b/docs/git-workflow/README.md
@@ -8,26 +8,11 @@
1. Create a new branch from master. Use the naming convention specified below.
2. Create and commit changes. Try to keep one commit per feature/change to reduce clutter during code reviews.
3. When ready to merge, create a Pull Request with master as the base branch.
- 4. Code must be reviewed and approved by at least one non-author. When applicable, the reviewer should manually test the changes and/or verify that adequate tests are in place.
+ 4. Code must be reviewed and approved by at least one or two non-author and should be manually tested at least by one person. For further PR rules check the related Confluence page.
6. Once approved, proceed to merge the changes. The preferred merging method, if available, is *Squash and merge*. This cuts down on the number of merge commits and reduce noise in the git history compared to a regular merge.
7. Delete the branch.
-
-3. **Release branches**
- Releases are achieved using release branches:
- 1. Initiate the release process by creating a new release branch from master. Use the naming convention specified below.
- 2. Push and protect this branch. There should be no commits directly to this branch until the release is in production. This must happen before the remaining steps to ensure that other developers can access the branch.
- 3. For any changes (version bump, update translations, bug fix, etc), create a new topic branch from this release branch, using the naming convention specified below.
- 4. When changes are complete, this topic branch should be merged back into the source release branch via Pull Request, after proper Code Review, and then deleted.
- 5. When a release is ready for final testing, use the appropriate workflow in the CI service to generate a release candidate APK from the head of the release branch and test appropriately.
- 6. When ready to deploy to production, use the appropriate workflow in the CI service to generate and upload a release APK to the internal Google Play test track. This will add the APK to the artifact library and will deploy the release to internal users who require early access to the release (e.g. the documentation team).
- 7. A developer should then manually initiate a new release in the Google Play console, using the APK uploaded in during the prior step, and begin rollout to a small subset of users (5-10%).
- 8. If no blocking issues are discovered during the rollout, the rollout percentage may be incrementally increased.
- 9. If issues are discovered during rollout, repeat the previous steps as necessary while bumping the version code (not the version name) for each new release.
- 10. Once rollout reaches 100% and the release is considered complete, create a git tag and GitHub release based on the head of the release branch. Follow the naming convention specified in the next section.
- 11. The release branch can now be merged back into master via Pull Request. If many changes have occurred on master since the release branch was created, it may be necessary to unprotect the branch, merge in master, and resolve any merge conflicts first.
- 12. Once merged, unprotect and delete the release branch.
-4. **Branch Naming Conventions**
+3. **Branch Naming Conventions**
- **Normal branches** - It is suggested that normal branch names include the associated project name and ticket number, where applicable, and optionally a few words describing the purpose of the branch.
*Examples:* `parent/MBL-1234`, `MBL-1234-update-rating-dialog`, `fix-teacher-release-crash`
@@ -41,7 +26,7 @@
*{project}-{version_name}-{version_code}*
*Example:* `parent-1.1.0-5`
-5. **Pull Request message tags**
+4. **Pull Request message tags**
Example: `[project][MBL-0000] This is my PR message`
Adding tags to your Pull Request messages helps provide information that will be useful for both reviewers and for future developers (including yourself). Tags take the form of one or two short words surrounded by square brackets, and should be added at the beginning of the first line of the PR message. These are the primary tags you'll be using:
@@ -57,10 +42,7 @@
- **Skip CI**: Often when your code is a work in progress you don't need to trigger Continuous Integration builds for your changes. If you want to skip these builds for your WIP PR and free up CI resources for other active PRs, add the [skip ci] tag. Like the [WIP] tag, make sure to remove this tag when your changes are complete.
*Example:* `[WIP][skip ci] Testing out some changes`
- - **Release Target**: If the pull request will be merged into a release branch, it is suggested that you add a tag for the project name (see above) as well as a release target tag that specifies the release version, in the form of [R-#.#.#]
- *Example:* `[Teacher][R-1.8.1] Fix dashboard regression`
-
-6. **Misc**
+5. **Misc**
To maintain SOC 2 compliance, no code should ever be committed directly to the master branch. All changes must be pushed to topic branches and merged via Pull Request after proper Code Review and QA. This also applies to release branches where possible, particularly for commits made prior to 100% deployment.
Generally, a topic branch should only belong to the developer who created it. In cases where multiple developers must commit to the same branch, ensure that you do not rebase pushed commits. Things will get messy.
diff --git a/foosball/.gitignore b/foosball/.gitignore
deleted file mode 100644
index a9cd6060b4..0000000000
--- a/foosball/.gitignore
+++ /dev/null
@@ -1,73 +0,0 @@
-# Private data
-debug.keystore
-debug.properties
-private.properties
-app/fabric.properties
-google-services.json
-app/src/tablet/res/raw/bagel_song.mp3
-app/src/tablet/res/raw/ding.mp3
-app/src/tablet/res/raw/ding_ding.mp3
-app/src/tablet/res/raw/ding_reverse.mp3
-app/src/tablet/res/raw/dominating.mp3
-app/src/tablet/res/raw/doublekill.mp3
-app/src/tablet/res/raw/firstblood.mp3
-app/src/tablet/res/raw/killingspree.mp3
-app/src/tablet/res/raw/massacre.mp3
-app/src/tablet/res/raw/multikill.mp3
-app/src/tablet/res/raw/murca.mp3
-app/src/tablet/res/raw/rotate_ding.mp3
-app/src/tablet/res/raw/unstoppable.mp3
-app/src/tablet/res/raw/motivation.mp3
-app/src/tablet/res/raw/leeroy_jenkins.mp3
-app/src/tablet/res/raw/illuminati_confirmed.mp3
-app/src/tablet/res/raw/mlg_horn.mp3
-app/src/tablet/res/raw/legitness.mp3
-app/src/tablet/res/raw/profamity.mp3
-app/src/tablet/res/raw/police_siren.mp3
-
-# built application files #
-*.apk
-*.ap_
-
-# files for the dex VM #
-*.dex
-
-# Java class files #
-*.class
-
-# generated files #
-bin
-gen
-target
-
-# Local configuration file (sdk path, etc) #
-local.properties
-
-# Windows thumbnail db #
-Thumbs.db
-
-# OSX files
-.DS_Store
-
-# Eclipse project files #
-.classpath
-.project
-
-# Android Studio #
-.idea
-*.iml
-build
-*.iws
-
-# Package Files #
-*.jar
-*.war
-*.ear
-
-# Project Files #
-.gitconfig
-
-# Gradle Files #
-gradle.properties
-.gradle
-total 1304
diff --git a/foosball/adb_foos.py b/foosball/adb_foos.py
deleted file mode 100644
index c2009853d9..0000000000
--- a/foosball/adb_foos.py
+++ /dev/null
@@ -1,113 +0,0 @@
-import CHIP_IO.GPIO as GPIO
-import time
-import os
-import urllib2
-import socket
-import logging
-from subprocess import call
-
-TIMESTR = time.strftime("%Y%m%d-%H%M%S")
-
-logging.basicConfig(level=logging.DEBUG, filename='foos_' + TIMESTR + '.log')
-
-TABLE_ID = "5FLOOR"
-
-BTNRESET = "GPIO2"
-BTN1 = "GPIO3"
-BTN2 = "GPIO4"
-LED1 = "GPIO5"
-LED2 = "GPIO6"
-
-OFF = GPIO.HIGH
-ON = GPIO.LOW
-
-FLASH_TIME = 0.2
-FLASH_COUNT = 10
-
-GPIO.setup(LED1, GPIO.OUT)
-GPIO.setup(LED2, GPIO.OUT)
-GPIO.output(LED1, OFF)
-GPIO.output(LED2, OFF)
-
-GPIO.setup(BTN1, GPIO.IN)
-GPIO.setup(BTN2, GPIO.IN)
-GPIO.setup(BTNRESET, GPIO.IN)
-
-def printlog(message):
- print(message)
- logging.info(message)
-
-def blink(speed, count, *leds):
- while count > 0:
- count -= 1
- for led in leds:
- GPIO.output(led, ON)
- time.sleep(speed)
- for led in leds:
- GPIO.output(led, OFF)
- time.sleep(speed)
-
-def command(command):
- call(command, shell=True)
-
-# Initialize
-GPIO.output(LED1, ON)
-GPIO.output(LED2, ON)
-
-#Init adb
-printlog("Initilizing ADB")
-command('adb devices')
-
-blink(FLASH_TIME, FLASH_COUNT, LED1, LED2)
-
-flashing = False
-
-def updateScore(side):
- try:
- if side == 0:
- command('adb shell am broadcast -a action_goal --es scoringSide "SIDE_1"')
- else:
- command('adb shell am broadcast -a action_goal --es scoringSide "SIDE_2"')
- return True
- except:
- logging.exception("Update Score failure:")
- return False
-
-def buttoncallback(button):
- global flashing
- if flashing:
- return
- flashing = True
- if button == BTN1:
- printlog("TEAM 1 GOAL!")
- GPIO.output(LED1, ON)
- if updateScore(0):
- blink(FLASH_TIME, FLASH_COUNT, LED1)
- else:
- GPIO.output(LED1, OFF)
- else:
- printlog("TEAM 2 GOAL!")
- GPIO.output(LED2, ON)
- if updateScore(1):
- blink(FLASH_TIME, FLASH_COUNT, LED2)
- else:
- GPIO.output(LED2, OFF)
- flashing = False
-
-GPIO.add_event_detect(BTN1, GPIO.FALLING, buttoncallback)
-GPIO.add_event_detect(BTN2, GPIO.FALLING, buttoncallback)
-
-printlog ("Awaiting Button press")
-
-try:
- GPIO.wait_for_edge(BTNRESET, GPIO.FALLING)
- printlog("Reset button pressed")
- blink(0.1, 20, LED1, LED2)
- os.system("reboot")
-except:
- logging.exception("Exiting reset button loop:")
- GPIO.cleanup()
-
-printlog("Cleaning up")
-
-GPIO.cleanup()
diff --git a/foosball/app/.gitignore b/foosball/app/.gitignore
deleted file mode 100644
index af55bfbd9f..0000000000
--- a/foosball/app/.gitignore
+++ /dev/null
@@ -1,51 +0,0 @@
-# Private data
-debug.keystore
-debug.properties
-private.properties
-google-services.json
-
-# built application files #
-*.apk
-*.ap_
-
-# files for the dex VM #
-*.dex
-
-# Java class files #
-*.class
-
-# generated files #
-bin
-gen
-target
-
-# Local configuration file (sdk path, etc) #
-local.properties
-
-# Windows thumbnail db #
-Thumbs.db
-
-# OSX files
-.DS_Store
-
-# Eclipse project files #
-.classpath
-.project
-
-# Android Studio #
-.idea
-*.iml
-build
-*.iws
-
-# Package Files #
-*.jar
-*.war
-*.ear
-
-# Project Files #
-.gitconfig
-
-# Gradle Files #
-gradle.properties
-.gradle
diff --git a/foosball/app/build.gradle b/foosball/app/build.gradle
deleted file mode 100644
index 846fc64a87..0000000000
--- a/foosball/app/build.gradle
+++ /dev/null
@@ -1,130 +0,0 @@
-/*
- * Copyright (C) 2016 - 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.
- */
-
-apply plugin: 'com.android.application'
-apply plugin: 'kotlin-android'
-apply plugin: 'kotlin-android-extensions'
-apply plugin: 'realm-android'
-apply plugin: 'kotlin-kapt'
-
-android {
- compileSdkVersion Versions.COMPILE_SDK
- buildToolsVersion Versions.BUILD_TOOLS
-
- defaultConfig {
- applicationId "com.instructure.androidfoosball"
- minSdkVersion Versions.MIN_SDK
- targetSdkVersion Versions.TARGET_SDK
- versionCode rootProject.ext.versionCode
- versionName rootProject.ext.versionName
- vectorDrawables.useSupportLibrary = true
- multiDexEnabled true
-
- buildConfigField "String", "FIREBASE_USERNAME", "\"$firebase_tab_username\""
- buildConfigField "String", "FIREBASE_PASSWORD", "\"$firebase_tab_password\""
- buildConfigField "String", "FIREBASE_SERVER_KEY", "\"$firebase_serverkey\""
- buildConfigField "String", "FIREBASE_STORAGE_BASE_URL", "\"$firebase_storage_base_url\""
- }
- buildTypes {
- release {
- minifyEnabled false
- proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
- }
- debug {
- minifyEnabled false
- signingConfig signingConfigs.debug
- }
- }
-
- flavorDimensions 'default'
- productFlavors {
- phone {
- dimension 'default'
- applicationId "com.instructure.androidfoosball"
- versionName "1.0-phone"
- }
- tablet {
- dimension 'default'
- applicationId "com.instructure.androidfoosball.tablet"
- versionName "1.0-tablet"
- }
- }
-
- signingConfigs {
- debug {
- storeFile file("../debug.keystore")
- storePassword "android"
- keyAlias "androiddebugkey"
- keyPassword "android"
- }
- }
-}
-
-androidExtensions {
- experimental = true
-}
-
-kotlin {
- experimental {
- coroutines 'enable'
- }
-}
-
-dependencies {
- implementation fileTree(include: ['*.jar'], dir: 'libs')
-
- api project(':wearutils')
- wearApp project(':wear')
-
- testImplementation 'junit:junit:4.12'
- implementation Libs.SUPPORT_APPCOMPAT
- implementation Libs.SUPPORT_V4
- implementation Libs.SUPPORT_DESIGN
- implementation Libs.SUPPORT_PERCENT
- implementation Libs.SUPPORT_CARDVIEW
- // Dependency for Google Sign-In
- implementation Libs.PLAY_SERVICES_AUTH
- implementation "com.google.firebase:firebase-core:${Versions.PLAY_SERVICES}"
- implementation "com.google.firebase:firebase-auth:${Versions.PLAY_SERVICES}"
- implementation "com.google.firebase:firebase-database:${Versions.PLAY_SERVICES}"
- implementation "com.google.firebase:firebase-storage:${Versions.PLAY_SERVICES}"
- implementation "com.google.firebase:firebase-messaging:${Versions.PLAY_SERVICES}"
- implementation 'com.firebaseui:firebase-ui-database:0.6.1'
-
- implementation 'com.squareup.picasso:picasso:2.5.2'
- implementation 'de.hdodenhof:circleimageview:2.0.0'
- implementation 'com.afollestad.material-dialogs:core:0.8.6.2'
- implementation Libs.KOTLIN_STD_LIB
- implementation Libs.KOTLIN_COROUTINES_CORE
- implementation Libs.KOTLIN_COROUTINES_ANDROID
- implementation 'com.davemorrissey.labs:subsampling-scale-image-view:3.5.0'
- implementation "org.jetbrains.anko:anko-commons:${Versions.KOTLIN_ANKO}"
- implementation "org.jetbrains.anko:anko-sdk21-listeners:${Versions.KOTLIN_ANKO}"
- implementation "org.jetbrains.anko:anko-sdk21:${Versions.KOTLIN_ANKO}"
- implementation 'io.reactivex:rxjava:1.0.16'
- implementation 'com.squareup.okhttp3:okhttp:3.4.1'
-
- implementation 'com.google.zxing:core:3.3.2'
- implementation ("com.github.bumptech.glide:glide:4.6.1") {
- exclude group: "com.android.support"
- }
-}
-
-apply plugin: 'com.google.gms.google-services'
-
diff --git a/foosball/app/proguard-rules.pro b/foosball/app/proguard-rules.pro
deleted file mode 100644
index 599abaa6db..0000000000
--- a/foosball/app/proguard-rules.pro
+++ /dev/null
@@ -1,17 +0,0 @@
-# Add project specific ProGuard rules here.
-# By default, the flags in this file are appended to flags specified
-# in /Users/{user}/android-sdks/tools/proguard/proguard-android.txt
-# You can edit the include path and order by changing the proguardFiles
-# directive in build.gradle.
-#
-# For more details, see
-# http://developer.android.com/guide/developing/tools/proguard.html
-
-# Add any project specific keep options here:
-
-# If your project uses WebView with JS, uncomment the following
-# and specify the fully qualified class name to the JavaScript interface
-# class:
-#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
-# public *;
-#}
diff --git a/foosball/app/src/main/AndroidManifest.xml b/foosball/app/src/main/AndroidManifest.xml
deleted file mode 100644
index 74f5b45476..0000000000
--- a/foosball/app/src/main/AndroidManifest.xml
+++ /dev/null
@@ -1,48 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/foosball/app/src/main/java/com/instructure/androidfoosball/activities/ChangeAvatarActivity.kt b/foosball/app/src/main/java/com/instructure/androidfoosball/activities/ChangeAvatarActivity.kt
deleted file mode 100644
index c4a168be52..0000000000
--- a/foosball/app/src/main/java/com/instructure/androidfoosball/activities/ChangeAvatarActivity.kt
+++ /dev/null
@@ -1,288 +0,0 @@
-/*
- * Copyright (C) 2016 - 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.androidfoosball.activities
-
-import android.app.Activity
-import android.app.ProgressDialog
-import android.content.ActivityNotFoundException
-import android.content.Context
-import android.content.Intent
-import android.graphics.*
-import android.media.ExifInterface
-import android.net.Uri
-import android.os.Build
-import android.os.Bundle
-import android.provider.MediaStore
-import android.support.v4.content.FileProvider
-import android.support.v7.app.AlertDialog
-import android.support.v7.app.AppCompatActivity
-import android.view.View
-import android.widget.Toast
-import com.davemorrissey.labs.subscaleview.ImageSource
-import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
-import com.google.firebase.database.FirebaseDatabase
-import com.google.firebase.storage.FirebaseStorage
-import com.instructure.androidfoosball.BuildConfig
-import com.instructure.androidfoosball.R
-import com.instructure.androidfoosball.utils.setVisible
-import kotlinx.android.synthetic.main.activity_change_avatar.*
-import org.jetbrains.anko.doAsync
-import org.jetbrains.anko.uiThread
-import java.io.File
-import java.io.FileOutputStream
-import java.util.*
-
-/**
- * I was going to add comments and documentation to this file, but then I didn't.
- */
-
-enum class Mode(val displayName : String) {
- CAMERA("Camera"),
- Gallery("Photo Library")
-}
-
-class ChangeAvatarActivity : AppCompatActivity() {
-
- companion object {
- val EXTRA_ACTION_MODE = "actionMode"
- val EXTRA_USER_ID = "userId"
- val EXTRA_AVATAR_URL = "avatarUrl"
- fun createIntent(context: Context, userId: String?, mode: Mode): Intent {
- return Intent(context, ChangeAvatarActivity::class.java).apply {
- putExtra(EXTRA_USER_ID, userId)
- putExtra(EXTRA_ACTION_MODE, mode)
- }
- }
- }
-
- val USER_AVATARS_DIR = "user_avatars"
- val AVATAR_TMP_FILE_NAME = "avatar_tmp"
-
- val REQUEST_CODE_TAKE_PICTURE = 1337
- val REQUEST_CODE_GET_GALLERY_IMAGE = 7331
-
- private val userId: String? by lazy { intent.getStringExtra(EXTRA_USER_ID) }
- private val mode: Mode by lazy { intent.getSerializableExtra(EXTRA_ACTION_MODE) as Mode }
- private val tmpFile by lazy { File(externalCacheDir, AVATAR_TMP_FILE_NAME) }
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- setContentView(R.layout.activity_change_avatar)
- setup()
- when (mode) {
- Mode.CAMERA -> takeImage()
- Mode.Gallery -> pickImage()
- }
- }
-
- fun setup() {
- btnBack.setOnClickListener { finish() }
- btnDone.setOnClickListener { cropAndPost() }
- imageView.setOnImageEventListener(object : SubsamplingScaleImageView.OnImageEventListener {
- override fun onReady() { }
- override fun onTileLoadError(p0: Exception?) { }
- override fun onImageLoadError(p0: Exception?) { }
- override fun onPreviewLoadError(p0: Exception?) { }
- override fun onImageLoaded() {
- btnDone.setVisible()
- progressBar.setVisible(false)
- }
- })
- }
-
- fun takeImage() {
- try {
- val intent = Intent()
- intent.action = MediaStore.ACTION_IMAGE_CAPTURE
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
- intent.flags = Intent.FLAG_GRANT_READ_URI_PERMISSION
- val contentUri = FileProvider.getUriForFile(this, "com.instructure.androidfoosball.fileProvider", tmpFile)
- intent.putExtra(MediaStore.EXTRA_OUTPUT, contentUri)
- } else {
- intent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(tmpFile))
- }
- startActivityForResult(intent, REQUEST_CODE_TAKE_PICTURE)
- } catch (anfe: ActivityNotFoundException) {
- Toast.makeText(this, "No activity found to open this attachment.", Toast.LENGTH_LONG).show()
- }
- }
-
- fun pickImage() {
- val intent = Intent()
- intent.type = "image/*"
- intent.action = Intent.ACTION_GET_CONTENT
- startActivityForResult(intent, REQUEST_CODE_GET_GALLERY_IMAGE)
- }
-
- override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
- if (resultCode != RESULT_OK) { finish(); return }
- when (requestCode) {
- REQUEST_CODE_TAKE_PICTURE -> setImage()
- REQUEST_CODE_GET_GALLERY_IMAGE -> {
- contentResolver.openInputStream(data!!.data).copyTo(FileOutputStream(tmpFile))
- setImage()
- }
- }
- }
-
- fun setImage() {
- root.visibility = View.VISIBLE
- imageView.orientation = SubsamplingScaleImageView.ORIENTATION_USE_EXIF
- imageView.setMinimumScaleType(SubsamplingScaleImageView.SCALE_TYPE_CENTER_CROP)
- imageView.setImage(ImageSource.uri(tmpFile.absolutePath))
- }
-
- private fun cropAndPost() {
-
- val dialog = ProgressDialog(this).apply {
- setMessage(context.getString(R.string.uploading))
- isIndeterminate = true
- }
-
- dialog.show()
-
- doAsync {
- val outFile = cropFile(tmpFile, getCropInfo())
- uiThread {
- val uuid = UUID.randomUUID().toString()
- val ref = FirebaseStorage.getInstance().getReferenceFromUrl(BuildConfig.FIREBASE_STORAGE_BASE_URL).child(USER_AVATARS_DIR).child("$uuid.jpg")
- ref.putFile(Uri.fromFile(outFile))
- .addOnSuccessListener {
- val avatarUrl = it.downloadUrl.toString()
- if (!userId.isNullOrBlank()) {
- FirebaseDatabase.getInstance().reference.child("users").child(userId).child("avatar").setValue(avatarUrl)
- }
- dialog.dismiss()
- if (callingActivity != null) {
- setResult(Activity.RESULT_OK, Intent().apply {
- putExtra(EXTRA_AVATAR_URL, avatarUrl)
- })
- }
- finish()
- }
- .addOnFailureListener {
- dialog.dismiss()
- Toast.makeText(this@ChangeAvatarActivity, R.string.error_uploading_photo, Toast.LENGTH_SHORT).show()
- }
- }
- }
- }
-
- fun getCropInfo(): RectF {
- val origin = imageView.viewToSourceCoord(0f, 0f)
- val dimen = imageView.viewToSourceCoord(imageView.width.toFloat(), imageView.height.toFloat())
-
- val (appliedWidth, appliedHeight) = when (imageView.appliedOrientation) {
- 90, 270 -> Pair(imageView.sHeight, imageView.sWidth)
- else -> Pair(imageView.sWidth, imageView.sHeight)
- }
-
- return RectF(
- origin.x / appliedWidth,
- origin.y / appliedHeight,
- dimen.x / appliedWidth,
- dimen.y / appliedHeight)
- }
-
-}
-
-fun showImageSourcePicker(context: Context, userId: String? = null, onSelected: (intent: Intent) -> Unit) {
- AlertDialog.Builder(context)
- .setItems(Mode.values().map { it.displayName }.toTypedArray()) { dialog, which ->
- onSelected(ChangeAvatarActivity.createIntent(context, userId, Mode.values()[which]))
- }.show()
-}
-
-private val CROP_SIZE = 512
-private val COMPRESS_QUALITY = 70
-
-private fun cropFile(srcFile: File, cropInfoInput: RectF?): File {
-
- val bOptions = BitmapFactory.Options()
- bOptions.inJustDecodeBounds = true
- BitmapFactory.decodeFile(srcFile.absolutePath, bOptions)
- bOptions.inJustDecodeBounds = false
-
- val orientationTag = ExifInterface(srcFile.absolutePath).getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL)
-
- val sWidth = bOptions.outWidth
- val sHeight = bOptions.outHeight
-
- val cropInfo: RectF = if (cropInfoInput != null) cropInfoInput else {
-
- var aWidth = sWidth
- var aHeight = sHeight
-
- if (orientationTag == ExifInterface.ORIENTATION_ROTATE_270 || orientationTag == ExifInterface.ORIENTATION_ROTATE_90) {
- aWidth = sHeight
- aHeight = sWidth
- }
-
- val dimenX: Float = if (aHeight > aWidth) 0.5f else aHeight / 2f / aWidth
- val dimenY: Float = if (aHeight > aWidth) aWidth / 2f / aHeight else 0.5f
- RectF(0.5f - dimenX, 0.5f - dimenY, 0.5f + dimenX, 0.5f + dimenY)
- }
-
- val adjustedCrop = when (orientationTag) {
- ExifInterface.ORIENTATION_ROTATE_90 -> RectF(cropInfo.top, 1 - cropInfo.right, cropInfo.bottom, 1 - cropInfo.left)
- ExifInterface.ORIENTATION_ROTATE_180 -> RectF(1 - cropInfo.right, 1 - cropInfo.bottom, 1 - cropInfo.left, 1 - cropInfo.top)
- ExifInterface.ORIENTATION_ROTATE_270 -> RectF(1 - cropInfo.bottom, cropInfo.left, 1 - cropInfo.top, cropInfo.right)
- else -> cropInfo
- }
-
- val croppedWidth = (sWidth * adjustedCrop.width()).toInt()
- val sampleScale = if (croppedWidth < CROP_SIZE) 1 else croppedWidth / CROP_SIZE * 2
-
- bOptions.inSampleSize = sampleScale
-
- // Ignored by system in Lollipop and higher
- @Suppress("DEPRECATION")
- bOptions.inPurgeable = true
-
- val decodeRect = Rect(
- (sWidth * adjustedCrop.left).toInt(),
- (sHeight * adjustedCrop.top).toInt(),
- (sWidth * adjustedCrop.right).toInt(),
- (sHeight * adjustedCrop.bottom).toInt())
-
- val input = BitmapRegionDecoder.newInstance(srcFile.absolutePath, false).decodeRegion(decodeRect, bOptions)
-
- val filterPaint = Paint(Paint.ANTI_ALIAS_FLAG)
- filterPaint.isFilterBitmap = true
-
- val output = Bitmap.createBitmap(CROP_SIZE, CROP_SIZE, Bitmap.Config.RGB_565)
- val cacheCanvas = Canvas(output)
-
- val matrix = Matrix()
- val scale = CROP_SIZE.toFloat() / input.width
- val center = CROP_SIZE / 2f
- matrix.preScale(scale, scale)
- when (orientationTag) {
- ExifInterface.ORIENTATION_ROTATE_90 -> matrix.postRotate(90f, center, center)
- ExifInterface.ORIENTATION_ROTATE_180 -> matrix.postRotate(180f, center, center)
- ExifInterface.ORIENTATION_ROTATE_270 -> matrix.postRotate(270f, center, center)
- }
-
- cacheCanvas.drawBitmap(input, matrix, filterPaint)
-
- val outFile = File(srcFile.absolutePath + "_cropped")
- output.compress(Bitmap.CompressFormat.JPEG, COMPRESS_QUALITY, outFile.outputStream())
- return outFile
-}
diff --git a/foosball/app/src/main/java/com/instructure/androidfoosball/activities/EditUserActivity.kt b/foosball/app/src/main/java/com/instructure/androidfoosball/activities/EditUserActivity.kt
deleted file mode 100644
index ccba2260e7..0000000000
--- a/foosball/app/src/main/java/com/instructure/androidfoosball/activities/EditUserActivity.kt
+++ /dev/null
@@ -1,130 +0,0 @@
-/*
- * Copyright (C) 2016 - 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.androidfoosball.activities
-
-import android.app.Activity
-import android.content.Intent
-import android.os.Bundle
-import android.support.v7.app.AppCompatActivity
-import android.view.View
-import com.google.firebase.database.DataSnapshot
-import com.google.firebase.database.DatabaseError
-import com.google.firebase.database.FirebaseDatabase
-import com.google.firebase.database.ValueEventListener
-import com.instructure.androidfoosball.BuildConfig
-import com.instructure.androidfoosball.R
-import com.instructure.androidfoosball.ktmodels.User
-import com.instructure.androidfoosball.utils.*
-import com.instructure.androidfoosball.views.ConfirmPinDialog
-import kotlinx.android.synthetic.main.activity_edit_player.*
-import org.jetbrains.anko.sdk21.listeners.onCheckedChange
-import org.jetbrains.anko.sdk21.listeners.onClick
-
-class EditUserActivity : AppCompatActivity() {
-
- private val REQUEST_CODE_AVATAR = 2212
-
- private val mUserId: String by lazy { intent.getStringExtra(Const.USER_ID) }
- lateinit private var mUser: User
- private val mDatabase = FirebaseDatabase.getInstance().reference
- private val mCommentator = Commentator()
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- mCommentator.initialize(this)
- getUser()
- }
-
- private fun getUser() {
- mDatabase.child("users").child(mUserId).addListenerForSingleValueEvent(object : ValueEventListener {
- override fun onDataChange(dataSnapshot: DataSnapshot) {
- dataSnapshot.getValue(User::class.java)?.let { user ->
- user.id = dataSnapshot.key
- mUser = user
- setup()
- }
- }
-
- override fun onCancelled(databaseError: DatabaseError) { }
- })
- }
-
- private fun setup() {
- setContentView(R.layout.activity_edit_player)
-
- enablePinView.onCheckedChange { button, isChecked ->
- mUser.pinDisabled = if (isChecked) "false" else "true"
- btnChangePin.isEnabled = isChecked
- }
-
- enablePinView.isChecked = mUser.pinDisabled != "true"
- avatarView.setAvatar(mUser, resources.getDimension(R.dimen.avatar_size_large).toInt())
- displayNameView.text = mUser.name
- assignmentTextView.text = mUser.customAssignmentPhrase
- victoryTextView.text = mUser.customVictoryPhrase
-
- addAvatarView.onClick {
- showImageSourcePicker(this, null) { startActivityForResult(it, REQUEST_CODE_AVATAR) }
- }
-
- btnChangePin.onClick {
- ConfirmPinDialog(this, mUser, true) {}.overrideOnPinHashCreated { mUser.pinHash = it }.show()
- }
-
- btnSave.onClick { save() }
-
- if (BuildConfig.APPLICATION_ID.endsWith("tablet")) {
- announceNameView.onClick { mCommentator.announce(displayNameView.text) }
- announceAssignmentView.onClick { mCommentator.announce(assignmentTextView.text) }
- announceVictoryView.onClick { mCommentator.announce(victoryTextView.text) }
- } else {
- announceNameView.visibility = View.GONE
- announceAssignmentView.visibility = View.GONE
- announceVictoryView.visibility = View.GONE
- }
-
- }
-
- fun save() {
- ValidatorChain()
- .first(displayNameView.validate("User name must be min 3 letters") { it.length > 3 })
- .then(mUserId.validate("Invalid user ID", { !it.isNullOrBlank() }, { shortToast(it) }))
- .finally {
- mUser.name = displayNameView.text
- mUser.customVictoryPhrase = victoryTextView.text
- mUser.customAssignmentPhrase = assignmentTextView.text
- mDatabase.child("users").child(mUserId).setValue(mUser)
- finish()
- }
- }
-
- override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
- if (requestCode == REQUEST_CODE_AVATAR && resultCode == Activity.RESULT_OK) {
- mUser.avatar = data?.getStringExtra(ChangeAvatarActivity.EXTRA_AVATAR_URL) ?: ""
- avatarView.setAvatar(mUser)
- }
- super.onActivityResult(requestCode, resultCode, data)
- }
-
- override fun onDestroy() {
- super.onDestroy()
- mCommentator.shutUp()
- }
-}
diff --git a/foosball/app/src/main/java/com/instructure/androidfoosball/utils/Const.kt b/foosball/app/src/main/java/com/instructure/androidfoosball/utils/Const.kt
deleted file mode 100644
index 5710eb1ac9..0000000000
--- a/foosball/app/src/main/java/com/instructure/androidfoosball/utils/Const.kt
+++ /dev/null
@@ -1,30 +0,0 @@
-/*
- * Copyright (C) 2016 - 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.androidfoosball.utils
-
-object Const {
- val USER_ID = "userId"
- val USER = "user"
- val GAME = "game"
- val TABLE = "table"
- val TEAM = "team"
- val SAVED_TABLE_ID = "saved_table_id"
- val PREFERRED_TABLE_ID = "preferred_table_id"
-}
diff --git a/foosball/app/src/main/java/com/instructure/androidfoosball/utils/Extensions.kt b/foosball/app/src/main/java/com/instructure/androidfoosball/utils/Extensions.kt
deleted file mode 100644
index f030c5c49a..0000000000
--- a/foosball/app/src/main/java/com/instructure/androidfoosball/utils/Extensions.kt
+++ /dev/null
@@ -1,253 +0,0 @@
-/*
- * Copyright (C) 2016 - 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.
- */
-
-@file:Suppress("unused")
-
-package com.instructure.androidfoosball.utils
-
-import android.app.Activity
-import android.app.Dialog
-import android.content.Context
-import android.graphics.Color
-import android.support.design.widget.TextInputLayout
-import android.support.v4.view.GestureDetectorCompat
-import android.text.Editable
-import android.text.TextWatcher
-import android.util.TypedValue
-import android.view.GestureDetector
-import android.view.MotionEvent
-import android.view.View
-import android.view.ViewGroup
-import android.widget.EditText
-import android.widget.ImageView
-import android.widget.Toast
-import com.instructure.androidfoosball.R
-import com.instructure.androidfoosball.ktmodels.User
-import com.instructure.androidfoosball.views.UserInitialsDrawable
-import com.squareup.picasso.Picasso
-import org.jetbrains.anko.displayMetrics
-import org.jetbrains.anko.sdk21.listeners.onTouch
-import java.util.*
-import java.util.regex.Pattern
-import kotlin.properties.ReadOnlyProperty
-import kotlin.reflect.KProperty
-
-
-fun ImageView.setAvatar(user: User?, avatarSize: Int = 0) {
- val picasso = Picasso.with(context).apply { cancelRequest(this@setAvatar) }
- when {
- user == null -> setImageResource(R.drawable.sadpanda)
- user.avatar.isBlank() -> setImageDrawable(UserInitialsDrawable(user, avatarSize))
- else -> picasso.load(user.avatar).placeholder(R.drawable.sadpanda).error(R.drawable.sadpanda).into(this)
- }
-}
-
-fun ImageView.setAvatarUrl(url: String?) {
- val picasso = Picasso.with(context).apply { cancelRequest(this@setAvatarUrl) }
- when {
- url.isNullOrBlank() -> setImageResource(R.drawable.sadpanda)
- else -> picasso.load(url).placeholder(R.drawable.sadpanda).error(R.drawable.sadpanda).into(this)
- }
-}
-
-fun String?.elseIfBlank(alt: String): String = if (isNullOrBlank()) alt else this!!
-
-fun Activity.shortToast(message: String) = Toast.makeText(this, message, Toast.LENGTH_SHORT).show()
-fun Activity.shortToast(messageId: Int) = Toast.makeText(this, messageId, Toast.LENGTH_SHORT).show()
-fun Activity.longToast(message: String) = Toast.makeText(this, message, Toast.LENGTH_LONG).show()
-fun Activity.longToast(messageId: Int) = Toast.makeText(this, messageId, Toast.LENGTH_LONG).show()
-
-fun TextInputLayout.validate(validator: (String) -> Boolean, errorMessage: String, afterValidation: (isValid: Boolean) -> Unit): Boolean {
- return if (validator(editText?.text.toString())) {
- error = ""
- afterValidation(true)
- true
- } else {
- error = errorMessage
- afterValidation(false)
- false
- }
-}
-
-fun TextInputLayout.onTextChanged(onChanged: (newText: String) -> Unit) {
- editText?.addTextChangedListener(object : TextWatcher {
- override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { }
- override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) { }
- override fun afterTextChanged(s: Editable?) { onChanged(s.toString()) }
- })
-}
-
-fun EditText.onTextChanged(onChanged: (newText: String) -> Unit) {
- addTextChangedListener(object : TextWatcher {
- override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { }
- override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) { }
- override fun afterTextChanged(s: Editable?) { onChanged(s.toString()) }
- })
-}
-
-fun TextInputLayout.validateOnTextChanged(validator: (String) -> Boolean, errorMessage: String, afterValidation: (newText: String, isValid: Boolean) -> Unit) {
- onTextChanged { newText -> validate(validator, errorMessage, { afterValidation(newText, it) }) }
-}
-
-var TextInputLayout.text: String
- get() = editText?.text.toString()
- set(value) {
- editText?.setText(value)
- }
-
-fun T.setVisible(isVisible: Boolean = true): T = apply {
- visibility = if (isVisible) View.VISIBLE else View.GONE
-}
-
-fun String.matches(pattern: Pattern) = pattern.matcher(this).matches()
-
-/**
- * Calculates this user's win rate.
- * @param minGamesRequired The minimum number of games this user must have played to warrant a proper win rate
- * *
- * @return A float between 0 and 100, or -1 if the user has not played enough games to warrant a win rate
- */
-fun User.getWinRate(minGamesRequired: Int) = when {
- wins + losses < minGamesRequired -> -1f
- wins == 0 -> 0f
- losses == 0 -> 100f
- else -> 100f * wins / (wins + losses)
-}
-
-fun List.sortByWinRatio(minGamesRequired: Int)
- = sortedWith(compareBy({ -it.getWinRate(minGamesRequired) }, { -it.wins }, { it.losses } ))
-
-fun List.sortByFoosRanking() = sortedBy { -it.foosRanking }
-
-/**
- * Clamps a color's brightness
- * @property maxBrightness A float between 0 and 1, where 1 is maximum possible brightness
- */
-fun Int.clampToBrightness(maxBrightness: Float): Int {
- val hsv = FloatArray(3)
- Color.colorToHSV(this, hsv)
- hsv[2] = hsv[2].coerceAtMost(maxBrightness)
- return Color.HSVToColor(hsv)
-}
-
-/** A color generated from the user's name */
-val User.avatarColor: Int
- get() = name.hashCode() or 0xFF000000.toInt()
-
-/** The user's initials, or entire name if it's only one word */
-val User.initials: String
- get() {
- if (name.isBlank()) return "?"
- val names = name.split(' ', limit = 3).filter { it.isNotBlank() }
- return if (names.size == 1) {
- names[0]
- } else {
- names.fold("") { initials, nextName -> initials + nextName[0].toUpperCase() }
- }
- }
-
-class Unless(val value: T, private val condition: (T) -> Boolean) {
- infix fun then(alternateValue: T) = if (condition(value)) alternateValue else value
-}
-
-infix fun T.unless(condition: (T) -> Boolean) = Unless(this, condition)
-
-class ValidatorChain(private val validators: MutableList> = ArrayList()) {
- infix fun first(validator: Validator<*>) = then(validator)
-
- infix fun then(validator: Validator<*>): ValidatorChain {
- validators.add(validator)
- return this
- }
-
- infix fun finally(block: () -> Unit) {
- if (validators.all { it.validate() }) block()
- }
-}
-
-data class Validator(val field: T, private val errorText: String, private val condition: (T) -> Boolean, private val onFail: (String) -> Unit) {
- fun validate() = condition(field).apply { if (!this) onFail(errorText) }
-}
-
-fun TextInputLayout.validate(errorText: String, condition: (String) -> Boolean) = Validator(this, errorText, { condition(text) }, { error = errorText; requestFocus() })
-
-fun T.validate(errorText: String, condition: (T) -> Boolean, onFail: (String) -> Unit) = Validator(this, errorText, { condition(this) }, onFail)
-
-fun validationBlock(block: ValidatorChain.() -> Unit) { ValidatorChain().block() }
-
-/** Returns a list of all children (direct descendants) in this ViewGroup */
-val ViewGroup.children: List get() = (0 until childCount).map { getChildAt(it) }
-
-/** Returns a list of all children of a specific type in this ViewGroup */
-@Suppress("UNCHECKED_CAST")
-inline fun ViewGroup.children() = children.filter { it is T } as List
-
-/** Returns a list of all descendants in this ViewGroup */
-val ViewGroup.descendants: List get() = children + children().flatMap { it.descendants }
-
-/** Returns a list of all descendants of a specific type in this ViewGroup */
-@Suppress("UNCHECKED_CAST")
-inline fun ViewGroup.descendants() = descendants.filter { it is T } as List
-
-/** Performs the provided function on all of this ViewGroup's descendants of a specific type */
-inline fun ViewGroup.modifyDescendants(mod: (T) -> Unit) = descendants().forEach { mod(it) }
-
-/** View binding, for scopes not covered by Kotlin's Android Extensions */
-fun Dialog.bind(id: Int): Binder = Binder { it.findViewById(id) }
-
-fun ViewGroup.bind(id: Int): Binder = Binder { it.findViewById(id) }
-
-@Suppress("UNCHECKED_CAST")
-class Binder(private val finder: (T) -> View?) : ReadOnlyProperty {
- private var cachedView: V? = null
- override fun getValue(thisRef: T, property: KProperty<*>): V {
- if (cachedView == null) {
- val v = finder(thisRef) ?: throw RuntimeException("Unable to bind ${property.name}; findViewById returned null.")
- cachedView = v as V
- }
- return cachedView!!
- }
-}
-
-fun Int.toDp(context: Context) = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, this.toFloat(), context.displayMetrics)
-fun Float.toDp(context: Context) = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, this, context.displayMetrics)
-
-fun View.onDoubleTap(listener: () -> Unit) {
- val detector = GestureDetectorCompat(context, object : GestureDetector.OnGestureListener {
- override fun onShowPress(p0: MotionEvent?) = Unit
- override fun onSingleTapUp(p0: MotionEvent?): Boolean = true
- override fun onDown(p0: MotionEvent?): Boolean = true
- override fun onFling(p0: MotionEvent?, p1: MotionEvent?, p2: Float, p3: Float): Boolean = true
- override fun onScroll(p0: MotionEvent?, p1: MotionEvent?, p2: Float, p3: Float): Boolean = true
- override fun onLongPress(p0: MotionEvent?) = Unit
- })
-
- detector.setOnDoubleTapListener(object : GestureDetector.OnDoubleTapListener {
- override fun onDoubleTapEvent(p0: MotionEvent?) = true
- override fun onSingleTapConfirmed(p0: MotionEvent?) = true
- override fun onDoubleTap(p0: MotionEvent?): Boolean {
- listener()
- return true
- }
- })
-
- this.onTouch { _, event -> detector.onTouchEvent(event) }
-}
-
-fun Iterable.disjunctiveUnion(other: Iterable): Set = (this - other).union(other - this)
diff --git a/foosball/app/src/main/java/com/instructure/androidfoosball/utils/PrefUtils.kt b/foosball/app/src/main/java/com/instructure/androidfoosball/utils/PrefUtils.kt
deleted file mode 100644
index c82e5e3a34..0000000000
--- a/foosball/app/src/main/java/com/instructure/androidfoosball/utils/PrefUtils.kt
+++ /dev/null
@@ -1,62 +0,0 @@
-/*
- * Copyright (C) 2016 - 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.androidfoosball.utils
-
-import android.content.Context
-import android.content.SharedPreferences
-import kotlin.properties.ReadWriteProperty
-import kotlin.reflect.KProperty
-
-open class PrefManager(val context: Context, val prefsFileName: String = "prefs") {
- val prefs: SharedPreferences by lazy { context.getSharedPreferences(prefsFileName, Context.MODE_PRIVATE) }
- val editor: SharedPreferences.Editor by lazy { prefs.edit() }
-}
-
-class Pref(val defaultValue: T, val keyName: String? = null) : ReadWriteProperty {
-
- @Suppress("UNCHECKED_CAST", "IMPLICIT_CAST_TO_ANY")
- override fun getValue(thisRef: PrefManager, property: KProperty<*>): T {
- val prefs = thisRef.prefs
- val key = keyName ?: property.name
- return when (defaultValue) {
- is Boolean -> prefs.getBoolean(key, defaultValue)
- is Float -> prefs.getFloat(key, defaultValue)
- is Int -> prefs.getInt(key, defaultValue)
- is Long -> prefs.getLong(key, defaultValue)
- is String -> prefs.getString(key, defaultValue)
- else -> throw UnsupportedOperationException("Unsupported preference type ${property.javaClass} on property ${property.name}")
- } as T
- }
-
- override fun setValue(thisRef: PrefManager, property: KProperty<*>, value: T) {
- val editor = thisRef.editor
- val key = keyName ?: property.name
- when (value) {
- is Boolean -> editor.putBoolean(key, value)
- is Float -> editor.putFloat(key, value)
- is Int -> editor.putInt(key, value)
- is Long -> editor.putLong(key, value)
- is String -> editor.putString(key, value)
- else -> throw UnsupportedOperationException("Unsupported preference type ${property.javaClass} on property ${property.name}")
- }
- editor.commit()
- }
-
-}
diff --git a/foosball/app/src/main/java/com/instructure/androidfoosball/views/ConfirmPinDialog.kt b/foosball/app/src/main/java/com/instructure/androidfoosball/views/ConfirmPinDialog.kt
deleted file mode 100644
index a48a932223..0000000000
--- a/foosball/app/src/main/java/com/instructure/androidfoosball/views/ConfirmPinDialog.kt
+++ /dev/null
@@ -1,189 +0,0 @@
-/*
- * Copyright (C) 2016 - 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.androidfoosball.views
-
-import android.app.AlertDialog
-import android.app.Dialog
-import android.content.Context
-import android.graphics.Color
-import android.os.Bundle
-import android.support.v4.content.ContextCompat
-import android.view.View
-import android.widget.TextView
-import android.widget.Toast
-import com.google.firebase.database.FirebaseDatabase
-import com.instructure.androidfoosball.R
-import com.instructure.androidfoosball.ktmodels.User
-import com.instructure.androidfoosball.utils.bind
-import org.jetbrains.anko.backgroundColor
-import org.jetbrains.anko.sdk21.listeners.onClick
-
-class ConfirmPinDialog(
- context: Context,
- val user: User,
- var createNewPin: Boolean = false,
- var onConfirmed: (User) -> Unit
-) : Dialog(context) {
-
- private enum class State {
- NEW_PIN,
- CONFIRM_NEW,
- CONFIRM_EXISTING
- }
-
- private var mNewPinHash = ""
- private var mPin = ""
- private var mState = State.CONFIRM_EXISTING
-
- private val mMessage by bind(R.id.pin_message)
- private val mPinDisplay by bind(R.id.pin_display)
- private val mPinPad by bind(R.id.pin_pad)
- private val mBackspace by bind(R.id.backspace)
- private val mUserName by bind(R.id.userName)
-
- private var onPinHashCreated: (String) -> Unit = { updateUserPinHash(it) }
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- setContentView(R.layout.dialog_confirm_pin)
- setupListeners()
- mUserName.text = user.name
- if (createNewPin) createPin() else confirmPin()
- }
-
- private fun setupListeners() {
- mPinPad.onButtonTapped = onButtonTapped
- mBackspace.onClick { updatePin(mPin.dropLast(1)) }
- }
-
- private val onButtonTapped = { number: String ->
- updatePin(mPin + number)
- if (mPin.length == 4) {
- val hash = mPin.hashCode().toString()
- when (mState) {
- ConfirmPinDialog.State.NEW_PIN -> {
- mNewPinHash = hash
- setMessage(R.string.confirm_pin, R.color.confirm_blue)
- mState = State.CONFIRM_NEW
- }
- ConfirmPinDialog.State.CONFIRM_NEW -> {
- if (hash != mNewPinHash) {
- setMessage(R.string.pin_mismatch, R.color.error_red)
- mNewPinHash = ""
- mState = State.NEW_PIN
- } else {
- onPinHashCreated(hash)
- }
- }
- ConfirmPinDialog.State.CONFIRM_EXISTING -> {
- if (hash != user.pinHash) {
- setMessage(R.string.incorrect_pin, R.color.error_red)
- } else {
- dismiss()
- onConfirmed(user)
- }
- }
- }
- updatePin("")
- }
- }
-
- private fun updatePin(newPin: String) {
- mPin = newPin
- mPinDisplay.text = "•".repeat(mPin.length)
- }
-
- fun overrideOnPinHashCreated(block: (String) -> Unit): ConfirmPinDialog {
- onPinHashCreated = {
- block(it)
- dismiss()
- }
- return this
- }
-
- override fun show() {
- if ("true" == user.pinDisabled) {
- onConfirmed(user)
- } else if (user.pinHash.isNullOrBlank()) {
- showInfoDialog()
- } else {
- super.show()
- }
- }
-
- private fun showInfoDialog() {
- AlertDialog.Builder(context)
- .setTitle(R.string.create_pin)
- .setMessage(R.string.pin_dialog_info_message)
- .setPositiveButton(R.string.create_pin) { dialog, which ->
- createNewPin = true
- super.show()
- }
- .setNegativeButton(R.string.maybe_later) { dialog, which ->
- onConfirmed(user)
- }
- .setNeutralButton(R.string.disable_pin) { dialog, which ->
- showConfirmDisablePinDialog()
- }
- .show()
- }
-
- private fun showConfirmDisablePinDialog() {
- AlertDialog.Builder(context)
- .setMessage(R.string.disable_pin_confirmation_message)
- .setPositiveButton(android.R.string.yes) { dialog, which ->
- FirebaseDatabase.getInstance().reference.child("users").child(user.id).child("pinDisabled").setValue("true")
- Toast.makeText(context, R.string.pin_has_been_disabled, Toast.LENGTH_SHORT).show()
- onConfirmed(user)
- }
- .setNegativeButton(android.R.string.no) { dialog, which ->
- showInfoDialog()
- }
- .show()
- }
-
- private fun createPin() {
- mState = State.NEW_PIN
- setMessage(R.string.enter_pin)
- }
-
- private fun confirmPin() {
- mState = State.CONFIRM_EXISTING
- setMessage(R.string.confirm_pin)
- }
-
- private fun setMessage(messageResId: Int, colorResId: Int? = null) {
- mMessage.setText(messageResId)
- if (colorResId == null) {
- mMessage.setTextColor(ContextCompat.getColor(context, android.R.color.darker_gray))
- mMessage.setBackgroundColor(Color.TRANSPARENT)
- } else {
- mMessage.setTextColor(Color.WHITE)
- mMessage.backgroundColor = ContextCompat.getColor(context, colorResId)
- }
- }
-
- private fun updateUserPinHash(pinHash: String) {
- FirebaseDatabase.getInstance().reference.child("users").child(user.id).child("pinHash").setValue(pinHash)
- Toast.makeText(context, R.string.pin_saved, Toast.LENGTH_SHORT).show()
- dismiss()
- onConfirmed(user)
- }
-}
diff --git a/foosball/app/src/main/java/com/instructure/androidfoosball/views/CropOverlay.kt b/foosball/app/src/main/java/com/instructure/androidfoosball/views/CropOverlay.kt
deleted file mode 100644
index 3db8b614b8..0000000000
--- a/foosball/app/src/main/java/com/instructure/androidfoosball/views/CropOverlay.kt
+++ /dev/null
@@ -1,109 +0,0 @@
-/*
- * Copyright (C) 2016 - 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.androidfoosball.views
-
-import android.content.Context
-import android.graphics.Canvas
-import android.graphics.Paint
-import android.graphics.Path
-import android.graphics.Region
-import android.util.AttributeSet
-import android.view.View
-import android.view.ViewGroup
-import com.instructure.androidfoosball.R
-
-
-class CropOverlay @JvmOverloads constructor(
- context: Context,
- attrs: AttributeSet? = null,
- defStyleRes: Int = 0
-) : View(context, attrs, defStyleRes) {
-
- /** Resource ID of the source view */
- private var mCutoutSourceId: Int = 0
-
- /** Reference to the source view. Bound in onSizeChanged() */
- private var mCutoutSourceView: View? = null
-
- /** Color of the primary overlay */
- private var mMaskColor = 0xAAFFFFFF.toInt()
-
- /** Frame path */
- private var mFramePath = Path()
-
- /** Frame backgroundPaint */
- private var mFramePaint = Paint(Paint.ANTI_ALIAS_FLAG)
-
- init { setup(attrs) }
-
- private fun setup(attrs: AttributeSet? = null) {
- mFramePaint.style = Paint.Style.STROKE
- if (attrs != null) {
- val a = context.obtainStyledAttributes(attrs, R.styleable.CropOverlay)
-
- // Get source view ID
- mCutoutSourceId = a.getResourceId(R.styleable.CropOverlay_cutoutViewId, 0)
-
- // Get primary overlay color
- mMaskColor = a.getColor(R.styleable.CropOverlay_maskColor, mMaskColor)
-
- // Get frame stroke width
- mFramePaint.strokeWidth = a.getDimension(R.styleable.CropOverlay_frameWidth, 10f)
-
- // Get frame color
- mFramePaint.color = a.getColor(R.styleable.CropOverlay_frameColor, 0xFF888888.toInt())
- a.recycle()
- }
- }
-
- override fun onSizeChanged(width: Int, height: Int, oldWidth: Int, oldHeight: Int) {
- super.onSizeChanged(width, height, oldWidth, oldHeight)
- if (mCutoutSourceId > 0) {
- mCutoutSourceView = (parent as ViewGroup).findViewById(mCutoutSourceId)
- mFramePath.rewind()
-
- val x = mCutoutSourceView!!.left.toFloat()
- val y = mCutoutSourceView!!.top.toFloat()
-
- val w = mCutoutSourceView!!.width.toFloat()
- val h = mCutoutSourceView!!.height.toFloat()
-
- val centerX = x + w / 2f
- val centerY = y + h / 2f
- val radius = Math.min(w, h) / 2f
-
- mFramePath.addCircle(centerX, centerY, radius, Path.Direction.CW)
- }
- }
-
- override fun onDraw(canvas: Canvas) {
- // Skip drawing if there is no source view
- if (mCutoutSourceView == null) return
-
- // Draw primary overlay
- canvas.save()
- canvas.clipPath(mFramePath, Region.Op.DIFFERENCE)
- canvas.drawColor(mMaskColor)
- canvas.restore()
-
- // Draw frame stroke
- canvas.drawPath(mFramePath, mFramePaint)
- }
-}
diff --git a/foosball/app/src/main/java/com/instructure/androidfoosball/views/PinButton.kt b/foosball/app/src/main/java/com/instructure/androidfoosball/views/PinButton.kt
deleted file mode 100644
index 821f91c485..0000000000
--- a/foosball/app/src/main/java/com/instructure/androidfoosball/views/PinButton.kt
+++ /dev/null
@@ -1,72 +0,0 @@
-/*
- * Copyright (C) 2016 - 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.androidfoosball.views
-
-import android.animation.AnimatorSet
-import android.animation.ObjectAnimator
-import android.content.Context
-import android.util.AttributeSet
-import android.util.TypedValue
-import android.view.Gravity
-import android.view.MotionEvent
-import android.view.View
-import android.widget.TextView
-
-class PinButton @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) : TextView(context, attrs, defStyleAttr) {
-
- private val animator = AnimatorSet().apply {
- playTogether(ObjectAnimator.ofFloat(this@PinButton, "scaleX", TOUCH_ZOOM, 1f),
- ObjectAnimator.ofFloat(this@PinButton, "scaleY", TOUCH_ZOOM, 1f))
- duration = 150
- }
-
- var onTap: (String) -> Unit = {}
-
- init {
- gravity = Gravity.CENTER
- setOnClickListener { if (!text.isNullOrBlank()) onTap(text.toString()) }
- }
-
- override fun onTouchEvent(event: MotionEvent): Boolean {
- when (event.action) {
- MotionEvent.ACTION_DOWN -> {
- animator.cancel()
- scaleX = TOUCH_ZOOM
- scaleY = TOUCH_ZOOM
- }
- MotionEvent.ACTION_UP,
- MotionEvent.ACTION_CANCEL -> animator.start()
- }
- return super.onTouchEvent(event)
- }
-
- override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
- if (View.MeasureSpec.getMode(heightMeasureSpec) == View.MeasureSpec.EXACTLY) {
- val height = View.MeasureSpec.getSize(heightMeasureSpec).toFloat()
- setTextSize(TypedValue.COMPLEX_UNIT_PX, height * NUMBER_SIZE)
- }
- super.onMeasure(widthMeasureSpec, heightMeasureSpec)
- }
-
- companion object {
- private val NUMBER_SIZE = 0.5f
- private val TOUCH_ZOOM = 1.3f
- }
-}
\ No newline at end of file
diff --git a/foosball/app/src/main/java/com/instructure/androidfoosball/views/PinPad.kt b/foosball/app/src/main/java/com/instructure/androidfoosball/views/PinPad.kt
deleted file mode 100644
index 197e03dfc6..0000000000
--- a/foosball/app/src/main/java/com/instructure/androidfoosball/views/PinPad.kt
+++ /dev/null
@@ -1,41 +0,0 @@
-/*
- * Copyright (C) 2016 - 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.androidfoosball.views
-
-import android.content.Context
-import android.util.AttributeSet
-import android.view.View
-import android.widget.FrameLayout
-import com.instructure.androidfoosball.R
-import com.instructure.androidfoosball.utils.modifyDescendants
-
-class PinPad @JvmOverloads constructor(
- context: Context,
- attrs: AttributeSet? = null,
- defStyleAttr: Int = 0
-) : FrameLayout(context, attrs, defStyleAttr) {
-
- var onButtonTapped: (String) -> Unit = {}
-
- init {
- View.inflate(context, R.layout.view_pin_pad, this)
- if (!isInEditMode) modifyDescendants { it.onTap = { number -> onButtonTapped(number) } }
- }
-}
diff --git a/foosball/app/src/main/java/com/instructure/androidfoosball/views/UserInitialsDrawable.kt b/foosball/app/src/main/java/com/instructure/androidfoosball/views/UserInitialsDrawable.kt
deleted file mode 100644
index 6d244222ae..0000000000
--- a/foosball/app/src/main/java/com/instructure/androidfoosball/views/UserInitialsDrawable.kt
+++ /dev/null
@@ -1,62 +0,0 @@
-/*
- * Copyright (C) 2016 - 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.androidfoosball.views
-
-import android.graphics.*
-import android.graphics.drawable.Drawable
-import com.instructure.androidfoosball.ktmodels.User
-import com.instructure.androidfoosball.utils.avatarColor
-import com.instructure.androidfoosball.utils.clampToBrightness
-import com.instructure.androidfoosball.utils.initials
-import com.instructure.androidfoosball.utils.unless
-
-class UserInitialsDrawable(val user: User, val size: Int = 0) : Drawable() {
-
- private val MAX_BG_BRIGHTNESS = 0.6f
- private val MAX_TEXT_WIDTH_PERCENTAGE = 0.9f
-
- private val mPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
- color = Color.WHITE
- textAlign = Paint.Align.CENTER
- typeface = Typeface.create(Typeface.DEFAULT, Typeface.BOLD)
- }
-
- override fun draw(canvas: Canvas) {
- canvas.drawColor(user.avatarColor.clampToBrightness(MAX_BG_BRIGHTNESS))
- val initials = user.initials
- val srcHyp = Math.sqrt(
- Math.pow(mPaint.measureText(initials).toDouble(), 2.0)
- + Math.pow(-mPaint.fontMetrics.ascent.toDouble(), 2.0)
- ).toFloat()
- mPaint.textSize = MAX_TEXT_WIDTH_PERCENTAGE * (mPaint.textSize * canvas.width / srcHyp)
- canvas.drawText(
- initials,
- canvas.width / 2f,
- (canvas.height - mPaint.fontMetrics.ascent - mPaint.fontMetrics.descent) / 2f,
- mPaint
- )
- }
-
- override fun getOpacity() = PixelFormat.TRANSLUCENT
- override fun setAlpha(alpha: Int) { }
- override fun setColorFilter(colorFilter: ColorFilter?) { }
- override fun getIntrinsicWidth() = bounds.width() unless {it == 0} then size
- override fun getIntrinsicHeight() = bounds.height() unless {it == 0} then size
-}
diff --git a/foosball/app/src/main/res/drawable-hdpi/ic_arrow_back_grey_700_36dp.png b/foosball/app/src/main/res/drawable-hdpi/ic_arrow_back_grey_700_36dp.png
deleted file mode 100644
index b873bdfa11..0000000000
Binary files a/foosball/app/src/main/res/drawable-hdpi/ic_arrow_back_grey_700_36dp.png and /dev/null differ
diff --git a/foosball/app/src/main/res/drawable-hdpi/ic_backspace_grey_500_48dp.png b/foosball/app/src/main/res/drawable-hdpi/ic_backspace_grey_500_48dp.png
deleted file mode 100644
index 293c729ec8..0000000000
Binary files a/foosball/app/src/main/res/drawable-hdpi/ic_backspace_grey_500_48dp.png and /dev/null differ
diff --git a/foosball/app/src/main/res/drawable-hdpi/ic_close_white_48dp.png b/foosball/app/src/main/res/drawable-hdpi/ic_close_white_48dp.png
deleted file mode 100644
index 717c7b5919..0000000000
Binary files a/foosball/app/src/main/res/drawable-hdpi/ic_close_white_48dp.png and /dev/null differ
diff --git a/foosball/app/src/main/res/drawable-hdpi/sadpanda.png b/foosball/app/src/main/res/drawable-hdpi/sadpanda.png
deleted file mode 100644
index 7bfd91bb89..0000000000
Binary files a/foosball/app/src/main/res/drawable-hdpi/sadpanda.png and /dev/null differ
diff --git a/foosball/app/src/main/res/drawable-mdpi/ic_arrow_back_grey_700_36dp.png b/foosball/app/src/main/res/drawable-mdpi/ic_arrow_back_grey_700_36dp.png
deleted file mode 100644
index 362fe33450..0000000000
Binary files a/foosball/app/src/main/res/drawable-mdpi/ic_arrow_back_grey_700_36dp.png and /dev/null differ
diff --git a/foosball/app/src/main/res/drawable-mdpi/ic_backspace_grey_500_48dp.png b/foosball/app/src/main/res/drawable-mdpi/ic_backspace_grey_500_48dp.png
deleted file mode 100644
index 112e373918..0000000000
Binary files a/foosball/app/src/main/res/drawable-mdpi/ic_backspace_grey_500_48dp.png and /dev/null differ
diff --git a/foosball/app/src/main/res/drawable-mdpi/ic_close_white_48dp.png b/foosball/app/src/main/res/drawable-mdpi/ic_close_white_48dp.png
deleted file mode 100644
index 0b00a33a72..0000000000
Binary files a/foosball/app/src/main/res/drawable-mdpi/ic_close_white_48dp.png and /dev/null differ
diff --git a/foosball/app/src/main/res/drawable-mdpi/sadpanda.png b/foosball/app/src/main/res/drawable-mdpi/sadpanda.png
deleted file mode 100644
index 536e4e7768..0000000000
Binary files a/foosball/app/src/main/res/drawable-mdpi/sadpanda.png and /dev/null differ
diff --git a/foosball/app/src/main/res/drawable-xhdpi/ic_arrow_back_grey_700_36dp.png b/foosball/app/src/main/res/drawable-xhdpi/ic_arrow_back_grey_700_36dp.png
deleted file mode 100644
index 3b412ed9a0..0000000000
Binary files a/foosball/app/src/main/res/drawable-xhdpi/ic_arrow_back_grey_700_36dp.png and /dev/null differ
diff --git a/foosball/app/src/main/res/drawable-xhdpi/ic_backspace_grey_500_48dp.png b/foosball/app/src/main/res/drawable-xhdpi/ic_backspace_grey_500_48dp.png
deleted file mode 100644
index d5b0fe5883..0000000000
Binary files a/foosball/app/src/main/res/drawable-xhdpi/ic_backspace_grey_500_48dp.png and /dev/null differ
diff --git a/foosball/app/src/main/res/drawable-xhdpi/ic_close_white_48dp.png b/foosball/app/src/main/res/drawable-xhdpi/ic_close_white_48dp.png
deleted file mode 100644
index 9ef2d8f9fb..0000000000
Binary files a/foosball/app/src/main/res/drawable-xhdpi/ic_close_white_48dp.png and /dev/null differ
diff --git a/foosball/app/src/main/res/drawable-xhdpi/sadpanda.png b/foosball/app/src/main/res/drawable-xhdpi/sadpanda.png
deleted file mode 100644
index 924f7eb8ee..0000000000
Binary files a/foosball/app/src/main/res/drawable-xhdpi/sadpanda.png and /dev/null differ
diff --git a/foosball/app/src/main/res/drawable-xxhdpi/ic_arrow_back_grey_700_36dp.png b/foosball/app/src/main/res/drawable-xxhdpi/ic_arrow_back_grey_700_36dp.png
deleted file mode 100644
index 8f2cce3d3d..0000000000
Binary files a/foosball/app/src/main/res/drawable-xxhdpi/ic_arrow_back_grey_700_36dp.png and /dev/null differ
diff --git a/foosball/app/src/main/res/drawable-xxhdpi/ic_backspace_grey_500_48dp.png b/foosball/app/src/main/res/drawable-xxhdpi/ic_backspace_grey_500_48dp.png
deleted file mode 100644
index 200b5f8be9..0000000000
Binary files a/foosball/app/src/main/res/drawable-xxhdpi/ic_backspace_grey_500_48dp.png and /dev/null differ
diff --git a/foosball/app/src/main/res/drawable-xxhdpi/ic_close_white_48dp.png b/foosball/app/src/main/res/drawable-xxhdpi/ic_close_white_48dp.png
deleted file mode 100644
index 5a99107d64..0000000000
Binary files a/foosball/app/src/main/res/drawable-xxhdpi/ic_close_white_48dp.png and /dev/null differ
diff --git a/foosball/app/src/main/res/drawable-xxhdpi/sadpanda.png b/foosball/app/src/main/res/drawable-xxhdpi/sadpanda.png
deleted file mode 100644
index a6028541e1..0000000000
Binary files a/foosball/app/src/main/res/drawable-xxhdpi/sadpanda.png and /dev/null differ
diff --git a/foosball/app/src/main/res/drawable-xxxhdpi/ic_arrow_back_grey_700_36dp.png b/foosball/app/src/main/res/drawable-xxxhdpi/ic_arrow_back_grey_700_36dp.png
deleted file mode 100644
index a1e1b1a153..0000000000
Binary files a/foosball/app/src/main/res/drawable-xxxhdpi/ic_arrow_back_grey_700_36dp.png and /dev/null differ
diff --git a/foosball/app/src/main/res/drawable-xxxhdpi/ic_backspace_grey_500_48dp.png b/foosball/app/src/main/res/drawable-xxxhdpi/ic_backspace_grey_500_48dp.png
deleted file mode 100644
index 31a11d94e8..0000000000
Binary files a/foosball/app/src/main/res/drawable-xxxhdpi/ic_backspace_grey_500_48dp.png and /dev/null differ
diff --git a/foosball/app/src/main/res/drawable-xxxhdpi/ic_close_white_48dp.png b/foosball/app/src/main/res/drawable-xxxhdpi/ic_close_white_48dp.png
deleted file mode 100644
index fa87f2514f..0000000000
Binary files a/foosball/app/src/main/res/drawable-xxxhdpi/ic_close_white_48dp.png and /dev/null differ
diff --git a/foosball/app/src/main/res/drawable/light_gray_circle.xml b/foosball/app/src/main/res/drawable/light_gray_circle.xml
deleted file mode 100644
index c002569fc9..0000000000
--- a/foosball/app/src/main/res/drawable/light_gray_circle.xml
+++ /dev/null
@@ -1,24 +0,0 @@
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/foosball/app/src/main/res/drawable/vd_add_black_24dp.xml b/foosball/app/src/main/res/drawable/vd_add_black_24dp.xml
deleted file mode 100644
index f06436f079..0000000000
--- a/foosball/app/src/main/res/drawable/vd_add_black_24dp.xml
+++ /dev/null
@@ -1,4 +0,0 @@
-
-
-
-
diff --git a/foosball/app/src/main/res/drawable/vd_group_add_white_48dp.xml b/foosball/app/src/main/res/drawable/vd_group_add_white_48dp.xml
deleted file mode 100644
index e85f005609..0000000000
--- a/foosball/app/src/main/res/drawable/vd_group_add_white_48dp.xml
+++ /dev/null
@@ -1,4 +0,0 @@
-
-
-
-
diff --git a/foosball/app/src/main/res/drawable/vd_search_black_24dp.xml b/foosball/app/src/main/res/drawable/vd_search_black_24dp.xml
deleted file mode 100644
index 56e0927aa8..0000000000
--- a/foosball/app/src/main/res/drawable/vd_search_black_24dp.xml
+++ /dev/null
@@ -1,4 +0,0 @@
-
-
-
-
diff --git a/foosball/app/src/main/res/drawable/vd_volume_up_grey_500_18dp.xml b/foosball/app/src/main/res/drawable/vd_volume_up_grey_500_18dp.xml
deleted file mode 100644
index 661fec29dd..0000000000
--- a/foosball/app/src/main/res/drawable/vd_volume_up_grey_500_18dp.xml
+++ /dev/null
@@ -1,4 +0,0 @@
-
-
-
-
diff --git a/foosball/app/src/main/res/layout/activity_change_avatar.xml b/foosball/app/src/main/res/layout/activity_change_avatar.xml
deleted file mode 100644
index ebf35e8983..0000000000
--- a/foosball/app/src/main/res/layout/activity_change_avatar.xml
+++ /dev/null
@@ -1,99 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/foosball/app/src/main/res/layout/activity_edit_player.xml b/foosball/app/src/main/res/layout/activity_edit_player.xml
deleted file mode 100644
index 8e9a16abf4..0000000000
--- a/foosball/app/src/main/res/layout/activity_edit_player.xml
+++ /dev/null
@@ -1,202 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/foosball/app/src/main/res/layout/adapter_foos_rank.xml b/foosball/app/src/main/res/layout/adapter_foos_rank.xml
deleted file mode 100644
index 3c83578237..0000000000
--- a/foosball/app/src/main/res/layout/adapter_foos_rank.xml
+++ /dev/null
@@ -1,102 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/foosball/app/src/main/res/layout/adapter_time_waster.xml b/foosball/app/src/main/res/layout/adapter_time_waster.xml
deleted file mode 100644
index 2d03657211..0000000000
--- a/foosball/app/src/main/res/layout/adapter_time_waster.xml
+++ /dev/null
@@ -1,110 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/foosball/app/src/main/res/layout/dialog_confirm_pin.xml b/foosball/app/src/main/res/layout/dialog_confirm_pin.xml
deleted file mode 100644
index e149ccbe58..0000000000
--- a/foosball/app/src/main/res/layout/dialog_confirm_pin.xml
+++ /dev/null
@@ -1,102 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/foosball/app/src/main/res/layout/dialog_team_picker.xml b/foosball/app/src/main/res/layout/dialog_team_picker.xml
deleted file mode 100644
index 62bf040ff8..0000000000
--- a/foosball/app/src/main/res/layout/dialog_team_picker.xml
+++ /dev/null
@@ -1,73 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/foosball/app/src/main/res/layout/dialog_user_picker.xml b/foosball/app/src/main/res/layout/dialog_user_picker.xml
deleted file mode 100644
index 4b40512a71..0000000000
--- a/foosball/app/src/main/res/layout/dialog_user_picker.xml
+++ /dev/null
@@ -1,73 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/foosball/app/src/main/res/layout/view_pin_pad.xml b/foosball/app/src/main/res/layout/view_pin_pad.xml
deleted file mode 100644
index 843748e25d..0000000000
--- a/foosball/app/src/main/res/layout/view_pin_pad.xml
+++ /dev/null
@@ -1,91 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/foosball/app/src/main/res/mipmap-hdpi/ic_launcher.png b/foosball/app/src/main/res/mipmap-hdpi/ic_launcher.png
deleted file mode 100755
index 3cd7753f36..0000000000
Binary files a/foosball/app/src/main/res/mipmap-hdpi/ic_launcher.png and /dev/null differ
diff --git a/foosball/app/src/main/res/mipmap-mdpi/ic_launcher.png b/foosball/app/src/main/res/mipmap-mdpi/ic_launcher.png
deleted file mode 100755
index b2872af092..0000000000
Binary files a/foosball/app/src/main/res/mipmap-mdpi/ic_launcher.png and /dev/null differ
diff --git a/foosball/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/foosball/app/src/main/res/mipmap-xhdpi/ic_launcher.png
deleted file mode 100755
index 16802cb60a..0000000000
Binary files a/foosball/app/src/main/res/mipmap-xhdpi/ic_launcher.png and /dev/null differ
diff --git a/foosball/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/foosball/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
deleted file mode 100755
index a9e0158ee4..0000000000
Binary files a/foosball/app/src/main/res/mipmap-xxhdpi/ic_launcher.png and /dev/null differ
diff --git a/foosball/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/foosball/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
deleted file mode 100755
index cbad722601..0000000000
Binary files a/foosball/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png and /dev/null differ
diff --git a/foosball/app/src/main/res/values-w820dp/dimens.xml b/foosball/app/src/main/res/values-w820dp/dimens.xml
deleted file mode 100644
index 8746fd7ce6..0000000000
--- a/foosball/app/src/main/res/values-w820dp/dimens.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-
-
-
-
- 64dp
-
diff --git a/foosball/app/src/main/res/values/attrs.xml b/foosball/app/src/main/res/values/attrs.xml
deleted file mode 100644
index 0e604f998f..0000000000
--- a/foosball/app/src/main/res/values/attrs.xml
+++ /dev/null
@@ -1,45 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/foosball/app/src/main/res/values/colors.xml b/foosball/app/src/main/res/values/colors.xml
deleted file mode 100644
index ea6139431a..0000000000
--- a/foosball/app/src/main/res/values/colors.xml
+++ /dev/null
@@ -1,30 +0,0 @@
-
-
-
-
- #383838
- #232323
- #00ACEC
- #00ACEC
- #DC3C00
- #00AC4C
- #DDD
- #fdcd20
-
diff --git a/foosball/app/src/main/res/values/dimens.xml b/foosball/app/src/main/res/values/dimens.xml
deleted file mode 100644
index 7f13e1bb14..0000000000
--- a/foosball/app/src/main/res/values/dimens.xml
+++ /dev/null
@@ -1,27 +0,0 @@
-
-
-
-
- 16dp
- 16dp
- 40dp
- 70dp
- 140dp
-
diff --git a/foosball/app/src/main/res/values/strings.xml b/foosball/app/src/main/res/values/strings.xml
deleted file mode 100644
index 6520f975d5..0000000000
--- a/foosball/app/src/main/res/values/strings.xml
+++ /dev/null
@@ -1,143 +0,0 @@
-
-
-
- Foosball
- Loading
- Wins
- Losses
- Pick a table
- Pick a user
- Pick a team
- Start Game
- Okay
- Winner Winner!
- %s won!
- All Done
- Swap sides
- Quit Game
- %d wins
- %d losses
- Leaderboard
- FoosRank
- Team
- Individual
- FoosRank Over Time
- Add Player
- Add Team
- Create Player
- Reset Teams
- Change Avatar
- Error uploading photo. Try again.
- Uploading…
- Pinch-zoom and drag to adjust your photo.
- Cancel
- Done
- Create New Player
- Display Name
- Email
- Confirm Email
- +\nAdd Avatar
- Please assign teams
- Assigning team…
- ASSIGNED TO
- Something broke, and it\'s probably your fault.
- Win Rate
- Foos Rank
- N/A
- %.1f%%
- Create Pin
- Maybe Later
- Disable for my account
- Are you sure you wish to disable PIN protection?
- PIN protection has been disabled for your account
- Your player account can now be protected with a 4-digit PIN. This will help prevent evil coworkers from changing your avatar or \'accidentally\' assigning you to a team when you\'re not around or \'accidentally\' making you lose several times.
- Enter a 4-digit PIN
- Confirm PIN
- PINs do not match. Do it again.
- Incorrect PIN
- Contact an Android engineer to reset your PIN
- PIN successfully saved
- Syncing Table Info
- New Game
- Cut-Throat
- Resume Game
- Players need at least %d games to rank
- Players need to suck less to do better
- Player rankings based on the ELO system
- Enter team name
- Best of
- rounds
- points per round
- Uneven Teams
- The teams have an uneven number of players. Do you still wish to start this game?
- Avg. Win Rate: %.1f%%
- Pause Game
- Game Paused
- Are you sure you wish to quit this game? You will NOT be able to resume it later.
- Starting next round in…
- Undo last goal
- Swap\nNow
- round
- of
- points to win
- %s wins this round!
- %s wins the game!
- Best of %d
- End Game
- Guest accounts cannot be edited
- Goal Timer
- SERVING
- Round Timer
- Game Timer
- Jordan Marshall and Brady Larson welcome your Motivosity love.]]>
- 3 players minimum
- Force rotation after
- points
- New Cut Throat Game
- NOTE: Results are not recorded for cut-throat games at this time. Player win rates will not be affected.
- Edit Player
- Custom Team Assignment Phrase
- Custom Victory Phrase
- Save Changes
- Enable PIN protection
- Change PIN
- Search
- Time Wasters
- Most rounds played
- Rounds
- %.2f hours wasted
- 4 Players Required
- Team Twister
- Which players make the best team?\nTwo players swap sides after each goal to cover all possible team combinations. First team to 4 points wins.\nUnranked.
- New Team Twister Game
- King of the Table
- Who will be King of the Table?\nPlayers change teams after every round, 3 rounds per set. The player with the most goals at the end of the game will be crowned King!\nUnranked.
- New King of the Table Game
- set(s)
- %d rounds total
- Duration: %1$d to %2$d goals
- Duration: %s goals minimum
- Warning! The table has been requested by another group.
- Table requested by %1$s on %2$s
- Table requested
- Another group wants to use the foosball table when you\'re done.\nPlease be mindful by quickening your pace and accepting the final score.
- Alternatively, if you are an unfriendly group of people, please consider taking some of the following actions to demonstrate your special qualities:
- • Play slower. There\'s no rush. Your time is the only time that matters.\n• Keep adding rounds; never accept defeat. It\'s a matter of pride, after all!\n• Generate lots of heat. Breath through your mouth so the next group doesn\'t have to play in the cold.\n• Don\'t wear deodorant. Intimidate your opponents with chemical warfare.\n• Eat a large meal right before playing. Ensure everyone enjoys the smell of company lunch a second time.\n• Eliminate any ventilation. Who knows when a stray draft might shift the ball and cost you the game?\n• Be as loud as possible. Surely you can level up your game through sheer volume.\n• Use brute strength instead of skill. Break some equipment and tell nobody.
-
diff --git a/foosball/app/src/main/res/values/styles.xml b/foosball/app/src/main/res/values/styles.xml
deleted file mode 100644
index 476ce214ec..0000000000
--- a/foosball/app/src/main/res/values/styles.xml
+++ /dev/null
@@ -1,59 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/foosball/app/src/main/res/xml/file_paths.xml b/foosball/app/src/main/res/xml/file_paths.xml
deleted file mode 100644
index 3e34f4832a..0000000000
--- a/foosball/app/src/main/res/xml/file_paths.xml
+++ /dev/null
@@ -1,24 +0,0 @@
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/foosball/app/src/phone/AndroidManifest.xml b/foosball/app/src/phone/AndroidManifest.xml
deleted file mode 100644
index bd10f5860b..0000000000
--- a/foosball/app/src/phone/AndroidManifest.xml
+++ /dev/null
@@ -1,100 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/foosball/app/src/phone/java/com/instructure/androidfoosball/activities/BaseFireBaseActivity.kt b/foosball/app/src/phone/java/com/instructure/androidfoosball/activities/BaseFireBaseActivity.kt
deleted file mode 100644
index 51505af9fc..0000000000
--- a/foosball/app/src/phone/java/com/instructure/androidfoosball/activities/BaseFireBaseActivity.kt
+++ /dev/null
@@ -1,53 +0,0 @@
-/*
- * Copyright (C) 2016 - 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.androidfoosball.activities
-
-import android.support.v7.app.AppCompatActivity
-import com.google.firebase.auth.FirebaseAuth
-import com.google.firebase.database.DatabaseReference
-import com.google.firebase.database.FirebaseDatabase
-import com.instructure.androidfoosball.interfaces.FragmentCallbacks
-import com.instructure.androidfoosball.models.User
-
-abstract class BaseFireBaseActivity : AppCompatActivity(), FragmentCallbacks {
-
- override var mUser: User? = null
- override var mAuth: FirebaseAuth? = null
- override var mDatabase: DatabaseReference? = null
-
- protected abstract fun onAuthStateChange(firebaseAuth: FirebaseAuth)
-
- protected fun initFireBase() {
- mAuth = FirebaseAuth.getInstance()
- mDatabase = FirebaseDatabase.getInstance().reference
- }
-
- private val mAuthStateListener = FirebaseAuth.AuthStateListener { firebaseAuth -> onAuthStateChange(firebaseAuth) }
-
- override fun onStop() {
- super.onStop()
- mAuth?.removeAuthStateListener(mAuthStateListener)
- }
-
- override fun onStart() {
- super.onStart()
- mAuth?.addAuthStateListener(mAuthStateListener)
- }
-}
diff --git a/foosball/app/src/phone/java/com/instructure/androidfoosball/activities/NfcReadActivity.kt b/foosball/app/src/phone/java/com/instructure/androidfoosball/activities/NfcReadActivity.kt
deleted file mode 100644
index 1a6af5ce5a..0000000000
--- a/foosball/app/src/phone/java/com/instructure/androidfoosball/activities/NfcReadActivity.kt
+++ /dev/null
@@ -1,155 +0,0 @@
-/*
- * Copyright (C) 2016 - 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.androidfoosball.activities
-
-import android.annotation.SuppressLint
-import android.graphics.Color
-import android.nfc.NdefMessage
-import android.nfc.NfcAdapter
-import android.os.Bundle
-import android.os.Handler
-import android.support.v7.app.AppCompatActivity
-import android.util.Log
-import android.view.View
-import android.widget.Toast
-import com.google.firebase.auth.FirebaseAuth
-import com.google.firebase.auth.FirebaseUser
-import com.google.firebase.database.DataSnapshot
-import com.google.firebase.database.DatabaseError
-import com.google.firebase.database.FirebaseDatabase
-import com.google.firebase.database.ValueEventListener
-import com.instructure.androidfoosball.R
-import com.instructure.androidfoosball.models.Table
-import com.instructure.androidfoosball.utils.Prefs
-import kotlinx.android.synthetic.phone.activity_nfc_read.*
-import org.jetbrains.anko.startActivity
-import org.jetbrains.anko.vibrator
-
-
-class NfcReadActivity : AppCompatActivity() {
-
- private val TAG = "FoosNFC"
-
- private val mAuth = FirebaseAuth.getInstance()
- internal lateinit var mUser: FirebaseUser
- private val mAuthListener = FirebaseAuth.AuthStateListener { firebaseAuth ->
- val user = firebaseAuth.currentUser
- if (user != null) {
- mUser = user
- parseNfc()
- } else {
- startActivity()
- finish()
- }
- }
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- vibrator.vibrate(200) // Provide haptic feedback as soon as possible
- setContentView(R.layout.activity_nfc_read)
- }
-
- private fun parseNfc() {
- try {
- val rawMsgs = intent.getParcelableArrayExtra(NfcAdapter.EXTRA_NDEF_MESSAGES)
- val foosRecord = (rawMsgs[0] as NdefMessage).records[0]
-
- val uri = foosRecord.toUri()
- val segments = uri.pathSegments
-
- Log.i(TAG, "Read NFC Tag for table " + segments[0] + ", side " + segments[1])
-
- assignTeam(segments[0], segments[1].toIntOrNull() ?: 0)
- } catch (e: Throwable) {
- try {
- val segments = intent.data.pathSegments
- Log.i(TAG, "Read URI for table " + segments[0] + ", side " + segments[1])
- assignTeam(segments[0], segments[1].toIntOrNull() ?: 0)
- } catch (e: Throwable) {
- fail(e.message ?: "Unknown error parsing NFC data")
- }
- }
- }
-
- private fun assignTeam(tableId: String, side: Int) {
-
- val db = FirebaseDatabase.getInstance().reference
- db.child("tables").child(tableId).addListenerForSingleValueEvent(object : ValueEventListener {
- override fun onDataChange(dataSnapshot: DataSnapshot) {
- val table = dataSnapshot.getValue(Table::class.java)!!
-
- val colorString: String
- val teamColor: Int
-
- colorString = if (side == 0) table.sideOneColor else table.sideTwoColor
- teamColor = Color.parseColor(colorString)
- val teamName = if (side == 0) table.sideOneName else table.sideTwoName
- val userId = Prefs(this@NfcReadActivity).userId
- Log.v(TAG, "Assigning Team " + side)
- db.child("incoming").child(tableId).child(if (side == 0) "sideOne" else "sideTwo").setValue(userId)
-
- success(table.name, teamName, teamColor)
- }
-
- override fun onCancelled(databaseError: DatabaseError) {
- fail(databaseError.message)
- }
- })
- }
-
- @SuppressLint("SetTextI18n")
- private fun success(tableName: String, teamName: String, teamColor: Int) {
- successView.setCardBackgroundColor(teamColor)
- tableLabel.text = tableName
- teamLabel.text = teamName
- loadingView.visibility = View.GONE
- successView.visibility = View.VISIBLE
- errorView.visibility = View.GONE
- finishAfterDelay()
- }
-
-
- private fun fail(message: String) {
- Toast.makeText(this, message, Toast.LENGTH_LONG).show()
- loadingView.visibility = View.GONE
- successView.visibility = View.GONE
- errorView.visibility = View.VISIBLE
- finishAfterDelay()
- }
-
- private fun finishAfterDelay() {
- Handler().postDelayed({ finish() }, 3000)
- }
-
- override fun onBackPressed() {
- // Do nothing. TRAP THE USER!!!!
- }
-
- override fun onStop() {
- super.onStop()
- mAuth.removeAuthStateListener(mAuthListener)
- }
-
- override fun onStart() {
- super.onStart()
- mAuth.addAuthStateListener(mAuthListener)
- }
-
-}
diff --git a/foosball/app/src/phone/java/com/instructure/androidfoosball/activities/PrimaryActivity.kt b/foosball/app/src/phone/java/com/instructure/androidfoosball/activities/PrimaryActivity.kt
deleted file mode 100644
index 1ea41a5f39..0000000000
--- a/foosball/app/src/phone/java/com/instructure/androidfoosball/activities/PrimaryActivity.kt
+++ /dev/null
@@ -1,205 +0,0 @@
-/*
- * Copyright (C) 2016 - 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.androidfoosball.activities
-
-import android.content.Context
-import android.content.Intent
-import android.os.Bundle
-import android.support.v4.app.Fragment
-import android.support.v4.app.FragmentManager
-import android.support.v4.app.FragmentPagerAdapter
-import android.view.Menu
-import android.view.MenuItem
-import android.view.View
-import android.view.inputmethod.InputMethodManager
-import com.google.firebase.auth.FirebaseAuth
-import com.instructure.androidfoosball.R
-import com.instructure.androidfoosball.fragments.TableFragment
-import com.instructure.androidfoosball.fragments.UserFragment
-import com.instructure.androidfoosball.interfaces.TextEditCallback
-import com.instructure.androidfoosball.models.User
-import com.instructure.androidfoosball.utils.AnimUtils
-import com.instructure.androidfoosball.utils.Const
-import com.instructure.androidfoosball.utils.FireUtils
-import com.instructure.androidfoosball.utils.Prefs
-import kotlinx.android.synthetic.phone.activity_primary.*
-import org.jetbrains.anko.startActivity
-import java.util.*
-
-class PrimaryActivity : BaseFireBaseActivity(), TextEditCallback {
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- setContentView(R.layout.activity_primary)
- setSupportActionBar(toolbar)
- initFireBase()
- setupUser()
-
- pager.offscreenPageLimit = 2
- pager.adapter = TabsPagerAdapter(supportFragmentManager)
- tabs.setupWithViewPager(pager)
-
- if (SignInActivity.ACTION_SHORTCUT_TABLES == intent.action)
- pager.currentItem = PAGE_TABLES
-
- phraseDone.setOnClickListener(mPhraseDoneClickListener)
- }
-
- private fun setupUser() {
- val user = intent.extras.getParcelable(Const.USER)
- if (user == null) {
- startActivity()
- finish()
- } else {
- mUser = user
- }
- }
-
- override fun onAuthStateChange(firebaseAuth: FirebaseAuth) {
- if (firebaseAuth.currentUser == null) {
- Prefs(this).userId = ""
- startActivity()
- finish()
- }
- }
-
- override fun onCreateOptionsMenu(menu: Menu): Boolean {
- menuInflater.inflate(R.menu.menu_logout, menu)
- return super.onCreateOptionsMenu(menu)
- }
-
- override fun onOptionsItemSelected(item: MenuItem): Boolean {
- if (item.itemId == R.id.logout) {
- mAuth?.signOut()
- return true
- }
- return super.onOptionsItemSelected(item)
- }
-
- inner class TabsPagerAdapter(fm: FragmentManager) : FragmentPagerAdapter(fm) {
-
- override fun getItem(position: Int): Fragment {
- when (position) {
- PAGE_USER -> return UserFragment.newInstance()
- else -> return TableFragment.newInstance()
- }
- }
-
- override fun getCount(): Int {
- return 2
- }
-
- override fun getPageTitle(position: Int): CharSequence {
- when (position) {
- PAGE_USER -> return mUser?.name?.toUpperCase() ?: ""
- else -> return getString(R.string.tables).toUpperCase(Locale.getDefault())
- }
- }
- }
-
- //region TextEditCallbacks
-
- override fun requestTextEdit(resId_requester: Int, text: String) {
- phraseDone.tag = resId_requester
- phraseEditText.setText(text)
- phraseEditText.setSelection(phraseEditText.text.length)
- if (phraseEditWrapper.visibility != View.VISIBLE) {
- showKeyboard()
- }
- AnimUtils.fadeIn(360, phraseEditWrapper)
- }
-
- private val mPhraseDoneClickListener = View.OnClickListener { v ->
- val resId_requester = v.tag as Int
- val fragment = supportFragmentManager.findFragmentByTag("android:switcher:" + R.id.pager + ":" + 0)
- when (resId_requester) {
- R.id.startupPhrase -> {
- FireUtils.setStartupPhrase(mUser!!.id, mDatabase!!, phraseEditText.text.toString())
- if (fragment is UserFragment) {
- fragment.updateStartupPhraseText(phraseEditText.text.toString())
- }
- }
- R.id.victoryPhrase -> {
- FireUtils.setVictoryPhrase(mUser!!.id, mDatabase!!, phraseEditText.text.toString())
- if (fragment is UserFragment) {
- fragment.updateVictoryPhraseText(phraseEditText.text.toString())
- }
- }
- }
- hideKeyboard()
- AnimUtils.fadeOut(360, phraseEditWrapper)
- }
-
- //endregion
-
- override fun onBackPressed() {
- if (phraseEditWrapper.visibility == View.VISIBLE) {
- AnimUtils.fadeOut(360, phraseEditWrapper)
- return
- }
- super.onBackPressed()
- }
-
- private fun showKeyboard() {
- val imm = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
- imm.toggleSoftInput(InputMethodManager.SHOW_FORCED, 0)
- }
-
- private fun hideKeyboard() {
- val view = this.currentFocus
- if (view != null) {
- val imm = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
- imm.hideSoftInputFromWindow(view.windowToken, 0)
- }
- }
-
- companion object {
-
- val PAGE_USER = 0
- val PAGE_TABLES = 1
-
- fun createIntent(context: Context, user: User): Intent {
- val intent = Intent(context, PrimaryActivity::class.java)
- val bundle = Bundle()
- bundle.putParcelable(Const.USER, user)
- intent.putExtras(bundle)
- return intent
- }
-
- /**
- * Creates an intent with the data provided
- * @param context Where we are coming from
- * *
- * @param user User's info
- * *
- * @param action The action
- * *
- * @return A fully armed and operational intent
- */
- fun createIntent(context: Context, user: User, action: String): Intent {
- val intent = Intent(context, PrimaryActivity::class.java)
- intent.action = action
- val bundle = Bundle()
- bundle.putParcelable(Const.USER, user)
- intent.putExtras(bundle)
- return intent
- }
- }
-}
diff --git a/foosball/app/src/phone/java/com/instructure/androidfoosball/activities/SignInActivity.kt b/foosball/app/src/phone/java/com/instructure/androidfoosball/activities/SignInActivity.kt
deleted file mode 100644
index da13da0ad5..0000000000
--- a/foosball/app/src/phone/java/com/instructure/androidfoosball/activities/SignInActivity.kt
+++ /dev/null
@@ -1,208 +0,0 @@
-/*
- * Copyright (C) 2016 - 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.androidfoosball.activities
-
-import android.app.ProgressDialog
-import android.content.Intent
-import android.os.Bundle
-import android.util.Log
-import android.widget.Toast
-import com.google.android.gms.auth.api.Auth
-import com.google.android.gms.auth.api.signin.GoogleSignInAccount
-import com.google.android.gms.auth.api.signin.GoogleSignInOptions
-import com.google.android.gms.auth.api.signin.GoogleSignInResult
-import com.google.android.gms.common.ConnectionResult
-import com.google.android.gms.common.api.GoogleApiClient
-import com.google.firebase.auth.FirebaseAuth
-import com.google.firebase.auth.GoogleAuthProvider
-import com.google.firebase.database.DataSnapshot
-import com.google.firebase.database.DatabaseError
-import com.google.firebase.database.ValueEventListener
-import com.instructure.androidfoosball.R
-import com.instructure.androidfoosball.models.User
-import com.instructure.androidfoosball.utils.Prefs
-import kotlinx.android.synthetic.phone.activity_sign_in.*
-import org.jetbrains.anko.sdk21.listeners.onClick
-
-
-class SignInActivity : BaseFireBaseActivity(), GoogleApiClient.OnConnectionFailedListener {
-
- private var mGoogleApiClient: GoogleApiClient? = null
- private var mProgressDialog: ProgressDialog? = null
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- setContentView(R.layout.activity_sign_in)
- setupListeners()
-
- // Configure sign-in to request the user's ID, email address, and basic
- // profile. ID and basic profile are included in DEFAULT_SIGN_IN.
- val gso = GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN).requestIdToken(getString(R.string.default_web_client_id)).requestEmail().build()
-
- // Build a GoogleApiClient with access to the Google Sign-In API and the
- // options specified by gso.
- mGoogleApiClient = GoogleApiClient.Builder(this).enableAutoManage(this /* FragmentActivity */, this /* OnConnectionFailedListener */).addApi(Auth.GOOGLE_SIGN_IN_API, gso).build()
-
- initFireBase()
- }
-
- override fun onAuthStateChange(firebaseAuth: FirebaseAuth) {
- val firebaseUser = firebaseAuth.currentUser
- if (firebaseUser != null) {
- // User is signed in
- Log.d(TAG, "onAuthStateChanged:signed_in:" + firebaseUser.uid)
- finish()
- overridePendingTransition(0, 0)
-
- val user = User(
- id = Prefs(this).userId,
- name = firebaseUser.displayName ?: "",
- email = firebaseUser.email ?: "",
- avatar = firebaseUser.photoUrl?.toString() ?: ""
- )
-
- startActivity(PrimaryActivity.createIntent(this@SignInActivity, user, intent.action))
- } else {
- // User is signed out
- Log.d(TAG, "onAuthStateChanged:signed_out")
- }
- }
-
-
- public override fun onStop() {
- super.onStop()
- hideProgressDialog()
- }
-
- private fun firebaseAuthWithGoogle(acct: GoogleSignInAccount, user: User) {
- Log.d(TAG, "firebaseAuthWithGoogle:" + acct.id!!)
- showProgressDialog()
-
- val credential = GoogleAuthProvider.getCredential(acct.idToken, null)
- mAuth?.signInWithCredential(credential)?.addOnCompleteListener(this) { task ->
- Log.d(TAG, "signInWithCredential:onComplete:" + task.isSuccessful)
-
- // If sign in fails, display a message to the user. If sign in succeeds
- // the auth state listener will be notified and logic to handle the
- // signed in user can be handled in the listener.
- if (!task.isSuccessful) {
- Log.w(TAG, "signInWithCredential", task.exception)
- Toast.makeText(this@SignInActivity, "Authentication failed.",
- Toast.LENGTH_SHORT).show()
- } else {
- postUser(acct, user)
- }
- hideProgressDialog()
- }
- }
-
- private fun postUser(acct: GoogleSignInAccount, user: User) {
- mDatabase!!.child("users").orderByChild("email").equalTo(user.email).limitToFirst(1).addListenerForSingleValueEvent(object : ValueEventListener {
- override fun onDataChange(dataSnapshot: DataSnapshot) {
- var acctId = acct.id ?: ""
- if (dataSnapshot.childrenCount == 0L) {
- mDatabase!!.child("users").child(acctId).setValue(user)
- } else {
- acctId = dataSnapshot.children.iterator().next().getValue(User::class.java)!!.id
- user.id = acctId
- }
- Prefs(this@SignInActivity).userId = acctId
- finish()
- overridePendingTransition(0, 0)
- startActivity(PrimaryActivity.createIntent(this@SignInActivity, user))
- }
-
- override fun onCancelled(databaseError: DatabaseError) {
- }
-
- })
- }
-
- fun showProgressDialog() {
- if (mProgressDialog == null) {
- mProgressDialog = ProgressDialog(this)
- mProgressDialog!!.setMessage(getString(R.string.loading))
- mProgressDialog!!.isIndeterminate = true
- }
-
- mProgressDialog!!.show()
- }
-
- fun hideProgressDialog() {
- if (mProgressDialog != null && mProgressDialog!!.isShowing) {
- mProgressDialog!!.dismiss()
- }
- }
-
- override fun onConnectionFailed(connectionResult: ConnectionResult) {
- Toast.makeText(this, "Connection failed: " + connectionResult.errorMessage!!, Toast.LENGTH_LONG).show()
- }
-
-
- private fun setupListeners() {
- sign_in_button.onClick { signIn() }
- }
-
- private fun signIn() {
- val signInIntent = Auth.GoogleSignInApi.getSignInIntent(mGoogleApiClient)
- startActivityForResult(signInIntent, RC_SIGN_IN)
- }
-
- private fun handleSignInResult(result: GoogleSignInResult, user: User) {
- Log.d("abcde", "handleSignInResult:" + result.isSuccess)
- if (result.isSuccess) {
- // Signed in successfully, show authenticated UI.
- val acct = result.signInAccount
- Toast.makeText(this@SignInActivity, "Signed in " + acct!!.displayName + " " + acct.photoUrl, Toast.LENGTH_SHORT).show()
- firebaseAuthWithGoogle(acct, user)
-
- //updateUI(true);
- } else {
- // Signed out, show unauthenticated UI.
- //updateUI(false);
- }
- }
-
- public override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent) {
- super.onActivityResult(requestCode, resultCode, data)
-
- // Result returned from launching the Intent from GoogleSignInApi.getSignInIntent(...);
- if (requestCode == RC_SIGN_IN) {
- val result = Auth.GoogleSignInApi.getSignInResultFromIntent(data)
- //add the user to the database
- val user = User()
- result.signInAccount?.let { acct ->
- user.id = acct.id ?: ""
- user.name = acct.displayName ?: ""
- user.email = acct.email ?: ""
- user.avatar = acct.photoUrl?.toString() ?: ""
- }
- handleSignInResult(result, user)
- }
- }
-
- companion object {
-
- private val TAG = "SignInActivity"
-
- val ACTION_SHORTCUT_TABLES = "com.instructure.androidfoosball.SHORTCUT_TABLES"
- private val RC_SIGN_IN = 9001
- }
-}
diff --git a/foosball/app/src/phone/java/com/instructure/androidfoosball/adapters/TableFireRecyclerAdapter.kt b/foosball/app/src/phone/java/com/instructure/androidfoosball/adapters/TableFireRecyclerAdapter.kt
deleted file mode 100644
index 1be9d7d360..0000000000
--- a/foosball/app/src/phone/java/com/instructure/androidfoosball/adapters/TableFireRecyclerAdapter.kt
+++ /dev/null
@@ -1,49 +0,0 @@
-/*
- * Copyright (C) 2016 - 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.androidfoosball.adapters
-
-import android.content.Context
-import com.firebase.ui.database.FirebaseRecyclerAdapter
-import com.google.firebase.database.DataSnapshot
-import com.google.firebase.database.DatabaseReference
-import com.instructure.androidfoosball.R
-import com.instructure.androidfoosball.holders.TableViewHolder
-import com.instructure.androidfoosball.models.Table
-import java.lang.ref.WeakReference
-
-
-class TableFireRecyclerAdapter(context: Context, ref: DatabaseReference) : FirebaseRecyclerAdapter(Table::class.java, R.layout.adapter_table, TableViewHolder::class.java, ref) {
-
- var currentPreferredTable: Int = -1
- var mContext: WeakReference = WeakReference(context)
-
- override fun parseSnapshot(snapshot: DataSnapshot?): Table {
- return super.parseSnapshot(snapshot).apply { id = snapshot?.key.orEmpty() }
- }
-
- override fun populateViewHolder(holder: TableViewHolder, table: Table, position: Int) {
- holder.bind(mContext.get(), table, adapterCallback = { pos ->
- notifyItemChanged(pos)
- if (currentPreferredTable != -1)
- notifyItemChanged(currentPreferredTable)
- currentPreferredTable = pos
- })
- }
-}
diff --git a/foosball/app/src/phone/java/com/instructure/androidfoosball/decorators/SpacesItemDecoration.kt b/foosball/app/src/phone/java/com/instructure/androidfoosball/decorators/SpacesItemDecoration.kt
deleted file mode 100644
index f1a99d8354..0000000000
--- a/foosball/app/src/phone/java/com/instructure/androidfoosball/decorators/SpacesItemDecoration.kt
+++ /dev/null
@@ -1,44 +0,0 @@
-/*
- * Copyright (C) 2016 - 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.androidfoosball.decorators
-
-import android.content.Context
-import android.graphics.Rect
-import android.support.annotation.DimenRes
-import android.support.v7.widget.RecyclerView
-import android.view.View
-
-class SpacesItemDecoration(context: Context, @DimenRes spaceResId: Int) : RecyclerView.ItemDecoration() {
-
- private val space: Int
-
- init {
- this.space = context.resources.getDimensionPixelOffset(spaceResId)
- }
-
- override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State?) {
- outRect.left = space
- outRect.right = space
- outRect.bottom = space
-
- if (parent.getChildAdapterPosition(view) == 0)
- outRect.top = space
- }
-}
diff --git a/foosball/app/src/phone/java/com/instructure/androidfoosball/fragments/TableFragment.kt b/foosball/app/src/phone/java/com/instructure/androidfoosball/fragments/TableFragment.kt
deleted file mode 100644
index 1e9bcca865..0000000000
--- a/foosball/app/src/phone/java/com/instructure/androidfoosball/fragments/TableFragment.kt
+++ /dev/null
@@ -1,69 +0,0 @@
-/*
- * Copyright (C) 2016 - 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.androidfoosball.fragments
-
-import android.content.Context
-import android.os.Bundle
-import android.support.v4.app.Fragment
-import android.support.v7.widget.LinearLayoutManager
-import android.view.LayoutInflater
-import android.view.View
-import android.view.ViewGroup
-import com.instructure.androidfoosball.R
-import com.instructure.androidfoosball.adapters.TableFireRecyclerAdapter
-import com.instructure.androidfoosball.decorators.SpacesItemDecoration
-import com.instructure.androidfoosball.interfaces.FragmentCallbacks
-import kotlinx.android.synthetic.phone.fragment_tables.*
-
-
-class TableFragment : Fragment() {
-
- lateinit private var mCallbacks: FragmentCallbacks
- private var mAdapter: TableFireRecyclerAdapter? = null
-
- override fun onAttach(context: Context) {
- super.onAttach(context)
- mCallbacks = context as FragmentCallbacks
- }
-
- override fun onCreateView(inflater: LayoutInflater?, container: ViewGroup?, savedInstanceState: Bundle?): View? {
- val rootView = inflater!!.inflate(R.layout.fragment_tables, container, false)
- return rootView
- }
-
- override fun onActivityCreated(savedInstanceState: Bundle?) {
- super.onActivityCreated(savedInstanceState)
- recyclerView.layoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)
- recyclerView.addItemDecoration(SpacesItemDecoration(context, R.dimen.card_spacing))
- mAdapter = TableFireRecyclerAdapter(context, mCallbacks.mDatabase!!.child("tables"))
- recyclerView.adapter = mAdapter
- }
-
- override fun onDestroy() {
- super.onDestroy()
- mAdapter?.cleanup()
- }
-
- companion object {
- fun newInstance(): TableFragment {
- return TableFragment()
- }
- }
-}
diff --git a/foosball/app/src/phone/java/com/instructure/androidfoosball/fragments/UserFragment.kt b/foosball/app/src/phone/java/com/instructure/androidfoosball/fragments/UserFragment.kt
deleted file mode 100644
index f0e3d4259c..0000000000
--- a/foosball/app/src/phone/java/com/instructure/androidfoosball/fragments/UserFragment.kt
+++ /dev/null
@@ -1,171 +0,0 @@
-/*
- * Copyright (C) 2016 - 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.androidfoosball.fragments
-
-import android.app.Activity
-import android.content.Context
-import android.content.Intent
-import android.os.Bundle
-import android.support.v4.app.Fragment
-import android.support.v4.widget.SwipeRefreshLayout
-import android.text.TextUtils
-import android.view.LayoutInflater
-import android.view.View
-import android.view.ViewGroup
-import com.google.firebase.database.DataSnapshot
-import com.google.firebase.database.DatabaseError
-import com.google.firebase.database.ValueEventListener
-import com.instructure.androidfoosball.R
-import com.instructure.androidfoosball.activities.ChangeAvatarActivity
-import com.instructure.androidfoosball.activities.Mode
-import com.instructure.androidfoosball.interfaces.FragmentCallbacks
-import com.instructure.androidfoosball.interfaces.TextEditCallback
-import com.instructure.androidfoosball.models.User
-import com.instructure.androidfoosball.utils.AnimUtils
-import com.instructure.androidfoosball.utils.FireUtils
-import kotlinx.android.synthetic.phone.fragment_user.*
-import com.squareup.picasso.Picasso
-
-
-class UserFragment : Fragment(), SwipeRefreshLayout.OnRefreshListener {
-
- private var mCallbacks: FragmentCallbacks? = null
- private var mTextEditCallbacks: TextEditCallback? = null
-
- override fun onAttach(context: Context?) {
- super.onAttach(context)
- mCallbacks = context as FragmentCallbacks?
- mTextEditCallbacks = context as TextEditCallback?
- }
-
- override fun onCreateView(inflater: LayoutInflater?, container: ViewGroup?, savedInstanceState: Bundle?): View? {
- val rootView = inflater!!.inflate(R.layout.fragment_user, container, false)
- return rootView
- }
-
- override fun onViewCreated(view: View?, savedInstanceState: Bundle?) {
- swipeRefreshLayout.setOnRefreshListener(this)
- }
-
- override fun onActivityCreated(savedInstanceState: Bundle?) {
- super.onActivityCreated(savedInstanceState)
- loadData()
- }
-
- private fun loadData() {
- winProgress.visibility = View.VISIBLE
- lossProgress.visibility = View.VISIBLE
-
- val user = mCallbacks?.mUser
- if (user != null) {
- userEmail.text = user.email
- setupAvatar(user)
- setupWinLossCount(user)
- setupPhrase(user)
- }
- }
-
- private fun setupAvatar(user: User) {
- val ref = mCallbacks!!.mDatabase!!.child("users").child(user.id).child("avatar")
- ref.keepSynced(false)
- ref.addValueEventListener(object : ValueEventListener {
- override fun onDataChange(dataSnapshot: DataSnapshot) {
- val url = dataSnapshot.getValue(String::class.java)
- Picasso.with(context).load(url).error(R.drawable.sadpanda).into(avatar)
- }
-
- override fun onCancelled(databaseError: DatabaseError) {
- Picasso.with(context).load(R.drawable.sadpanda).into(avatar)
- }
- })
- if (!TextUtils.isEmpty(user.avatar)) {
- Picasso.with(context).load(user.avatar).placeholder(R.drawable.sadpanda).error(R.drawable.sadpanda).into(avatar)
- } else {
- Picasso.with(context).load(R.drawable.sadpanda).into(avatar)
- }
-
- avatar.setOnClickListener {
- mCallbacks?.mUser?.let { u ->
- startActivityForResult(Intent(ChangeAvatarActivity.createIntent(activity, u.id, Mode.CAMERA)), REQUEST_CODE_TAKE_PICTURE)
- }
- }
- }
-
- private fun setupWinLossCount(user: User) {
- FireUtils.getWinCount(user.id, mCallbacks!!.mDatabase!!) { value ->
- win.text = value.toString()
- winProgress!!.visibility = View.INVISIBLE
- }
-
- FireUtils.getLossCount(user.id, mCallbacks!!.mDatabase!!) { value ->
- loss.text = value.toString()
- lossProgress!!.visibility = View.INVISIBLE
- }
- }
-
- private fun setupPhrase(user: User) {
- FireUtils.getVictoryPhrase(user.id, mCallbacks!!.mDatabase!!) { value ->
- victoryPhrase.text = value
- swipeRefreshLayout.isRefreshing = false
- AnimUtils.fadeIn(320, victoryPhraseCard)
- }
-
- FireUtils.getStartupPhrase(user.id, mCallbacks!!.mDatabase!!) { value ->
- startupPhrase!!.text = value
- AnimUtils.fadeIn(320, startupPhraseCard)
- }
-
- victoryEdit.setOnClickListener { mTextEditCallbacks!!.requestTextEdit(victoryPhrase.id, victoryPhrase.text.toString()) }
- startupEdit.setOnClickListener { mTextEditCallbacks!!.requestTextEdit(startupPhrase.id, startupPhrase.text.toString()) }
- }
-
- fun updateStartupPhraseText(text: String) {
- startupPhrase.text = text
- }
-
- fun updateVictoryPhraseText(text: String) {
- victoryPhrase.text = text
- }
-
- override fun onRefresh() {
- swipeRefreshLayout.isRefreshing = true
- loadData()
- }
-
- override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
- super.onActivityResult(requestCode, resultCode, data)
-
- if (requestCode == REQUEST_CODE_TAKE_PICTURE && resultCode == Activity.RESULT_OK && data != null) {
- val avatarUrl = data.getStringExtra(ChangeAvatarActivity.EXTRA_AVATAR_URL)
- if (!TextUtils.isEmpty(avatarUrl)) {
- Picasso.with(context).load(avatarUrl).placeholder(R.drawable.sadpanda).error(R.drawable.sadpanda).into(avatar)
- }
- }
- }
-
- companion object {
-
- fun newInstance(): UserFragment {
- return UserFragment()
- }
-
- private val REQUEST_CODE_TAKE_PICTURE = 1337
- }
-}
diff --git a/foosball/app/src/phone/java/com/instructure/androidfoosball/holders/TableViewHolder.kt b/foosball/app/src/phone/java/com/instructure/androidfoosball/holders/TableViewHolder.kt
deleted file mode 100644
index 721c3fdf4c..0000000000
--- a/foosball/app/src/phone/java/com/instructure/androidfoosball/holders/TableViewHolder.kt
+++ /dev/null
@@ -1,133 +0,0 @@
-/*
- * Copyright (C) 2016 - 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.androidfoosball.holders
-
-
-import android.content.Context
-import android.support.v7.widget.RecyclerView
-import android.util.Log
-import android.view.View
-import com.google.firebase.database.FirebaseDatabase
-import com.google.firebase.messaging.FirebaseMessaging
-import com.instructure.androidfoosball.R
-import com.instructure.androidfoosball.interfaces.FragmentCallbacks
-import com.instructure.androidfoosball.models.Table
-import com.instructure.androidfoosball.models.User
-import com.instructure.androidfoosball.utils.Prefs
-import com.squareup.picasso.Picasso
-import de.hdodenhof.circleimageview.CircleImageView
-import kotlinx.android.synthetic.phone.adapter_table.view.*
-
-
-class TableViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
-
- fun bind(context: Context?, table: Table, adapterCallback: (Int) -> Unit) {
-
- itemView.tableLabel.text = table.name
- val currentGame = table.currentGame
-
- if (context != null) {
- when (currentGame) {
- "FREE" -> {
- //Table not busy
- itemView.tableStatusResult.text = context.getString(R.string.status_free)
- itemView.cardGameState.visibility = View.GONE
- }
- "TABLE_KING" -> {
- itemView.tableStatusResult.text = context.getString(R.string.tableKing)
- itemView.cardGameState.visibility = View.GONE
- }
- "TEAM_TWISTER" -> {
- itemView.tableStatusResult.text = context.getString(R.string.teamTwister)
- itemView.cardGameState.visibility = View.GONE
- }
- "BUSY" -> {
- //Table busy
- itemView.tableStatusResult.text = context.getString(R.string.status_busy)
- itemView.cardGameState.visibility = View.VISIBLE
- itemView.bestOfCount.text = table.currentBestOf
- itemView.roundCount.text = table.currentRound
- itemView.pointsCount.text = table.currentPointsToWin
- itemView.teamOneScore.text = table.currentScoreTeamOne
- itemView.teamTwoScore.text = table.currentScoreTeamTwo
-
- // Reset avatar views
- with(itemView) { listOf(playerOne, playerTwo, playerThree, playerFour).forEach { it.visibility = View.INVISIBLE }}
-
- safeLet(table.teamOne, table.teamTwo) { teamOne, teamTwo ->
- fun setTeamAvatars(users: List, vararg views: CircleImageView) {
- for ( (user, view) in users.zip(views)) {
- view.visibility = View.VISIBLE
- if (user.avatar.isNullOrBlank()) {
- view.setImageResource(R.drawable.sadpanda)
- } else {
- Picasso.with(view.context).load(user.avatar).error(R.drawable.sadpanda).into(view)
- }
- }
- }
- setTeamAvatars(teamOne.users.reversed(), itemView.playerTwo, itemView.playerOne)
- setTeamAvatars(teamTwo.users, itemView.playerThree, itemView.playerFour)
- }
-
- itemView.buttonNotifyWhenDone.setOnClickListener {
- if (!table.pushId.isBlank()) {
- FirebaseMessaging.getInstance().subscribeToTopic(table.pushId)
- } else {
- Log.e("push", "Table PushId was null cannot subscribe to topic")
- }
- (context as? FragmentCallbacks)?.mUser?.let { user ->
- val ref = FirebaseDatabase.getInstance().reference.child("incoming").child(table.id)
- ref.updateChildren(
- mapOf(
- "tableRequestUserId" to user.id,
- "tableRequestTime" to System.currentTimeMillis().toString()
- )
- )
- }
- }
-
- }
- else -> {
- itemView.tableStatusResult.text = context.getString(R.string.status_unknown)
- itemView.cardGameState.visibility = View.GONE
- }
- }
-
- if (Prefs(context).preferredTableId == table.pushId)
- itemView.preferredTable.visibility = View.VISIBLE
- else itemView.preferredTable.visibility = View.GONE
-
- itemView.rootView.setOnLongClickListener {
- Prefs(context).preferredTableId = table.pushId
- itemView.preferredTable.visibility = View.VISIBLE
- adapterCallback(adapterPosition)
- true
- }
- }
- }
-
- fun safeLet(p1: T1?, p2: T2?, block: (T1, T2)-> Unit): Boolean {
- if (p1 != null && p2 != null) {
- block(p1, p2)
- return true
- }
- return false
- }
-}
diff --git a/foosball/app/src/phone/java/com/instructure/androidfoosball/interfaces/FragmentCallbacks.kt b/foosball/app/src/phone/java/com/instructure/androidfoosball/interfaces/FragmentCallbacks.kt
deleted file mode 100644
index 00307faa60..0000000000
--- a/foosball/app/src/phone/java/com/instructure/androidfoosball/interfaces/FragmentCallbacks.kt
+++ /dev/null
@@ -1,30 +0,0 @@
-/*
- * Copyright (C) 2016 - 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.androidfoosball.interfaces
-
-import com.google.firebase.auth.FirebaseAuth
-import com.google.firebase.database.DatabaseReference
-import com.instructure.androidfoosball.models.User
-
-interface FragmentCallbacks {
- var mUser: User?
- var mAuth: FirebaseAuth?
- var mDatabase: DatabaseReference?
-}
diff --git a/foosball/app/src/phone/java/com/instructure/androidfoosball/interfaces/TextEditCallback.kt b/foosball/app/src/phone/java/com/instructure/androidfoosball/interfaces/TextEditCallback.kt
deleted file mode 100644
index 2afe3f352a..0000000000
--- a/foosball/app/src/phone/java/com/instructure/androidfoosball/interfaces/TextEditCallback.kt
+++ /dev/null
@@ -1,25 +0,0 @@
-/*
- * Copyright (C) 2016 - 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.androidfoosball.interfaces
-
-
-interface TextEditCallback {
- fun requestTextEdit(resId_requester: Int, text: String)
-}
diff --git a/foosball/app/src/phone/java/com/instructure/androidfoosball/ktmodels/User.kt b/foosball/app/src/phone/java/com/instructure/androidfoosball/ktmodels/User.kt
deleted file mode 100644
index 8c8c714d46..0000000000
--- a/foosball/app/src/phone/java/com/instructure/androidfoosball/ktmodels/User.kt
+++ /dev/null
@@ -1,36 +0,0 @@
-/*
- * Copyright (C) 2016 - 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.androidfoosball.ktmodels
-
-
-class User(
- var id: String = "",
- var name: String = "",
- var email: String = "",
- var avatar: String = "",
- var pinHash: String = "",
- var pinDisabled: String = "",
- var guest: Boolean = false,
- var customAssignmentPhrase: String = "",
- var customVictoryPhrase: String = "",
- var foosRanking: Int = 0,
- var wins: Int = 0,
- var losses: Int = 0
-)
diff --git a/foosball/app/src/phone/java/com/instructure/androidfoosball/models/Game.kt b/foosball/app/src/phone/java/com/instructure/androidfoosball/models/Game.kt
deleted file mode 100644
index 86241b9e3c..0000000000
--- a/foosball/app/src/phone/java/com/instructure/androidfoosball/models/Game.kt
+++ /dev/null
@@ -1,29 +0,0 @@
-/*
- * Copyright (C) 2016 - 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.androidfoosball.models
-
-import android.os.Parcelable
-import kotlinx.android.parcel.Parcelize
-
-@Parcelize
-class Game(
- var teamList: List = arrayListOf(),
- var table: String = ""
-) : Parcelable
diff --git a/foosball/app/src/phone/java/com/instructure/androidfoosball/models/Table.kt b/foosball/app/src/phone/java/com/instructure/androidfoosball/models/Table.kt
deleted file mode 100644
index a4bc075e45..0000000000
--- a/foosball/app/src/phone/java/com/instructure/androidfoosball/models/Table.kt
+++ /dev/null
@@ -1,42 +0,0 @@
-/*
- * Copyright (C) 2016 - 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.androidfoosball.models
-
-import android.os.Parcelable
-import kotlinx.android.parcel.Parcelize
-
-@Parcelize
-class Table (
- var id: String = "",
- var currentBestOf: String = "",
- var currentGame: String = "",
- var currentPointsToWin: String = "",
- var currentRound: String = "",
- var currentScoreTeamOne: String = "",
- var currentScoreTeamTwo: String = "",
- var name: String = "",
- var sideOneColor: String = "",
- var sideOneName: String = "",
- var sideTwoColor: String = "",
- var sideTwoName: String = "",
- var pushId: String = "",
- var teamOne: TableTeam? = null,
- var teamTwo: TableTeam? = null
-) : Parcelable
diff --git a/foosball/app/src/phone/java/com/instructure/androidfoosball/models/TableTeam.kt b/foosball/app/src/phone/java/com/instructure/androidfoosball/models/TableTeam.kt
deleted file mode 100644
index 5f1de0c4ae..0000000000
--- a/foosball/app/src/phone/java/com/instructure/androidfoosball/models/TableTeam.kt
+++ /dev/null
@@ -1,30 +0,0 @@
-/*
- * Copyright (C) 2016 - 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.androidfoosball.models
-
-import android.os.Parcelable
-import kotlinx.android.parcel.Parcelize
-
-@Parcelize
-class TableTeam(
- var averageWinRate: Float = 0f,
- var customName: String = "",
- var users: List = listOf()
-) : Parcelable
diff --git a/foosball/app/src/phone/java/com/instructure/androidfoosball/models/Team.kt b/foosball/app/src/phone/java/com/instructure/androidfoosball/models/Team.kt
deleted file mode 100644
index 3da2f071f8..0000000000
--- a/foosball/app/src/phone/java/com/instructure/androidfoosball/models/Team.kt
+++ /dev/null
@@ -1,31 +0,0 @@
-/*
- * Copyright (C) 2016 - 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.androidfoosball.models
-
-import android.os.Parcelable
-import kotlinx.android.parcel.Parcelize
-
-@Parcelize
-class Team(
- var id: String = "",
- var users: List = arrayListOf(),
- var score: Int = 0,
- var side: String = ""
-) : Parcelable
diff --git a/foosball/app/src/phone/java/com/instructure/androidfoosball/models/User.kt b/foosball/app/src/phone/java/com/instructure/androidfoosball/models/User.kt
deleted file mode 100644
index 7b8f1f696b..0000000000
--- a/foosball/app/src/phone/java/com/instructure/androidfoosball/models/User.kt
+++ /dev/null
@@ -1,33 +0,0 @@
-/*
- * Copyright (C) 2016 - 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.androidfoosball.models
-
-import android.os.Parcelable
-import kotlinx.android.parcel.Parcelize
-
-@Parcelize
-class User(
- var id: String = "",
- var name: String = "",
- var email: String = "",
- var avatar: String = "",
- var wins: Int = 0,
- var losses: Int = 0
-) : Parcelable
diff --git a/foosball/app/src/phone/java/com/instructure/androidfoosball/services/FireBasePushNotificationService.kt b/foosball/app/src/phone/java/com/instructure/androidfoosball/services/FireBasePushNotificationService.kt
deleted file mode 100644
index fa8ea29499..0000000000
--- a/foosball/app/src/phone/java/com/instructure/androidfoosball/services/FireBasePushNotificationService.kt
+++ /dev/null
@@ -1,92 +0,0 @@
-/*
- * Copyright (C) 2016 - 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.androidfoosball.services
-
-import android.app.NotificationManager
-import android.app.PendingIntent
-import android.content.Context
-import android.content.Intent
-import android.support.v7.app.NotificationCompat
-import android.util.Log
-
-import com.google.firebase.messaging.FirebaseMessaging
-import com.google.firebase.messaging.FirebaseMessagingService
-import com.google.firebase.messaging.RemoteMessage
-import com.instructure.androidfoosball.R
-import com.instructure.androidfoosball.activities.SignInActivity
-
-
-class FireBasePushNotificationService : FirebaseMessagingService() {
-
- override fun onMessageReceived(remoteMessage: RemoteMessage?) {
- super.onMessageReceived(remoteMessage)
-
- Log.d("abcde", "From: " + remoteMessage!!.from)
- //Get the channel the users subscribed too
- val pushId = remoteMessage.from.replace("/topics/", "")
- //Unsubscribe
- FirebaseMessaging.getInstance().unsubscribeFromTopic(pushId)
-
- val notificationIntent = Intent(applicationContext, SignInActivity::class.java)
- notificationIntent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP
- val intent = PendingIntent.getActivity(applicationContext, 0, notificationIntent, 0)
-
- //post notification
- val builder = NotificationCompat.Builder(applicationContext)
- builder.setSmallIcon(R.drawable.ic_status_notification)
- builder.setContentTitle(getString(R.string.app_name))
- builder.setContentIntent(intent)
- builder.setAutoCancel(true)
- val tableName = remoteMessage.data["message"]
- val contentText = String.format(getString(R.string.push_text), tableName)
- builder.setContentText(contentText)
-
- val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
- manager.notify(NOTIFICATION_ID, builder.build())
-
- if (remoteMessage.data.size > 0) {
- Log.d("abcde", "Message data payload: " + remoteMessage.data)
- }
-
- if (remoteMessage.notification != null) {
- Log.d("abcde", "Message Notification Body: " + remoteMessage.notification.body)
- }
- }
-
- override fun onDeletedMessages() {
- super.onDeletedMessages()
- Log.d("abcde", "Push Message Deleted")
- }
-
- override fun onMessageSent(s: String?) {
- super.onMessageSent(s)
- Log.d("abcde", "Push Message Sent: " + s!!)
- }
-
- override fun onSendError(s: String?, e: Exception?) {
- super.onSendError(s, e)
- Log.d("abcde", "Push Message Error: " + s!!)
- }
-
- companion object {
-
- private val NOTIFICATION_ID = 444930
- }
-}
diff --git a/foosball/app/src/phone/java/com/instructure/androidfoosball/utils/AnimUtils.kt b/foosball/app/src/phone/java/com/instructure/androidfoosball/utils/AnimUtils.kt
deleted file mode 100644
index e14e9933e9..0000000000
--- a/foosball/app/src/phone/java/com/instructure/androidfoosball/utils/AnimUtils.kt
+++ /dev/null
@@ -1,67 +0,0 @@
-/*
- * Copyright (C) 2016 - 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.androidfoosball.utils
-
-import android.view.View
-import android.view.animation.AlphaAnimation
-import android.view.animation.Animation
-
-
-object AnimUtils {
-
- fun fadeIn(duration: Long, view: View) {
- if (view.visibility == View.VISIBLE) return
-
- val anim = AlphaAnimation(0.0f, 1.0f)
- anim.duration = duration
- anim.setAnimationListener(object : Animation.AnimationListener {
- override fun onAnimationStart(animation: Animation) {
- }
-
- override fun onAnimationEnd(animation: Animation) {
- view.visibility = View.VISIBLE
- }
-
- override fun onAnimationRepeat(animation: Animation) {
- }
- })
- view.startAnimation(anim)
- }
-
- fun fadeOut(duration: Long, view: View) {
- if (view.visibility != View.VISIBLE) return
-
- val anim = AlphaAnimation(1.0f, 0.0f)
- anim.duration = duration
- anim.setAnimationListener(object : Animation.AnimationListener {
- override fun onAnimationStart(animation: Animation) {
- }
-
- override fun onAnimationEnd(animation: Animation) {
- view.visibility = View.INVISIBLE
- }
-
- override fun onAnimationRepeat(animation: Animation) {
- }
- })
- view.startAnimation(anim)
- }
-
-}
diff --git a/foosball/app/src/phone/java/com/instructure/androidfoosball/utils/Commentator.kt b/foosball/app/src/phone/java/com/instructure/androidfoosball/utils/Commentator.kt
deleted file mode 100755
index 0c9d584717..0000000000
--- a/foosball/app/src/phone/java/com/instructure/androidfoosball/utils/Commentator.kt
+++ /dev/null
@@ -1,34 +0,0 @@
-/*
- * Copyright (C) 2016 - 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.androidfoosball.utils
-
-import android.content.Context
-import android.speech.tts.TextToSpeech
-import com.instructure.androidfoosball.BuildConfig
-import com.instructure.androidfoosball.R
-import com.instructure.androidfoosball.models.*
-import java.util.*
-
-@Suppress("UNUSED_PARAMETER")
-class Commentator {
- fun initialize(context: Context) { }
- fun shutUp() { }
- fun announce(text: String, flush: Boolean = true) {}
-}
diff --git a/foosball/app/src/phone/java/com/instructure/androidfoosball/utils/FireUtils.kt b/foosball/app/src/phone/java/com/instructure/androidfoosball/utils/FireUtils.kt
deleted file mode 100644
index 4cef420f3d..0000000000
--- a/foosball/app/src/phone/java/com/instructure/androidfoosball/utils/FireUtils.kt
+++ /dev/null
@@ -1,99 +0,0 @@
-/*
- * Copyright (C) 2016 - 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.androidfoosball.utils
-
-import com.google.firebase.database.DataSnapshot
-import com.google.firebase.database.DatabaseError
-import com.google.firebase.database.DatabaseReference
-import com.google.firebase.database.ValueEventListener
-import com.instructure.androidfoosball.models.Table
-
-
-object FireUtils {
-
- fun getTables(database: DatabaseReference, onFinish: (tables: List) -> Unit) {
- database.keepSynced(false)
- database.child("tables").addListenerForSingleValueEvent(object : ValueEventListener {
- override fun onDataChange(dataSnapshot: DataSnapshot) {
- onFinish(dataSnapshot.children.map { it.getValue(Table::class.java)!!.apply { id = it.key } })
- }
-
- override fun onCancelled(databaseError: DatabaseError) {
- onFinish(emptyList())
- }
- })
- }
-
- fun setStartupPhrase(id: String, database: DatabaseReference, phrase: String) {
- database.child("users").child(id).child("customAssignmentPhrase").setValue(phrase)
- }
-
- fun setVictoryPhrase(id: String, database: DatabaseReference, phrase: String) {
- database.child("users").child(id).child("customVictoryPhrase").setValue(phrase)
- }
-
- fun getWinCount(id: String, database: DatabaseReference, callback: (value: Int) -> Unit) {
- database.child("users").child(id).child("wins").addListenerForSingleValueEvent(object : ValueEventListener {
- override fun onDataChange(dataSnapshot: DataSnapshot) {
- callback(dataSnapshot.getValue(Int::class.java) ?: 0)
- }
-
- override fun onCancelled(databaseError: DatabaseError) {
- callback(0)
- }
- })
- }
-
- fun getLossCount(id: String, database: DatabaseReference, callback: (value: Int) -> Unit) {
- database.child("users").child(id).child("losses").addListenerForSingleValueEvent(object : ValueEventListener {
- override fun onDataChange(dataSnapshot: DataSnapshot) {
- callback(dataSnapshot.getValue(Int::class.java) ?: 0)
- }
-
- override fun onCancelled(databaseError: DatabaseError) {
- callback(0)
- }
- })
- }
-
- fun getStartupPhrase(id: String, database: DatabaseReference, callback: (value: String) -> Unit) {
- database.child("users").child(id).child("customAssignmentPhrase").addListenerForSingleValueEvent(object : ValueEventListener {
- override fun onDataChange(dataSnapshot: DataSnapshot) {
- callback(dataSnapshot.getValue(String::class.java) ?: "")
- }
-
- override fun onCancelled(databaseError: DatabaseError) {
- callback("")
- }
- })
- }
-
- fun getVictoryPhrase(id: String, database: DatabaseReference, callback: (value: String) -> Unit) {
- database.child("users").child(id).child("customVictoryPhrase").addListenerForSingleValueEvent(object : ValueEventListener {
- override fun onDataChange(dataSnapshot: DataSnapshot) {
- callback(dataSnapshot.getValue(String::class.java) ?: "")
- }
-
- override fun onCancelled(databaseError: DatabaseError) {
- callback("")
- }
- })
- }
-}
diff --git a/foosball/app/src/phone/java/com/instructure/androidfoosball/utils/Prefs.kt b/foosball/app/src/phone/java/com/instructure/androidfoosball/utils/Prefs.kt
deleted file mode 100644
index fdbd1b4470..0000000000
--- a/foosball/app/src/phone/java/com/instructure/androidfoosball/utils/Prefs.kt
+++ /dev/null
@@ -1,27 +0,0 @@
-/*
- * Copyright (C) 2016 - 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.androidfoosball.utils
-
-import android.content.Context
-
-class Prefs(context: Context) : PrefManager(context, "prefs") {
- var userId by Pref("", Const.USER_ID)
- var preferredTableId by Pref("\\", Const.PREFERRED_TABLE_ID)
-}
diff --git a/foosball/app/src/phone/java/com/instructure/androidfoosball/wear/WearService.kt b/foosball/app/src/phone/java/com/instructure/androidfoosball/wear/WearService.kt
deleted file mode 100644
index 565a617a61..0000000000
--- a/foosball/app/src/phone/java/com/instructure/androidfoosball/wear/WearService.kt
+++ /dev/null
@@ -1,87 +0,0 @@
-/*
- * Copyright (C) 2016 - 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.androidfoosball.wear
-
-import android.os.Handler
-import android.util.Log
-import com.google.android.gms.wearable.DataMap
-import com.google.firebase.database.FirebaseDatabase
-import com.google.gson.Gson
-import com.google.gson.reflect.TypeToken
-import com.instructure.androidfoosball.utils.FireUtils
-import com.instructure.androidfoosball.utils.Prefs
-import com.instructure.wearutils.WearConst
-import com.instructure.wearutils.interfaces.WearableCallbacks
-import com.instructure.wearutils.models.DataPage
-import com.instructure.wearutils.services.BaseWearService
-import java.util.*
-
-
-class WearService : BaseWearService(), WearableCallbacks {
-
- override fun onCreate() {
- super.onCreate()
- Log.d("wear", "Wear Service Created")
- setCallbacks(this)
- }
-
- override fun OnSyncDataItemTask(dataMap: DataMap) {
- Log.d("wear", "OnSyncDataItemTask()")
- Handler().post { /* Action requests from wear device */ }
- }
-
- override fun OnGetMessageTask(messagePath: String) {
- Log.d("wear", "OnGetMessageTask()")
- Handler().post {
- //Data request from wear device
- if (WearConst.WEAR_DATA_REQUEST == messagePath) {
-
- val userId = Prefs(applicationContext).userId
- val database = FirebaseDatabase.getInstance().reference
-
- FireUtils.getWinCount(userId, database) { value ->
- Log.d("wear", "Sending win count data: " + value)
- syncString(WearConst.DATA_ITEM_WIN_COUNT, value.toString())
- }
-
- FireUtils.getLossCount(userId, database) { value ->
- Log.d("wear", "Sending loss count data: " + value)
- syncString(WearConst.DATA_ITEM_LOSS_COUNT, value.toString())
- }
-
- FireUtils.getTables(database) { values ->
- Log.d("wear", "Sending table data")
-
- val type = object : TypeToken>() {
-
- }.type
- val pages = ArrayList(values.size)
-
- for (table in values) {
- pages.add(DataPage(table.name, table.currentGame, 0, DataPage.TABLE))
- }
-
- val json = Gson().toJson(pages, type)
- syncString(WearConst.DATA_ITEM_TABLES, json)
- }
- }
- }
- }
-}
diff --git a/foosball/app/src/phone/res/drawable-hdpi/ic_check_white_48dp.png b/foosball/app/src/phone/res/drawable-hdpi/ic_check_white_48dp.png
deleted file mode 100644
index 699d0b2c46..0000000000
Binary files a/foosball/app/src/phone/res/drawable-hdpi/ic_check_white_48dp.png and /dev/null differ
diff --git a/foosball/app/src/phone/res/drawable-hdpi/ic_close_white_48dp.png b/foosball/app/src/phone/res/drawable-hdpi/ic_close_white_48dp.png
deleted file mode 100644
index 717c7b5919..0000000000
Binary files a/foosball/app/src/phone/res/drawable-hdpi/ic_close_white_48dp.png and /dev/null differ
diff --git a/foosball/app/src/phone/res/drawable-hdpi/ic_shortcut_tables.png b/foosball/app/src/phone/res/drawable-hdpi/ic_shortcut_tables.png
deleted file mode 100644
index adedd5272c..0000000000
Binary files a/foosball/app/src/phone/res/drawable-hdpi/ic_shortcut_tables.png and /dev/null differ
diff --git a/foosball/app/src/phone/res/drawable-hdpi/ic_status_notification.png b/foosball/app/src/phone/res/drawable-hdpi/ic_status_notification.png
deleted file mode 100755
index 3435d18aac..0000000000
Binary files a/foosball/app/src/phone/res/drawable-hdpi/ic_status_notification.png and /dev/null differ
diff --git a/foosball/app/src/phone/res/drawable-hdpi/sadpanda.png b/foosball/app/src/phone/res/drawable-hdpi/sadpanda.png
deleted file mode 100644
index 7bfd91bb89..0000000000
Binary files a/foosball/app/src/phone/res/drawable-hdpi/sadpanda.png and /dev/null differ
diff --git a/foosball/app/src/phone/res/drawable-mdpi/ic_check_white_48dp.png b/foosball/app/src/phone/res/drawable-mdpi/ic_check_white_48dp.png
deleted file mode 100644
index 1f4846cbea..0000000000
Binary files a/foosball/app/src/phone/res/drawable-mdpi/ic_check_white_48dp.png and /dev/null differ
diff --git a/foosball/app/src/phone/res/drawable-mdpi/ic_close_white_48dp.png b/foosball/app/src/phone/res/drawable-mdpi/ic_close_white_48dp.png
deleted file mode 100644
index 0b00a33a72..0000000000
Binary files a/foosball/app/src/phone/res/drawable-mdpi/ic_close_white_48dp.png and /dev/null differ
diff --git a/foosball/app/src/phone/res/drawable-mdpi/ic_shortcut_tables.png b/foosball/app/src/phone/res/drawable-mdpi/ic_shortcut_tables.png
deleted file mode 100644
index 123cb3f303..0000000000
Binary files a/foosball/app/src/phone/res/drawable-mdpi/ic_shortcut_tables.png and /dev/null differ
diff --git a/foosball/app/src/phone/res/drawable-mdpi/ic_status_notification.png b/foosball/app/src/phone/res/drawable-mdpi/ic_status_notification.png
deleted file mode 100755
index 49b076c5ce..0000000000
Binary files a/foosball/app/src/phone/res/drawable-mdpi/ic_status_notification.png and /dev/null differ
diff --git a/foosball/app/src/phone/res/drawable-mdpi/sadpanda.png b/foosball/app/src/phone/res/drawable-mdpi/sadpanda.png
deleted file mode 100644
index 536e4e7768..0000000000
Binary files a/foosball/app/src/phone/res/drawable-mdpi/sadpanda.png and /dev/null differ
diff --git a/foosball/app/src/phone/res/drawable-xhdpi/ic_check_white_48dp.png b/foosball/app/src/phone/res/drawable-xhdpi/ic_check_white_48dp.png
deleted file mode 100644
index bf5bc7e090..0000000000
Binary files a/foosball/app/src/phone/res/drawable-xhdpi/ic_check_white_48dp.png and /dev/null differ
diff --git a/foosball/app/src/phone/res/drawable-xhdpi/ic_close_white_48dp.png b/foosball/app/src/phone/res/drawable-xhdpi/ic_close_white_48dp.png
deleted file mode 100644
index 9ef2d8f9fb..0000000000
Binary files a/foosball/app/src/phone/res/drawable-xhdpi/ic_close_white_48dp.png and /dev/null differ
diff --git a/foosball/app/src/phone/res/drawable-xhdpi/ic_shortcut_tables.png b/foosball/app/src/phone/res/drawable-xhdpi/ic_shortcut_tables.png
deleted file mode 100644
index 30f1d16391..0000000000
Binary files a/foosball/app/src/phone/res/drawable-xhdpi/ic_shortcut_tables.png and /dev/null differ
diff --git a/foosball/app/src/phone/res/drawable-xhdpi/ic_status_notification.png b/foosball/app/src/phone/res/drawable-xhdpi/ic_status_notification.png
deleted file mode 100755
index 537ab9408b..0000000000
Binary files a/foosball/app/src/phone/res/drawable-xhdpi/ic_status_notification.png and /dev/null differ
diff --git a/foosball/app/src/phone/res/drawable-xhdpi/sadpanda.png b/foosball/app/src/phone/res/drawable-xhdpi/sadpanda.png
deleted file mode 100644
index 924f7eb8ee..0000000000
Binary files a/foosball/app/src/phone/res/drawable-xhdpi/sadpanda.png and /dev/null differ
diff --git a/foosball/app/src/phone/res/drawable-xxhdpi/ic_check_white_48dp.png b/foosball/app/src/phone/res/drawable-xxhdpi/ic_check_white_48dp.png
deleted file mode 100644
index 226553b8a0..0000000000
Binary files a/foosball/app/src/phone/res/drawable-xxhdpi/ic_check_white_48dp.png and /dev/null differ
diff --git a/foosball/app/src/phone/res/drawable-xxhdpi/ic_close_white_48dp.png b/foosball/app/src/phone/res/drawable-xxhdpi/ic_close_white_48dp.png
deleted file mode 100644
index 5a99107d64..0000000000
Binary files a/foosball/app/src/phone/res/drawable-xxhdpi/ic_close_white_48dp.png and /dev/null differ
diff --git a/foosball/app/src/phone/res/drawable-xxhdpi/ic_shortcut_tables.png b/foosball/app/src/phone/res/drawable-xxhdpi/ic_shortcut_tables.png
deleted file mode 100644
index 0b8aaa5ef7..0000000000
Binary files a/foosball/app/src/phone/res/drawable-xxhdpi/ic_shortcut_tables.png and /dev/null differ
diff --git a/foosball/app/src/phone/res/drawable-xxhdpi/ic_status_notification.png b/foosball/app/src/phone/res/drawable-xxhdpi/ic_status_notification.png
deleted file mode 100755
index ac6cf5df93..0000000000
Binary files a/foosball/app/src/phone/res/drawable-xxhdpi/ic_status_notification.png and /dev/null differ
diff --git a/foosball/app/src/phone/res/drawable-xxhdpi/sadpanda.png b/foosball/app/src/phone/res/drawable-xxhdpi/sadpanda.png
deleted file mode 100644
index a6028541e1..0000000000
Binary files a/foosball/app/src/phone/res/drawable-xxhdpi/sadpanda.png and /dev/null differ
diff --git a/foosball/app/src/phone/res/drawable-xxxhdpi/ic_check_white_48dp.png b/foosball/app/src/phone/res/drawable-xxxhdpi/ic_check_white_48dp.png
deleted file mode 100644
index 7c722eedc1..0000000000
Binary files a/foosball/app/src/phone/res/drawable-xxxhdpi/ic_check_white_48dp.png and /dev/null differ
diff --git a/foosball/app/src/phone/res/drawable-xxxhdpi/ic_close_white_48dp.png b/foosball/app/src/phone/res/drawable-xxxhdpi/ic_close_white_48dp.png
deleted file mode 100644
index fa87f2514f..0000000000
Binary files a/foosball/app/src/phone/res/drawable-xxxhdpi/ic_close_white_48dp.png and /dev/null differ
diff --git a/foosball/app/src/phone/res/drawable-xxxhdpi/ic_shortcut_tables.png b/foosball/app/src/phone/res/drawable-xxxhdpi/ic_shortcut_tables.png
deleted file mode 100644
index 34fbe4462e..0000000000
Binary files a/foosball/app/src/phone/res/drawable-xxxhdpi/ic_shortcut_tables.png and /dev/null differ
diff --git a/foosball/app/src/phone/res/drawable-xxxhdpi/ic_status_notification.png b/foosball/app/src/phone/res/drawable-xxxhdpi/ic_status_notification.png
deleted file mode 100755
index 404df03f7c..0000000000
Binary files a/foosball/app/src/phone/res/drawable-xxxhdpi/ic_status_notification.png and /dev/null differ
diff --git a/foosball/app/src/phone/res/drawable/vd_check_circle_green_800_24dp.xml b/foosball/app/src/phone/res/drawable/vd_check_circle_green_800_24dp.xml
deleted file mode 100644
index d8b0ec61c4..0000000000
--- a/foosball/app/src/phone/res/drawable/vd_check_circle_green_800_24dp.xml
+++ /dev/null
@@ -1,4 +0,0 @@
-
-
-
-
diff --git a/foosball/app/src/phone/res/drawable/vd_done.xml b/foosball/app/src/phone/res/drawable/vd_done.xml
deleted file mode 100644
index 7dcd09249b..0000000000
--- a/foosball/app/src/phone/res/drawable/vd_done.xml
+++ /dev/null
@@ -1,14 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/foosball/app/src/phone/res/drawable/vd_edit.xml b/foosball/app/src/phone/res/drawable/vd_edit.xml
deleted file mode 100644
index 3d2da0b22a..0000000000
--- a/foosball/app/src/phone/res/drawable/vd_edit.xml
+++ /dev/null
@@ -1,14 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/foosball/app/src/phone/res/drawable/vd_person_add_black.xml b/foosball/app/src/phone/res/drawable/vd_person_add_black.xml
deleted file mode 100644
index 61a9ffc0e5..0000000000
--- a/foosball/app/src/phone/res/drawable/vd_person_add_black.xml
+++ /dev/null
@@ -1,4 +0,0 @@
-
-
-
diff --git a/foosball/app/src/phone/res/layout/activity_leaderboard_tablet.xml b/foosball/app/src/phone/res/layout/activity_leaderboard_tablet.xml
deleted file mode 100644
index 5797ce42b3..0000000000
--- a/foosball/app/src/phone/res/layout/activity_leaderboard_tablet.xml
+++ /dev/null
@@ -1,30 +0,0 @@
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/foosball/app/src/phone/res/layout/activity_main_tablet.xml b/foosball/app/src/phone/res/layout/activity_main_tablet.xml
deleted file mode 100644
index 25f9e858cb..0000000000
--- a/foosball/app/src/phone/res/layout/activity_main_tablet.xml
+++ /dev/null
@@ -1,290 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/foosball/app/src/phone/res/layout/activity_nfc_read.xml b/foosball/app/src/phone/res/layout/activity_nfc_read.xml
deleted file mode 100644
index 9330ad4238..0000000000
--- a/foosball/app/src/phone/res/layout/activity_nfc_read.xml
+++ /dev/null
@@ -1,171 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/foosball/app/src/phone/res/layout/activity_primary.xml b/foosball/app/src/phone/res/layout/activity_primary.xml
deleted file mode 100644
index d2f5d5d6c3..0000000000
--- a/foosball/app/src/phone/res/layout/activity_primary.xml
+++ /dev/null
@@ -1,93 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/foosball/app/src/phone/res/layout/activity_sign_in.xml b/foosball/app/src/phone/res/layout/activity_sign_in.xml
deleted file mode 100644
index 20bd679fc5..0000000000
--- a/foosball/app/src/phone/res/layout/activity_sign_in.xml
+++ /dev/null
@@ -1,31 +0,0 @@
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/foosball/app/src/phone/res/layout/adapter_leaderboard_tablet.xml b/foosball/app/src/phone/res/layout/adapter_leaderboard_tablet.xml
deleted file mode 100644
index dd6e93d438..0000000000
--- a/foosball/app/src/phone/res/layout/adapter_leaderboard_tablet.xml
+++ /dev/null
@@ -1,62 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/foosball/app/src/phone/res/layout/adapter_table.xml b/foosball/app/src/phone/res/layout/adapter_table.xml
deleted file mode 100644
index 5fd5f8795b..0000000000
--- a/foosball/app/src/phone/res/layout/adapter_table.xml
+++ /dev/null
@@ -1,306 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/foosball/app/src/phone/res/layout/adapter_user_dialog.xml b/foosball/app/src/phone/res/layout/adapter_user_dialog.xml
deleted file mode 100644
index 40b7a7477d..0000000000
--- a/foosball/app/src/phone/res/layout/adapter_user_dialog.xml
+++ /dev/null
@@ -1,45 +0,0 @@
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/foosball/app/src/phone/res/layout/fragment_tables.xml b/foosball/app/src/phone/res/layout/fragment_tables.xml
deleted file mode 100644
index a0798b60ca..0000000000
--- a/foosball/app/src/phone/res/layout/fragment_tables.xml
+++ /dev/null
@@ -1,30 +0,0 @@
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/foosball/app/src/phone/res/layout/fragment_user.xml b/foosball/app/src/phone/res/layout/fragment_user.xml
deleted file mode 100644
index 5c7768402a..0000000000
--- a/foosball/app/src/phone/res/layout/fragment_user.xml
+++ /dev/null
@@ -1,288 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/foosball/app/src/phone/res/menu/menu_logout.xml b/foosball/app/src/phone/res/menu/menu_logout.xml
deleted file mode 100644
index 001cd3b99f..0000000000
--- a/foosball/app/src/phone/res/menu/menu_logout.xml
+++ /dev/null
@@ -1,31 +0,0 @@
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/foosball/app/src/phone/res/mipmap-hdpi/ic_launcher.png b/foosball/app/src/phone/res/mipmap-hdpi/ic_launcher.png
deleted file mode 100755
index 3cd7753f36..0000000000
Binary files a/foosball/app/src/phone/res/mipmap-hdpi/ic_launcher.png and /dev/null differ
diff --git a/foosball/app/src/phone/res/mipmap-mdpi/ic_launcher.png b/foosball/app/src/phone/res/mipmap-mdpi/ic_launcher.png
deleted file mode 100755
index b2872af092..0000000000
Binary files a/foosball/app/src/phone/res/mipmap-mdpi/ic_launcher.png and /dev/null differ
diff --git a/foosball/app/src/phone/res/mipmap-xhdpi/ic_launcher.png b/foosball/app/src/phone/res/mipmap-xhdpi/ic_launcher.png
deleted file mode 100755
index 16802cb60a..0000000000
Binary files a/foosball/app/src/phone/res/mipmap-xhdpi/ic_launcher.png and /dev/null differ
diff --git a/foosball/app/src/phone/res/mipmap-xxhdpi/ic_launcher.png b/foosball/app/src/phone/res/mipmap-xxhdpi/ic_launcher.png
deleted file mode 100755
index a9e0158ee4..0000000000
Binary files a/foosball/app/src/phone/res/mipmap-xxhdpi/ic_launcher.png and /dev/null differ
diff --git a/foosball/app/src/phone/res/mipmap-xxxhdpi/ic_launcher.png b/foosball/app/src/phone/res/mipmap-xxxhdpi/ic_launcher.png
deleted file mode 100755
index cbad722601..0000000000
Binary files a/foosball/app/src/phone/res/mipmap-xxxhdpi/ic_launcher.png and /dev/null differ
diff --git a/foosball/app/src/phone/res/values-w820dp/dimens.xml b/foosball/app/src/phone/res/values-w820dp/dimens.xml
deleted file mode 100644
index 8746fd7ce6..0000000000
--- a/foosball/app/src/phone/res/values-w820dp/dimens.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-
-
-
-
- 64dp
-
diff --git a/foosball/app/src/phone/res/values/colors.xml b/foosball/app/src/phone/res/values/colors.xml
deleted file mode 100644
index 76091fb6dc..0000000000
--- a/foosball/app/src/phone/res/values/colors.xml
+++ /dev/null
@@ -1,29 +0,0 @@
-
-
-
-
- #003977
- #00224e
- #cb0040
- #FFFFFF
- #000000
- #C1C1C1
- #f2f2f2
-
diff --git a/foosball/app/src/phone/res/values/dimens.xml b/foosball/app/src/phone/res/values/dimens.xml
deleted file mode 100644
index f307b27452..0000000000
--- a/foosball/app/src/phone/res/values/dimens.xml
+++ /dev/null
@@ -1,28 +0,0 @@
-
-
-
-
- 16dp
- 16dp
-
- 100dp
- 30dp
- 16dp
-
diff --git a/foosball/app/src/phone/res/values/strings.xml b/foosball/app/src/phone/res/values/strings.xml
deleted file mode 100644
index f6a443d50d..0000000000
--- a/foosball/app/src/phone/res/values/strings.xml
+++ /dev/null
@@ -1,66 +0,0 @@
-
-
-
- Foosball
- Loading
- Win
- Loss
- Pick a table
- Pick a user
- Start Game
- Okay
- Winner Winner!
- %s won!
- All Done
- Swap sides
- Quit Game
- %d wins
- %d losses
- Leaderboard
- Add Player
- Add User
- Reset Teams
- Logout
- Tables
- Random Message
- Victory Message
- Welcome Message
- Add your phrase
-
- Table Status:
- Busy
- Free
- Unknown
- Round
- of
- Points to Win
- •
- Score:
- to
- Request Table
-
- The table %1$s is now available.
-
-
- View Tables
- Tables
- Disabled
-
-
diff --git a/foosball/app/src/phone/res/values/styles.xml b/foosball/app/src/phone/res/values/styles.xml
deleted file mode 100644
index c4fdcd8f1b..0000000000
--- a/foosball/app/src/phone/res/values/styles.xml
+++ /dev/null
@@ -1,50 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/foosball/app/src/phone/res/xml-v25/shortcuts.xml b/foosball/app/src/phone/res/xml-v25/shortcuts.xml
deleted file mode 100644
index 31985a8fc6..0000000000
--- a/foosball/app/src/phone/res/xml-v25/shortcuts.xml
+++ /dev/null
@@ -1,18 +0,0 @@
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/foosball/app/src/tablet/AndroidManifest.xml b/foosball/app/src/tablet/AndroidManifest.xml
deleted file mode 100644
index aefc4eea12..0000000000
--- a/foosball/app/src/tablet/AndroidManifest.xml
+++ /dev/null
@@ -1,88 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/foosball/app/src/tablet/java/com/instructure/androidfoosball/App.kt b/foosball/app/src/tablet/java/com/instructure/androidfoosball/App.kt
deleted file mode 100644
index be0c3b546d..0000000000
--- a/foosball/app/src/tablet/java/com/instructure/androidfoosball/App.kt
+++ /dev/null
@@ -1,52 +0,0 @@
-/*
- * Copyright (C) 2017 - 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.androidfoosball
-
-import android.app.Application
-import android.content.Context
-import android.content.Intent
-import com.instructure.androidfoosball.services.FoosballSyncService
-import com.instructure.androidfoosball.utils.Commentator
-import io.realm.Realm
-import io.realm.RealmConfiguration
-
-class App : Application() {
-
- companion object {
-
- lateinit var context: Context
-
- val realm: Realm by lazy {
- val realmConfig = RealmConfiguration.Builder()
- .schemaVersion(1)
- .deleteRealmIfMigrationNeeded()
- .build()
- Realm.getInstance(realmConfig)
- }
-
- val commentator = Commentator()
- }
-
- override fun onCreate() {
- super.onCreate()
- context = this
- startService(Intent(this, FoosballSyncService::class.java))
- commentator.initialize(this)
- Realm.init(this)
- }
-
-}
diff --git a/foosball/app/src/tablet/java/com/instructure/androidfoosball/activities/CreateCutThroatGameActivity.kt b/foosball/app/src/tablet/java/com/instructure/androidfoosball/activities/CreateCutThroatGameActivity.kt
deleted file mode 100644
index a2089b2cc2..0000000000
--- a/foosball/app/src/tablet/java/com/instructure/androidfoosball/activities/CreateCutThroatGameActivity.kt
+++ /dev/null
@@ -1,166 +0,0 @@
-/*
- * Copyright (C) 2017 - 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.androidfoosball.activities
-
-import android.media.AudioManager
-import android.os.Bundle
-import android.support.v7.app.AppCompatActivity
-import com.afollestad.materialdialogs.MaterialDialog
-import com.google.firebase.database.DataSnapshot
-import com.google.firebase.database.DatabaseError
-import com.google.firebase.database.FirebaseDatabase
-import com.google.firebase.database.ValueEventListener
-import com.instructure.androidfoosball.App
-import com.instructure.androidfoosball.R
-import com.instructure.androidfoosball.ktmodels.*
-import com.instructure.androidfoosball.utils.*
-import io.realm.RealmList
-import kotlinx.android.synthetic.tablet.activity_create_cut_throat_game.*
-import org.jetbrains.anko.sdk21.listeners.onClick
-import org.jetbrains.anko.startActivity
-import java.util.*
-
-class CreateCutThroatGameActivity : AppCompatActivity() {
-
- private val mTable = Table.getSelectedTable()
- private val mIncomingNfcRef = FirebaseDatabase.getInstance().reference.child("incoming").child(mTable.id)
-
- private val ROTATE_AFTER_DEFAULT = 2
- private val POINTS_DEFAULT = 5
-
- private var rotateAfter = 0
- set(value) {
- field = value
- rotateAfterButton.text = if (value == 0) String(Character.toChars(0x1F60E)) else value.toString()
- updateDurationRange()
- }
-
- private var points = 0
- set(value) {
- field = value
- pointButton.text = value.toString()
- if (rotateAfter >= value) rotateAfter = value - 1
- updateDurationRange()
- }
-
- private val nfcListener = object : ValueEventListener {
- override fun onDataChange(dataSnapshot: DataSnapshot) {
- val nfc = dataSnapshot.getValue(IncomingData::class.java) ?: return
- if (nfc.sideOne.isBlank() && nfc.sideTwo.isBlank()) return
- fun getUserById(userId: String): User? = App.realm.where(User::class.java).equalTo("id", userId).findFirst()
- when {
- nfc.sideOne.isNotBlank() -> getUserById(nfc.sideOne)?.let { addUser(it) }
- nfc.sideTwo.isNotBlank() -> getUserById(nfc.sideTwo)?.let { addUser(it) }
- }
- mIncomingNfcRef.setValue(IncomingData())
- }
-
- override fun onCancelled(databaseError: DatabaseError) { }
- }
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- setContentView(R.layout.activity_create_cut_throat_game)
- volumeControlStream = AudioManager.STREAM_MUSIC
- setupViews()
- mIncomingNfcRef.addValueEventListener(nfcListener)
- }
-
- private fun setupViews() {
- // Player selection
- playersLayout.onAddPlayerClicked = { selectPlayer() }
-
- // On players changed
- playersLayout.onPlayersChanged = { onPlayersChanged() }
-
- // Points selection
- points = POINTS_DEFAULT
- pointButton.onClick {
- val options = (3..15).toList()
- MaterialDialog.Builder(this)
- .items(options)
- .itemsCallback { _, _, i, _ -> points = options[i] }
- .show()
- }
-
- // Rotate after selection
- rotateAfter = ROTATE_AFTER_DEFAULT
- rotateAfterButton.onClick {
- val options = (0 until points).toList()
- MaterialDialog.Builder(this)
- .items(options.map { if (it == 0) "NEVER" else it.toString() })
- .itemsCallback { _, _, i, _ -> rotateAfter = options[i] }
- .show()
- }
-
- // Start game
- startGameButton.onClick { createGame() }
-
- // Setup QR code
- qrCode.setTableSide(mTable, TableSide.SIDE_1)
- }
-
- private fun updateDurationRange() {
- val playerCount = playersLayout.players.size
- if (playerCount < 3) {
- durationView.text = getString(R.string.durationRangeNoMax, "-")
- } else {
- val ra = if (rotateAfter in 1..points) rotateAfter else points
- val minRotations: Int = Math.ceil(points.toDouble() / ra).toInt() - 1
- val minGoals = points + ((playerCount - 1) * minRotations)
- durationView.text = getString(R.string.durationRangeNoMax, minGoals.toString())
- }
- }
-
- private fun onPlayersChanged() {
- // Show start button if ready
- val ready = playersLayout.players.size >= 3
- assignTeamsView.setVisible(!ready)
- startGameButton.setVisible(ready)
- updateDurationRange()
- }
-
- private fun selectPlayer() {
- showUserPicker(this) { addUser(it) }
- }
-
- private fun addUser(user: User) {
- playersLayout.addUser(user)
- mCommentator.announce(user.customAssignmentPhrase.elseIfBlank(user.name))
- }
-
- private fun createGame() {
- val players = ArrayList(playersLayout.players)
- Collections.shuffle(players)
-
- val game = CutThroatGame()
- game.status = GameStatus.ONGOING.name
- game.pointsToWin = points
- game.rotateAfter = rotateAfter
- game.startTime = System.currentTimeMillis()
- game.players = players.mapTo(RealmList()) { it }
- game.copyToRealmOrUpdate()
-
- startActivity(CutThroatGameActivity.EXTRA_GAME_ID to game.id)
- finish()
- }
-
- override fun onDestroy() {
- super.onDestroy()
- mIncomingNfcRef.removeEventListener(nfcListener)
- }
-}
diff --git a/foosball/app/src/tablet/java/com/instructure/androidfoosball/activities/CreateGameActivity.kt b/foosball/app/src/tablet/java/com/instructure/androidfoosball/activities/CreateGameActivity.kt
deleted file mode 100644
index f9325d446f..0000000000
--- a/foosball/app/src/tablet/java/com/instructure/androidfoosball/activities/CreateGameActivity.kt
+++ /dev/null
@@ -1,257 +0,0 @@
-/*
- * Copyright (C) 2017 - 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.androidfoosball.activities
-
-import android.graphics.Color
-import android.media.AudioManager
-import android.os.Bundle
-import android.support.v7.app.AppCompatActivity
-import com.afollestad.materialdialogs.MaterialDialog
-import com.google.firebase.database.*
-import com.instructure.androidfoosball.App
-import com.instructure.androidfoosball.R
-import com.instructure.androidfoosball.ktmodels.*
-import com.instructure.androidfoosball.utils.*
-import com.instructure.androidfoosball.views.ConfirmPinDialog
-import com.instructure.androidfoosball.views.TeamLayout
-import kotlinx.android.synthetic.tablet.activity_create_game.*
-import org.jetbrains.anko.sdk21.listeners.onClick
-import org.jetbrains.anko.startActivity
-
-class CreateGameActivity : AppCompatActivity() {
-
- private val mTable = Table.getSelectedTable()
- private val mIncomingNfcRef = FirebaseDatabase.getInstance().reference.child("incoming").child(mTable.id)
- private val mDatabase: DatabaseReference = FirebaseDatabase.getInstance().reference
-
- private val BEST_OF_DEFAULT = 3
- private val POINTS_DEFAULT = 5
-
- private var bestOf = 0
- set(value) {
- field = value
- bestOfButton.text = value.toString()
- updateDurationRange()
- }
-
- private var points = 0
- set(value) {
- field = value
- pointButton.text = value.toString()
- updateDurationRange()
- }
-
- private val nfcListener = object : ValueEventListener {
- override fun onDataChange(dataSnapshot: DataSnapshot) {
- val nfc = dataSnapshot.getValue(IncomingData::class.java) ?: return
- if (nfc.sideOne.isBlank() && nfc.sideTwo.isBlank()) return
- fun getUserById(userId: String): User? = App.realm.where(User::class.java).equalTo("id", userId).findFirst()
- when {
- nfc.sideOne.isNotBlank() -> getUserById(nfc.sideOne)?.let { addUser(it, teamOneLayout) }
- nfc.sideTwo.isNotBlank() -> getUserById(nfc.sideTwo)?.let { addUser(it, teamTwoLayout) }
- }
- mIncomingNfcRef.setValue(IncomingData())
- }
-
- override fun onCancelled(databaseError: DatabaseError) { }
- }
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- setContentView(R.layout.activity_create_game)
- volumeControlStream = AudioManager.STREAM_MUSIC
- setupViews()
- mIncomingNfcRef.addValueEventListener(nfcListener)
- }
-
- private fun setupViews() {
- // Set initial team names
- teamOneNameView.setText(mTable.sideOneName)
- teamTwoNameView.setText(mTable.sideTwoName)
-
- // Set team colors
- teamOneLayout.setTeamColor(Color.parseColor(mTable.sideOneColor))
- teamTwoLayout.setTeamColor(Color.parseColor(mTable.sideTwoColor))
-
- // Player selection listeners
- teamOneLayout.onAddPlayerClicked = { selectPlayer(teamOneLayout) }
- teamTwoLayout.onAddPlayerClicked = { selectPlayer(teamTwoLayout) }
-
- // Player selection listeners
- teamOneLayout.onAddTeamClicked = { selectTeam(teamOneLayout) }
- teamTwoLayout.onAddTeamClicked = { selectTeam(teamTwoLayout) }
-
- // Team changed listeners
- teamOneLayout.onTeamChanged = { onTeamChanged(TableSide.SIDE_1) }
- teamTwoLayout.onTeamChanged = { onTeamChanged(TableSide.SIDE_2) }
-
- // Setup QR codes
- teamOneQR.setTableSide(mTable, TableSide.SIDE_1)
- teamTwoQR.setTableSide(mTable, TableSide.SIDE_2)
-
- // Best of selection
- bestOf = BEST_OF_DEFAULT
- bestOfButton.onClick {
- val options = (1..9 step 2).toList()
- MaterialDialog.Builder(this)
- .items(options)
- .itemsCallback { _, _, i, _ -> bestOf = options[i] }
- .show()
- }
-
- // Points selection
- points = POINTS_DEFAULT
- pointButton.onClick {
- val options = (3..15).toList()
- MaterialDialog.Builder(this)
- .items(options)
- .itemsCallback { _, _, i, _ -> points = options[i] }
- .show()
- }
-
- // Start game
- startGameButton.onClick {
- if (teamOneLayout.team.users.size != teamTwoLayout.team.users.size) {
- MaterialDialog.Builder(this)
- .title(R.string.uneven_teams)
- .content(R.string.uneven_teams_content)
- .positiveText(android.R.string.yes)
- .onPositive { _, _ -> createGame() }
- .negativeText(android.R.string.no)
- .show()
- } else {
- createGame()
- }
- }
- }
-
- private fun updateDurationRange() {
- val minGoals = points * (bestOf / 2 + 1)
- val maxGoals = bestOf * (points * 2 - 1)
- durationView.text = getString(R.string.durationRange, minGoals, maxGoals)
- }
-
- private fun onTeamChanged(side: TableSide) {
- // Update average team win rates
- winRateTeamOne.text = if (teamOneLayout.hasUsers()) getString(R.string.avg_win_rate_formatted).format(teamOneLayout.team.getAverageWinRate()) else ""
- winRateTeamTwo.text = if (teamTwoLayout.hasUsers()) getString(R.string.avg_win_rate_formatted).format(teamTwoLayout.team.getAverageWinRate()) else ""
-
- if (side == TableSide.SIDE_1) {
- // Update team one custom name
- teamOneNameView.setText(teamOneLayout.team.teamName.elseIfBlank(mTable.sideOneName))
- } else {
- // Update team two custom name
- teamTwoNameView.setText(teamTwoLayout.team.teamName.elseIfBlank(mTable.sideTwoName))
- }
-
-
- // Show start button if ready
- val ready = teamOneLayout.hasUsers() && teamTwoLayout.hasUsers()
- assignTeamsView.setVisible(!ready)
- startGameButton.setVisible(ready)
- }
-
- private fun selectPlayer(teamLayout: TeamLayout) {
- showUserPicker(this) {
- ConfirmPinDialog(this, it) { confirmedUser ->
- addUser(confirmedUser, teamLayout)
- }.show()
- }
- }
-
- private fun selectTeam(teamLayout: TeamLayout) {
- showTeamPicker(this) {
- ConfirmPinDialog(this, it) { confirmedUser ->
- addUser(confirmedUser, teamLayout)
- }.show()
- }
- }
-
- private fun addUser(user: User, teamLayout: TeamLayout) {
- (if (teamLayout == teamOneLayout) teamTwoLayout else teamOneLayout).removeUser(user)
- if (teamLayout.addUser(user)) {
- mCommentator.announcePlayerAssignment(
- user,
- if (teamLayout == teamOneLayout) mTable.sideOneName else mTable.sideTwoName
- )
- }
- }
-
- private fun createGame() {
-
- // Set/update team one custom name
- val teamOne = teamOneLayout.team as Team
- teamOneNameView.text.toString().apply {
- if (isNotBlank() && this != mTable.sideOneName) {
- saveOrUpdateTeam(this, teamOne.users.map(User::id))
- }
- }
-
- // Set/update team two custom name
- val teamTwo = teamTwoLayout.team as Team
- teamTwoNameView.text.toString().apply {
- if (isNotBlank() && this != mTable.sideTwoName) {
- saveOrUpdateTeam(this, teamTwo.users.map(User::id))
- }
- }
-
- // Create first round
- val round = Round(
- pointsToWin = points,
- sideOneTeam = teamOne,
- sideTwoTeam = teamTwo,
- startTime = System.currentTimeMillis()
- )
-
- // Create and save game
- val game = Game()
- game.bestOf = bestOf
- game.rounds.add(round)
- game.startTime = round.startTime
- game.status = GameStatus.ONGOING.name
- game.teamOne = teamOne
- game.teamTwo = teamTwo
- game.copyToRealmOrUpdate()
-
- startActivity(GameActivity.EXTRA_GAME_ID to game.id)
- finish()
-
- }
-
- private fun saveOrUpdateTeam(teamName: String, userIds: List) {
- val teamHash = userIds.getTeamHash()
- val team = App.realm.where(RealmTeam::class.java).equalTo("id", teamHash).findFirst()
- when {
- // Create new team if it doesn't exist
- team == null -> {
- val newTeam = CustomTeam(teamHash, teamName, 0L, 0L, userIds)
- App.realm.inTransaction { copyToRealmOrUpdate(newTeam.toRealmTeam()) }
- mDatabase.child("customTeams").child(teamHash).setValue(newTeam)
- }
- // Update team name if it has changed
- teamName != team.teamName -> {
- team.edit { this.teamName = teamName }
- mDatabase.child("customTeams").child(teamHash).child("teamName").setValue(team.teamName)
- }
- }
- }
-
- override fun onDestroy() {
- super.onDestroy()
- mIncomingNfcRef.removeEventListener(nfcListener)
- }
-}
diff --git a/foosball/app/src/tablet/java/com/instructure/androidfoosball/activities/CreatePlayerActivity.kt b/foosball/app/src/tablet/java/com/instructure/androidfoosball/activities/CreatePlayerActivity.kt
deleted file mode 100644
index 651b903169..0000000000
--- a/foosball/app/src/tablet/java/com/instructure/androidfoosball/activities/CreatePlayerActivity.kt
+++ /dev/null
@@ -1,101 +0,0 @@
-/*
- * Copyright (C) 2017 - 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.androidfoosball.activities
-
-import android.app.Activity
-import android.content.Intent
-import android.os.Bundle
-import android.support.v7.app.AppCompatActivity
-import android.util.Patterns
-import android.widget.Toast
-import com.google.firebase.database.FirebaseDatabase
-import com.instructure.androidfoosball.App
-import com.instructure.androidfoosball.R
-import com.instructure.androidfoosball.ktmodels.User
-import com.instructure.androidfoosball.utils.*
-import io.realm.Case
-import kotlinx.android.synthetic.tablet.activity_create_player.*
-import java.util.*
-
-
-class CreatePlayerActivity : AppCompatActivity() {
-
- private val REQUEST_CODE_ADD_AVATAR = 123
-
- private val mDatabase by lazy {
- FirebaseDatabase.getInstance().reference
- }
-
- private var mAvatarUrl = ""
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- setContentView(R.layout.activity_create_player)
-
- /* Show Email input on Name input validation */
- tilDisplayName.validateOnTextChanged({ it.length >= 3 }, "Display name must be at least 3 characters") { newText, isValid ->
- tilEmail.setVisible(isValid)
- }
-
- /* Show Confirm Email input when a valid email is entered */
- tilEmail.validateOnTextChanged({it.matches(Patterns.EMAIL_ADDRESS)}, "Enter a valid email address") { newText, isValid ->
- tilConfirmEmail.setVisible(isValid)
- }
-
- tilConfirmEmail.validateOnTextChanged({ it == tilEmail.text }, "Emails do not match") { newText, isValid ->
- controlsContainer.setVisible(isValid)
- }
-
- btnCreateUser.setOnClickListener {
- val user = User(id = UUID.randomUUID().toString(), name = tilDisplayName.text, email = tilConfirmEmail.text, avatar = mAvatarUrl)
- if (App.realm.where(User::class.java).equalTo("email", user.email, Case.INSENSITIVE).count() > 0){
- Toast.makeText(this@CreatePlayerActivity, "User with email ${user.email} already exists", Toast.LENGTH_SHORT).show()
- } else {
- postUser(user)
- }
- }
-
- tvAddAvatar.setOnClickListener {
- showImageSourcePicker(this) {
- startActivityForResult(it, REQUEST_CODE_ADD_AVATAR)
- }
- }
- }
-
- private fun postUser(user: User) {
- if (user.id.isNullOrBlank()) {
- shortToast("Error creating user - invalid user ID")
- return
- }
- mDatabase.child("users").child(user.id).setValue(user).addOnCompleteListener { task ->
- if (task.isSuccessful) {
- Toast.makeText(this@CreatePlayerActivity, "${user.name} successfully added", Toast.LENGTH_SHORT).show()
- finish()
- } else {
- Toast.makeText(this@CreatePlayerActivity, "Error adding user: ${task.exception?.message}", Toast.LENGTH_SHORT).show()
- }
- }
- }
-
- override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
- if (requestCode == REQUEST_CODE_ADD_AVATAR && resultCode == Activity.RESULT_OK) {
- mAvatarUrl = data?.getStringExtra(ChangeAvatarActivity.EXTRA_AVATAR_URL) ?: ""
- ivAvatar.setAvatarUrl(mAvatarUrl)
- }
- super.onActivityResult(requestCode, resultCode, data)
- }
-}
diff --git a/foosball/app/src/tablet/java/com/instructure/androidfoosball/activities/CreateTableKingGameActivity.kt b/foosball/app/src/tablet/java/com/instructure/androidfoosball/activities/CreateTableKingGameActivity.kt
deleted file mode 100644
index 7c4ad4fb05..0000000000
--- a/foosball/app/src/tablet/java/com/instructure/androidfoosball/activities/CreateTableKingGameActivity.kt
+++ /dev/null
@@ -1,147 +0,0 @@
-/*
- * Copyright (C) 2017 - 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.androidfoosball.activities
-
-import android.media.AudioManager
-import android.os.Bundle
-import android.support.v7.app.AppCompatActivity
-import com.afollestad.materialdialogs.MaterialDialog
-import com.google.firebase.database.DataSnapshot
-import com.google.firebase.database.DatabaseError
-import com.google.firebase.database.FirebaseDatabase
-import com.google.firebase.database.ValueEventListener
-import com.instructure.androidfoosball.App
-import com.instructure.androidfoosball.R
-import com.instructure.androidfoosball.ktmodels.*
-import com.instructure.androidfoosball.utils.*
-import io.realm.RealmList
-import kotlinx.android.synthetic.tablet.activity_create_table_king_game.*
-import org.jetbrains.anko.sdk21.listeners.onClick
-import org.jetbrains.anko.startActivity
-import java.util.*
-
-class CreateTableKingGameActivity : AppCompatActivity() {
-
- private val mTable = Table.getSelectedTable()
- private val mIncomingNfcRef = FirebaseDatabase.getInstance().reference.child("incoming").child(mTable.id)
-
- private val DEFAULT_POINTS = 5
-
- private var points = 0
- set(value) {
- field = value
- pointButton.text = value.toString()
- updateDurationRange()
- }
-
- private val nfcListener = object : ValueEventListener {
- override fun onDataChange(dataSnapshot: DataSnapshot) {
- val nfc = dataSnapshot.getValue(IncomingData::class.java) ?: return
- if (nfc.sideOne.isBlank() && nfc.sideTwo.isBlank()) return
- fun getUserById(userId: String): User? = App.realm.where(User::class.java).equalTo("id", userId).findFirst()
- when {
- nfc.sideOne.isNotBlank() -> getUserById(nfc.sideOne)?.let { addUser(it) }
- nfc.sideTwo.isNotBlank() -> getUserById(nfc.sideTwo)?.let { addUser(it) }
- }
- mIncomingNfcRef.setValue(IncomingData())
- }
-
- override fun onCancelled(databaseError: DatabaseError) { }
- }
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- setContentView(R.layout.activity_create_table_king_game)
- volumeControlStream = AudioManager.STREAM_MUSIC
- setupViews()
- mIncomingNfcRef.addValueEventListener(nfcListener)
- }
-
- private fun setupViews() {
- points = DEFAULT_POINTS
-
- // Player selection
- playersLayout.onAddPlayerClicked = { selectPlayer() }
-
- // On players changed
- playersLayout.onPlayersChanged = { onPlayersChanged() }
-
- // Points selection
- pointButton.onClick {
- val options = (3..9).toList()
- MaterialDialog.Builder(this)
- .items(options)
- .itemsCallback { _, _, i, _ -> points = options[i] }
- .show()
- }
-
- // Start game
- startGameButton.onClick { createGame() }
-
- // Set up QR code
- qrCode.setTableSide(mTable, TableSide.SIDE_1)
- }
-
- private fun updateDurationRange() {
- val minGoals = 3 * points
- val maxGoals = 3 * (points * 2 - 1)
- durationView.text = getString(R.string.durationRange, minGoals, maxGoals)
- }
-
- private fun onPlayersChanged() {
- // Show start button if ready
- val ready = playersLayout.players.size == 4
- assignTeamsView.setVisible(!ready)
- startGameButton.setVisible(ready)
- }
-
- private fun selectPlayer() {
- showUserPicker(this) { addUser(it) }
- }
-
- private fun addUser(user: User) {
- playersLayout.addUser(user)
- mCommentator.announce(user.customAssignmentPhrase.elseIfBlank(user.name))
- }
-
- private fun createGame() {
- val players = ArrayList(playersLayout.players)
- Collections.shuffle(players)
-
- val game = TableKingGame()
- game.status = GameStatus.ONGOING.name
- game.pointsToWin = points
- game.startTime = System.currentTimeMillis()
- game.teams = RealmList(
- TeamWithPoints(users = RealmList(players[0], players[1])),
- TeamWithPoints(users = RealmList(players[0], players[2])),
- TeamWithPoints(users = RealmList(players[0], players[3])),
- TeamWithPoints(users = RealmList(players[1], players[2])),
- TeamWithPoints(users = RealmList(players[1], players[3])),
- TeamWithPoints(users = RealmList(players[2], players[3]))
- )
- game.copyToRealmOrUpdate()
-
- startActivity(TableKingGameActivity.EXTRA_GAME_ID to game.id)
- finish()
- }
-
- override fun onDestroy() {
- super.onDestroy()
- mIncomingNfcRef.removeEventListener(nfcListener)
- }
-}
diff --git a/foosball/app/src/tablet/java/com/instructure/androidfoosball/activities/CreateTeamTwisterGameActivity.kt b/foosball/app/src/tablet/java/com/instructure/androidfoosball/activities/CreateTeamTwisterGameActivity.kt
deleted file mode 100644
index 9502f18ff1..0000000000
--- a/foosball/app/src/tablet/java/com/instructure/androidfoosball/activities/CreateTeamTwisterGameActivity.kt
+++ /dev/null
@@ -1,147 +0,0 @@
-/*
- * Copyright (C) 2017 - 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.androidfoosball.activities
-
-import android.media.AudioManager
-import android.os.Bundle
-import android.support.v7.app.AppCompatActivity
-import com.afollestad.materialdialogs.MaterialDialog
-import com.google.firebase.database.DataSnapshot
-import com.google.firebase.database.DatabaseError
-import com.google.firebase.database.FirebaseDatabase
-import com.google.firebase.database.ValueEventListener
-import com.instructure.androidfoosball.App
-import com.instructure.androidfoosball.R
-import com.instructure.androidfoosball.ktmodels.*
-import com.instructure.androidfoosball.utils.*
-import io.realm.RealmList
-import kotlinx.android.synthetic.tablet.activity_create_team_twister_game.*
-import org.jetbrains.anko.sdk21.listeners.onClick
-import org.jetbrains.anko.startActivity
-import java.util.*
-
-class CreateTeamTwisterGameActivity : AppCompatActivity() {
-
- private val mTable = Table.getSelectedTable()
- private val mIncomingNfcRef = FirebaseDatabase.getInstance().reference.child("incoming").child(mTable.id)
-
- private val DEFAULT_POINTS = 4
-
- private var points = 0
- set(value) {
- field = value
- pointButton.text = value.toString()
- updateDurationRange()
- }
-
- private val nfcListener = object : ValueEventListener {
- override fun onDataChange(dataSnapshot: DataSnapshot) {
- val nfc = dataSnapshot.getValue(IncomingData::class.java) ?: return
- if (nfc.sideOne.isBlank() && nfc.sideTwo.isBlank()) return
- fun getUserById(userId: String): User? = App.realm.where(User::class.java).equalTo("id", userId).findFirst()
- when {
- nfc.sideOne.isNotBlank() -> getUserById(nfc.sideOne)?.let { addUser(it) }
- nfc.sideTwo.isNotBlank() -> getUserById(nfc.sideTwo)?.let { addUser(it) }
- }
- mIncomingNfcRef.setValue(IncomingData())
- }
-
- override fun onCancelled(databaseError: DatabaseError) { }
- }
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- setContentView(R.layout.activity_create_team_twister_game)
- volumeControlStream = AudioManager.STREAM_MUSIC
- setupViews()
- mIncomingNfcRef.addValueEventListener(nfcListener)
- }
-
- private fun setupViews() {
- points = DEFAULT_POINTS
-
- // Player selection
- playersLayout.onAddPlayerClicked = { selectPlayer() }
-
- // On players changed
- playersLayout.onPlayersChanged = { onPlayersChanged() }
-
- // Points selection
- pointButton.onClick {
- val options = (3..9).toList()
- MaterialDialog.Builder(this)
- .items(options)
- .itemsCallback { _, _, i, _ -> points = options[i] }
- .show()
- }
-
- // Start game
- startGameButton.onClick { createGame() }
-
- // Set up QR code
- qrCode.setTableSide(mTable, TableSide.SIDE_1)
- }
-
- private fun updateDurationRange() {
- val minGoals = 1 + 3 * (points - 1)
- val maxGoals = 1 + 6 * (points - 1)
- durationView.text = getString(R.string.durationRange, minGoals, maxGoals)
- }
-
- private fun onPlayersChanged() {
- // Show start button if ready
- val ready = playersLayout.players.size == 4
- assignTeamsView.setVisible(!ready)
- startGameButton.setVisible(ready)
- }
-
- private fun selectPlayer() {
- showUserPicker(this) { addUser(it) }
- }
-
- private fun addUser(user: User) {
- playersLayout.addUser(user)
- mCommentator.announce(user.customAssignmentPhrase.elseIfBlank(user.name))
- }
-
- private fun createGame() {
- val players = ArrayList(playersLayout.players)
- Collections.shuffle(players)
-
- val game = TeamTwisterGame()
- game.status = GameStatus.ONGOING.name
- game.pointsToWin = points
- game.startTime = System.currentTimeMillis()
- game.teams = RealmList(
- TeamWithPoints(users = RealmList(players[0], players[1])),
- TeamWithPoints(users = RealmList(players[0], players[2])),
- TeamWithPoints(users = RealmList(players[0], players[3])),
- TeamWithPoints(users = RealmList(players[1], players[2])),
- TeamWithPoints(users = RealmList(players[1], players[3])),
- TeamWithPoints(users = RealmList(players[2], players[3]))
- )
- game.copyToRealmOrUpdate()
-
- startActivity(TeamTwisterGameActivity.EXTRA_GAME_ID to game.id)
- finish()
- }
-
- override fun onDestroy() {
- super.onDestroy()
- mIncomingNfcRef.removeEventListener(nfcListener)
- }
-}
diff --git a/foosball/app/src/tablet/java/com/instructure/androidfoosball/activities/CutThroatGameActivity.kt b/foosball/app/src/tablet/java/com/instructure/androidfoosball/activities/CutThroatGameActivity.kt
deleted file mode 100644
index d8b20501ba..0000000000
--- a/foosball/app/src/tablet/java/com/instructure/androidfoosball/activities/CutThroatGameActivity.kt
+++ /dev/null
@@ -1,211 +0,0 @@
-/*
- * Copyright (C) 2017 - 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.androidfoosball.activities
-
-import android.app.AlarmManager
-import android.app.PendingIntent
-import android.content.Context
-import android.graphics.Color
-import android.media.AudioManager
-import android.os.Bundle
-import android.support.v7.app.AppCompatActivity
-import android.view.View
-import com.afollestad.materialdialogs.MaterialDialog
-import com.google.firebase.database.DatabaseReference
-import com.google.firebase.database.FirebaseDatabase
-import com.instructure.androidfoosball.App
-import com.instructure.androidfoosball.R
-import com.instructure.androidfoosball.ktmodels.CutThroatGame
-import com.instructure.androidfoosball.ktmodels.GameStatus
-import com.instructure.androidfoosball.ktmodels.Table
-import com.instructure.androidfoosball.ktmodels.TableSide
-import com.instructure.androidfoosball.push.PushIntentService
-import com.instructure.androidfoosball.receivers.GoalReceiver
-import com.instructure.androidfoosball.utils.*
-import com.instructure.androidfoosball.views.WinCutThroatGameDialog
-import kotlinx.android.synthetic.tablet.activity_game_cut_throat.*
-import org.jetbrains.anko.sdk21.listeners.onClick
-
-class CutThroatGameActivity : AppCompatActivity() {
-
- companion object {
- val EXTRA_GAME_ID = "gameId"
- }
-
- private val mGameId by lazy { intent.getStringExtra(EXTRA_GAME_ID) ?: "" }
- private val mGame by lazy { App.realm.where(CutThroatGame::class.java).equalTo("id", mGameId).findFirst()!! }
- private val mTable = Table.getSelectedTable()
- val mDatabase: DatabaseReference = FirebaseDatabase.getInstance().reference
- private val mGameHistory = mutableListOf()
-
- private val goalReceiver: GoalReceiver = GoalReceiver { side ->
- when (side) {
- TableSide.SIDE_1 -> rotate()
- TableSide.SIDE_2 -> goal()
- }
- }
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- setContentView(R.layout.activity_game_cut_throat)
- volumeControlStream = AudioManager.STREAM_MUSIC
- setupViews()
- setupGame()
- GoalReceiver.register(this, goalReceiver, 100)
- updateGameStatusBusy()
- }
-
- private fun setupViews() {
-
- // Get table colors
- val sideOneColor = Color.parseColor(mTable.sideOneColor)
- val sideTwoColor = Color.parseColor(mTable.sideTwoColor)
-
- // Set TeamLayout colors
- doublesLayout.setBgColor(sideOneColor)
- singlesLayout.setBgColor(sideTwoColor)
-
- // Hide extras layout if there are only 3 players
- if (mGame.players.size == 3) extrasLayout.visibility = View.GONE
-
- // Pause game
- pauseGameButton.onClick {
- shortToast(R.string.game_paused)
- updateGameStatusFree()
- finish()
- }
-
- // Quit game
- quitGameButton.onClick {
- MaterialDialog.Builder(this)
- .title(R.string.quit_game)
- .content(R.string.confirm_quit_game)
- .negativeText(android.R.string.cancel)
- .positiveText(R.string.quit_game)
- .onPositive { _, _ ->
- mGame.edit { status = GameStatus.CANCELED.name }
- updateGameStatusFree()
- finish()
- }
- .show()
- }
-
- // Tap team to count a goal
- doublesLayout.onClick { goalReceiver.onGoal(TableSide.SIDE_1) }
- singlesLayout.onClick { goalReceiver.onGoal(TableSide.SIDE_2) }
-
- }
-
- private fun setupGame() {
- pointsToWinView.text = mGame.pointsToWin.toString()
- rotateAfterView.text = mGame.rotateAfter.toString()
- refreshPlayers()
- mCommentator.announceGameStart()
- undoView.onClick { undo() }
- }
-
- private fun refreshPlayers() {
- val (singles, doubles, extras) = mGame.players.shift(mGame.singleIdx).split(1, 2)
- singlesLayout.players = singles.toMutableList()
- doublesLayout.players = doubles.toMutableList()
- extrasLayout.players = extras.toMutableList()
- }
-
- private fun goal() {
- mGameHistory += App.realm.copyFromRealm(mGame)
- undoView.setVisible()
- mCommentator.announce(Commentator.Sfx.DING.name)
- mGame.edit {
- getSingle().score++
- pointsSinceRotation++
- }
-
- if (mGame.hasWinner()) {
- mCommentator.announce("${Commentator.Sfx.WINNING_GOAL} ${mGame.getWinner()?.name} wins the game.")
- mCommentator.queueAnnounce(mGame.getWinner()?.customVictoryPhrase ?: "")
- WinCutThroatGameDialog(this, mGame, this::endGame, this::undo).show()
- } else if (mGame.rotateAfter > 0 && mGame.pointsSinceRotation >= mGame.rotateAfter) {
- rotate(false)
- } else {
- singlesLayout.addPlayer(mGame.getSingle())
- }
-
- }
-
- private fun rotate(addToHistory: Boolean = true) {
- if (addToHistory) {
- mGameHistory += App.realm.copyFromRealm(mGame)
- undoView.setVisible()
- }
- mCommentator.queueAnnounce("${Commentator.Sfx.ROTATE_DING} Rotate.")
- mGame.edit {
- pointsSinceRotation = 0
- singleIdx = (singleIdx + 1) % players.size
- }
- refreshPlayers()
- mCommentator.queueAnnounce(mGame.getSingle().user?.name.orEmpty())
- }
-
- private fun undo() {
- mGameHistory.removeLast()?.let { snapshot ->
- App.realm.inTransaction { it.copyToRealmOrUpdate(snapshot) }
- refreshPlayers()
- mCommentator.announce("Oops")
- }
- undoView.setVisible(mGameHistory.isNotEmpty())
- }
-
- private fun endGame() {
- mGame.edit { status = GameStatus.FINISHED.name }
- updateGameStatusFree()
- finish()
- }
-
- override fun onStart() {
- super.onStart()
- val am = this.getSystemService(Context.ALARM_SERVICE) as AlarmManager
- val pi = PendingIntent.getService(this, 0, PushIntentService.getIntent(this, mTable.pushId, mTable.name), PendingIntent.FLAG_CANCEL_CURRENT)
- am.cancel(pi)
- }
-
- override fun onStop() {
- super.onStop()
- val am = this.getSystemService(Context.ALARM_SERVICE) as AlarmManager
- val pi = PendingIntent.getService(this, 0, PushIntentService.getIntent(this, mTable.pushId, mTable.name), PendingIntent.FLAG_CANCEL_CURRENT)
- am.set(AlarmManager.RTC_WAKEUP, 10000, pi)
- }
-
- override fun onDestroy() {
- super.onDestroy()
- GoalReceiver.unregister(this, goalReceiver)
- }
-
- private fun updateGameStatusBusy() {
- mDatabase.child("tables").child(mTable.id).child("currentGame").setValue("BUSY")
- }
-
- private fun updateGameStatusFree() {
- mDatabase.child("tables").child(mTable.id).child("currentGame").setValue("FREE")
-
- }
-
- override fun onBackPressed() {
- // Do nothing
- }
-}
-
-private fun MutableList.removeLast(): E? = if (isEmpty()) null else removeAt(lastIndex)
diff --git a/foosball/app/src/tablet/java/com/instructure/androidfoosball/activities/EloDialogActivity.kt b/foosball/app/src/tablet/java/com/instructure/androidfoosball/activities/EloDialogActivity.kt
deleted file mode 100644
index 6555f2177a..0000000000
--- a/foosball/app/src/tablet/java/com/instructure/androidfoosball/activities/EloDialogActivity.kt
+++ /dev/null
@@ -1,56 +0,0 @@
-/*
- * Copyright (C) 2017 - 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.androidfoosball.activities
-
-import android.content.Context
-import android.content.Intent
-import android.os.Bundle
-import android.support.v7.app.AppCompatActivity
-import android.view.View
-import com.instructure.androidfoosball.R
-import kotlinx.android.synthetic.tablet.activity_elo_layout.*
-import java.util.*
-
-class EloDialogActivity : AppCompatActivity() {
-
- companion object {
- fun createIntent(context: Context, data: HashMap