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 @@ + + + + + + + +