From d471d79e7bcce6de8351cf045458af23711fcdb2 Mon Sep 17 00:00:00 2001 From: Kenneth Murerwa Date: Tue, 24 Oct 2023 04:34:57 +0300 Subject: [PATCH] Fix part of #5025: App and OS Deprecation Milestone 3 - Add New Deprecation Dialog Fragments (#5096) ## Explanation Fix part of #5025 When this PR is merged, it will; - Add fragments and fragment presenters for the Forced, Optional, and OS deprecation dialogs. - Add strings for the new deprecation dialogs. ## Screenshots of the introduced dialog fragments Forced Deprecation Dialog | Optional Deprecation Dialog | OS Deprecation Dialog :-------------------------:|:-------------------------:|:-------------------------: ![A screenshot of the forced deprecation dialog](https://github.com/oppia/oppia-android/assets/18438114/33808235-7867-458b-aa99-bb1166511121) | ![A screenshot of the optional deprecation dialog](https://github.com/oppia/oppia-android/assets/18438114/84e0ffc1-f1ea-49e0-a997-1871aed0134f) | ![A screenshot of the OS deprecation dialog](https://github.com/oppia/oppia-android/assets/18438114/fe4fd5dc-5461-4d12-99d7-8eb3c9160aaf) ## Videos of the dialogs in action ### Forced Deprecation Dialog https://github.com/oppia/oppia-android/assets/18438114/45e8319b-89dd-479a-a41d-5c458707ea50 ### Optional Deprecation Dialog https://github.com/oppia/oppia-android/assets/18438114/32343ff1-34be-4c53-bb64-a4624a15ea86 ### OS Deprecation Dialog https://github.com/oppia/oppia-android/assets/18438114/7d289d25-b7ba-4362-8c4f-a2f8a27f8e63 ## Essential Checklist - [x] The PR title and explanation each start with "Fix #bugnum: " (If this PR fixes part of an issue, prefix the title with "Fix part of #bugnum: ...".) - [x] Any changes to [scripts/assets](https://github.com/oppia/oppia-android/tree/develop/scripts/assets) files have their rationale included in the PR explanation. - [x] The PR follows the [style guide](https://github.com/oppia/oppia-android/wiki/Coding-style-guide). - [x] The PR does not contain any unnecessary code changes from Android Studio ([reference](https://github.com/oppia/oppia-android/wiki/Guidance-on-submitting-a-PR#undo-unnecessary-changes)). - [x] The PR is made from a branch that's **not** called "develop" and is up-to-date with "develop". - [x] The PR is **assigned** to the appropriate reviewers ([reference](https://github.com/oppia/oppia-android/wiki/Guidance-on-submitting-a-PR#clarification-regarding-assignees-and-reviewers-section)). ## For UI-specific PRs only If your PR includes UI-related changes, then: - Add screenshots for portrait/landscape for both a tablet & phone of the before & after UI changes - For the screenshots above, include both English and pseudo-localized (RTL) screenshots (see [RTL guide](https://github.com/oppia/oppia-android/wiki/RTL-Guidelines)) - Add a video showing the full UX flow with a screen reader enabled (see [accessibility guide](https://github.com/oppia/oppia-android/wiki/Accessibility-A11y-Guide)) - Add a screenshot demonstrating that you ran affected Espresso tests locally & that they're passing --------- Co-authored-by: Kenneth Murerwa Co-authored-by: Ben Henning --- app/src/main/AndroidManifest.xml | 12 + .../app/fragment/FragmentComponentImpl.kt | 11 +- .../notice/DeprecationNoticeActionListener.kt | 9 + ...orcedAppDeprecationNoticeDialogFragment.kt | 29 ++ ...eprecationNoticeDialogFragmentPresenter.kt | 47 +++ ...ionalAppDeprecationNoticeDialogFragment.kt | 30 ++ ...eprecationNoticeDialogFragmentPresenter.kt | 47 +++ .../OsDeprecationNoticeDialogFragment.kt | 33 +++ ...eprecationNoticeDialogFragmentPresenter.kt | 42 +++ .../android/app/notice/testing/BUILD.bazel | 39 +++ ...ecationNoticeDialogFragmentTestActivity.kt | 30 ++ ...ecationNoticeDialogFragmentTestActivity.kt | 30 ++ ...ecationNoticeDialogFragmentTestActivity.kt | 30 ++ .../android/app/splash/SplashActivity.kt | 16 +- .../app/splash/SplashActivityPresenter.kt | 39 ++- app/src/main/res/values/strings.xml | 45 ++- .../org/oppia/android/app/notice/BUILD.bazel | 84 ++++++ ...dAppDeprecationNoticeDialogFragmentTest.kt | 274 +++++++++++++++++ ...lAppDeprecationNoticeDialogFragmentTest.kt | 276 ++++++++++++++++++ .../OsDeprecationNoticeDialogFragmentTest.kt | 258 ++++++++++++++++ scripts/assets/test_file_exemptions.textproto | 7 + 21 files changed, 1381 insertions(+), 7 deletions(-) create mode 100644 app/src/main/java/org/oppia/android/app/notice/DeprecationNoticeActionListener.kt create mode 100644 app/src/main/java/org/oppia/android/app/notice/ForcedAppDeprecationNoticeDialogFragment.kt create mode 100644 app/src/main/java/org/oppia/android/app/notice/ForcedAppDeprecationNoticeDialogFragmentPresenter.kt create mode 100644 app/src/main/java/org/oppia/android/app/notice/OptionalAppDeprecationNoticeDialogFragment.kt create mode 100644 app/src/main/java/org/oppia/android/app/notice/OptionalAppDeprecationNoticeDialogFragmentPresenter.kt create mode 100644 app/src/main/java/org/oppia/android/app/notice/OsDeprecationNoticeDialogFragment.kt create mode 100644 app/src/main/java/org/oppia/android/app/notice/OsDeprecationNoticeDialogFragmentPresenter.kt create mode 100644 app/src/main/java/org/oppia/android/app/notice/testing/ForcedAppDeprecationNoticeDialogFragmentTestActivity.kt create mode 100644 app/src/main/java/org/oppia/android/app/notice/testing/OptionalAppDeprecationNoticeDialogFragmentTestActivity.kt create mode 100644 app/src/main/java/org/oppia/android/app/notice/testing/OsDeprecationNoticeDialogFragmentTestActivity.kt create mode 100644 app/src/sharedTest/java/org/oppia/android/app/notice/ForcedAppDeprecationNoticeDialogFragmentTest.kt create mode 100644 app/src/sharedTest/java/org/oppia/android/app/notice/OptionalAppDeprecationNoticeDialogFragmentTest.kt create mode 100644 app/src/sharedTest/java/org/oppia/android/app/notice/OsDeprecationNoticeDialogFragmentTest.kt diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 897fc034879..c324a1a7119 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -92,6 +92,18 @@ android:name=".app.notice.testing.GeneralAvailabilityUpgradeNoticeDialogFragmentTestActivity" android:label="@string/test_activity_label" android:theme="@style/OppiaThemeWithoutActionBar" /> + + + + deprecationNoticeActionListener.onActionButtonClicked( + DeprecationNoticeActionType.UPDATE + ) + } + .setNegativeButton(R.string.forced_app_update_dialog_close_button_text) { _, _ -> + deprecationNoticeActionListener.onActionButtonClicked( + DeprecationNoticeActionType.CLOSE + ) + } + .setCancelable(false) + .create() + dialog.setCanceledOnTouchOutside(false) + return dialog + } +} diff --git a/app/src/main/java/org/oppia/android/app/notice/OptionalAppDeprecationNoticeDialogFragment.kt b/app/src/main/java/org/oppia/android/app/notice/OptionalAppDeprecationNoticeDialogFragment.kt new file mode 100644 index 00000000000..29eb21fd005 --- /dev/null +++ b/app/src/main/java/org/oppia/android/app/notice/OptionalAppDeprecationNoticeDialogFragment.kt @@ -0,0 +1,30 @@ +package org.oppia.android.app.notice + +import android.app.Dialog +import android.content.Context +import android.os.Bundle +import org.oppia.android.app.fragment.FragmentComponentImpl +import org.oppia.android.app.fragment.InjectableDialogFragment +import javax.inject.Inject + +/** Dialog fragment that informs the user that a new app version is available for download. */ +class OptionalAppDeprecationNoticeDialogFragment : InjectableDialogFragment() { + companion object { + /** Returns a new instance of [OptionalAppDeprecationNoticeDialogFragment]. */ + fun newInstance(): OptionalAppDeprecationNoticeDialogFragment { + return OptionalAppDeprecationNoticeDialogFragment() + } + } + + @Inject lateinit var optionalAppDeprecationNoticeDialogFragmentPresenter: + OptionalAppDeprecationNoticeDialogFragmentPresenter + + override fun onAttach(context: Context) { + super.onAttach(context) + (fragmentComponent as FragmentComponentImpl).inject(this) + } + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + return optionalAppDeprecationNoticeDialogFragmentPresenter.handleOnCreateDialog() + } +} diff --git a/app/src/main/java/org/oppia/android/app/notice/OptionalAppDeprecationNoticeDialogFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/notice/OptionalAppDeprecationNoticeDialogFragmentPresenter.kt new file mode 100644 index 00000000000..be4b938c522 --- /dev/null +++ b/app/src/main/java/org/oppia/android/app/notice/OptionalAppDeprecationNoticeDialogFragmentPresenter.kt @@ -0,0 +1,47 @@ +package org.oppia.android.app.notice + +import android.app.Dialog +import androidx.appcompat.app.AlertDialog +import androidx.appcompat.app.AppCompatActivity +import org.oppia.android.R +import org.oppia.android.app.splash.DeprecationNoticeActionType +import org.oppia.android.app.translation.AppLanguageResourceHandler +import javax.inject.Inject + +/** Presenter class responsible for showing an optional update dialog to the user. */ +class OptionalAppDeprecationNoticeDialogFragmentPresenter @Inject constructor( + private val activity: AppCompatActivity, + private val resourceHandler: AppLanguageResourceHandler +) { + private val deprecationNoticeActionListener by lazy { + activity as DeprecationNoticeActionListener + } + + /** Handles dialog creation for the optional app deprecation notice. */ + fun handleOnCreateDialog(): Dialog { + val appName = resourceHandler.getStringInLocale(R.string.app_name) + + val dialog = AlertDialog.Builder(activity, R.style.DeprecationAlertDialogTheme) + .setTitle(R.string.optional_app_update_dialog_title) + .setMessage( + resourceHandler.getStringInLocaleWithWrapping( + R.string.optional_app_update_dialog_message, + appName + ) + ) + .setPositiveButton(R.string.optional_app_update_dialog_update_button_text) { _, _ -> + deprecationNoticeActionListener.onActionButtonClicked( + DeprecationNoticeActionType.UPDATE + ) + } + .setNegativeButton(R.string.optional_app_update_dialog_dismiss_button_text) { _, _ -> + deprecationNoticeActionListener.onActionButtonClicked( + DeprecationNoticeActionType.DISMISS + ) + } + .setCancelable(false) + .create() + dialog.setCanceledOnTouchOutside(false) + return dialog + } +} diff --git a/app/src/main/java/org/oppia/android/app/notice/OsDeprecationNoticeDialogFragment.kt b/app/src/main/java/org/oppia/android/app/notice/OsDeprecationNoticeDialogFragment.kt new file mode 100644 index 00000000000..48e5fb59181 --- /dev/null +++ b/app/src/main/java/org/oppia/android/app/notice/OsDeprecationNoticeDialogFragment.kt @@ -0,0 +1,33 @@ +package org.oppia.android.app.notice + +import android.app.Dialog +import android.content.Context +import android.os.Bundle +import org.oppia.android.app.fragment.FragmentComponentImpl +import org.oppia.android.app.fragment.InjectableDialogFragment +import javax.inject.Inject + +/** + * Dialog fragment that informs the user that their phone OS is no longer supported by Oppia and + * they will no longer be able to update their app to the latest version. + */ +class OsDeprecationNoticeDialogFragment : InjectableDialogFragment() { + companion object { + /** Returns a new instance of [OsDeprecationNoticeDialogFragment]. */ + fun newInstance(): OsDeprecationNoticeDialogFragment { + return OsDeprecationNoticeDialogFragment() + } + } + + @Inject lateinit var osDeprecationNoticeDialogFragmentPresenter: + OsDeprecationNoticeDialogFragmentPresenter + + override fun onAttach(context: Context) { + super.onAttach(context) + (fragmentComponent as FragmentComponentImpl).inject(this) + } + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + return osDeprecationNoticeDialogFragmentPresenter.handleOnCreateDialog() + } +} diff --git a/app/src/main/java/org/oppia/android/app/notice/OsDeprecationNoticeDialogFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/notice/OsDeprecationNoticeDialogFragmentPresenter.kt new file mode 100644 index 00000000000..efcfc84ed20 --- /dev/null +++ b/app/src/main/java/org/oppia/android/app/notice/OsDeprecationNoticeDialogFragmentPresenter.kt @@ -0,0 +1,42 @@ +package org.oppia.android.app.notice + +import android.app.Dialog +import androidx.appcompat.app.AlertDialog +import androidx.appcompat.app.AppCompatActivity +import org.oppia.android.R +import org.oppia.android.app.splash.DeprecationNoticeActionType +import org.oppia.android.app.translation.AppLanguageResourceHandler +import javax.inject.Inject + +/** Presenter class responsible for showing an OS deprecation dialog to the user. */ +class OsDeprecationNoticeDialogFragmentPresenter @Inject constructor( + private val activity: AppCompatActivity, + private val resourceHandler: AppLanguageResourceHandler +) { + private val deprecationNoticeActionListener by lazy { + activity as DeprecationNoticeActionListener + } + + /** Handles dialog creation for the OS deprecation notice. */ + fun handleOnCreateDialog(): Dialog { + val appName = resourceHandler.getStringInLocale(R.string.app_name) + + val dialog = AlertDialog.Builder(activity, R.style.DeprecationAlertDialogTheme) + .setTitle(R.string.os_deprecation_dialog_title) + .setMessage( + resourceHandler.getStringInLocaleWithWrapping( + R.string.os_deprecation_dialog_message, + appName + ) + ) + .setNegativeButton(R.string.os_deprecation_dialog_dismiss_button_text) { _, _ -> + deprecationNoticeActionListener.onActionButtonClicked( + DeprecationNoticeActionType.DISMISS + ) + } + .setCancelable(false) + .create() + dialog.setCanceledOnTouchOutside(false) + return dialog + } +} diff --git a/app/src/main/java/org/oppia/android/app/notice/testing/BUILD.bazel b/app/src/main/java/org/oppia/android/app/notice/testing/BUILD.bazel index 92f1a0ae62b..00ba0065c2b 100644 --- a/app/src/main/java/org/oppia/android/app/notice/testing/BUILD.bazel +++ b/app/src/main/java/org/oppia/android/app/notice/testing/BUILD.bazel @@ -18,6 +18,19 @@ kt_android_library( ], ) +kt_android_library( + name = "forced_app_deprecation_notice_dialog_fragment_test_activity", + testonly = True, + srcs = [ + "ForcedAppDeprecationNoticeDialogFragmentTestActivity.kt", + ], + visibility = ["//app:app_testing_visibility"], + deps = [ + "//app", + "//app/src/main/java/org/oppia/android/app/testing/activity:test_activity", + ], +) + kt_android_library( name = "general_availability_upgrade_notice_dialog_fragment_test_activity", testonly = True, @@ -31,4 +44,30 @@ kt_android_library( ], ) +kt_android_library( + name = "optional_app_deprecation_notice_dialog_fragment_test_activity", + testonly = True, + srcs = [ + "OptionalAppDeprecationNoticeDialogFragmentTestActivity.kt", + ], + visibility = ["//app:app_testing_visibility"], + deps = [ + "//app", + "//app/src/main/java/org/oppia/android/app/testing/activity:test_activity", + ], +) + +kt_android_library( + name = "os_deprecation_notice_dialog_fragment_test_activity", + testonly = True, + srcs = [ + "OsDeprecationNoticeDialogFragmentTestActivity.kt", + ], + visibility = ["//app:app_testing_visibility"], + deps = [ + "//app", + "//app/src/main/java/org/oppia/android/app/testing/activity:test_activity", + ], +) + dagger_rules() diff --git a/app/src/main/java/org/oppia/android/app/notice/testing/ForcedAppDeprecationNoticeDialogFragmentTestActivity.kt b/app/src/main/java/org/oppia/android/app/notice/testing/ForcedAppDeprecationNoticeDialogFragmentTestActivity.kt new file mode 100644 index 00000000000..e6b223af14d --- /dev/null +++ b/app/src/main/java/org/oppia/android/app/notice/testing/ForcedAppDeprecationNoticeDialogFragmentTestActivity.kt @@ -0,0 +1,30 @@ +package org.oppia.android.app.notice.testing + +import android.os.Bundle +import org.oppia.android.app.notice.DeprecationNoticeActionListener +import org.oppia.android.app.notice.ForcedAppDeprecationNoticeDialogFragment +import org.oppia.android.app.splash.DeprecationNoticeActionType +import org.oppia.android.app.testing.activity.TestActivity + +/** [TestActivity] for setting up a test environment for testing the beta notice dialog. */ +class ForcedAppDeprecationNoticeDialogFragmentTestActivity : + TestActivity(), + DeprecationNoticeActionListener { + /** + * [DeprecationNoticeActionListener] that must be initialized by the test, and is presumed to be a + * Mockito mock (though this is not, strictly speaking, required). + * + * This listener will be used as the callback for the dialog in response to UI operations. + */ + lateinit var mockCallbackListener: DeprecationNoticeActionListener + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + ForcedAppDeprecationNoticeDialogFragment.newInstance() + .showNow(supportFragmentManager, "forced_app_deprecation_dialog") + } + + override fun onActionButtonClicked(noticeType: DeprecationNoticeActionType) { + mockCallbackListener.onActionButtonClicked(noticeType) + } +} diff --git a/app/src/main/java/org/oppia/android/app/notice/testing/OptionalAppDeprecationNoticeDialogFragmentTestActivity.kt b/app/src/main/java/org/oppia/android/app/notice/testing/OptionalAppDeprecationNoticeDialogFragmentTestActivity.kt new file mode 100644 index 00000000000..d3ffd8b519e --- /dev/null +++ b/app/src/main/java/org/oppia/android/app/notice/testing/OptionalAppDeprecationNoticeDialogFragmentTestActivity.kt @@ -0,0 +1,30 @@ +package org.oppia.android.app.notice.testing + +import android.os.Bundle +import org.oppia.android.app.notice.DeprecationNoticeActionListener +import org.oppia.android.app.notice.OptionalAppDeprecationNoticeDialogFragment +import org.oppia.android.app.splash.DeprecationNoticeActionType +import org.oppia.android.app.testing.activity.TestActivity + +/** [TestActivity] for setting up a test environment for testing the beta notice dialog. */ +class OptionalAppDeprecationNoticeDialogFragmentTestActivity : + TestActivity(), + DeprecationNoticeActionListener { + /** + * [DeprecationNoticeActionListener] that must be initialized by the test, and is presumed to be a + * Mockito mock (though this is not, strictly speaking, required). + * + * This listener will be used as the callback for the dialog in response to UI operations. + */ + lateinit var mockCallbackListener: DeprecationNoticeActionListener + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + OptionalAppDeprecationNoticeDialogFragment.newInstance() + .showNow(supportFragmentManager, "optional_app_deprecation_dialog") + } + + override fun onActionButtonClicked(noticeType: DeprecationNoticeActionType) { + mockCallbackListener.onActionButtonClicked(noticeType) + } +} diff --git a/app/src/main/java/org/oppia/android/app/notice/testing/OsDeprecationNoticeDialogFragmentTestActivity.kt b/app/src/main/java/org/oppia/android/app/notice/testing/OsDeprecationNoticeDialogFragmentTestActivity.kt new file mode 100644 index 00000000000..13923f43fd8 --- /dev/null +++ b/app/src/main/java/org/oppia/android/app/notice/testing/OsDeprecationNoticeDialogFragmentTestActivity.kt @@ -0,0 +1,30 @@ +package org.oppia.android.app.notice.testing + +import android.os.Bundle +import org.oppia.android.app.notice.DeprecationNoticeActionListener +import org.oppia.android.app.notice.OsDeprecationNoticeDialogFragment +import org.oppia.android.app.splash.DeprecationNoticeActionType +import org.oppia.android.app.testing.activity.TestActivity + +/** [TestActivity] for setting up a test environment for testing the beta notice dialog. */ +class OsDeprecationNoticeDialogFragmentTestActivity : + TestActivity(), + DeprecationNoticeActionListener { + /** + * [DeprecationNoticeActionListener] that must be initialized by the test, and is presumed to be a + * Mockito mock (though this is not, strictly speaking, required). + * + * This listener will be used as the callback for the dialog in response to UI operations. + */ + lateinit var mockCallbackListener: DeprecationNoticeActionListener + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + OsDeprecationNoticeDialogFragment.newInstance() + .showNow(supportFragmentManager, "os_deprecation_dialog") + } + + override fun onActionButtonClicked(noticeType: DeprecationNoticeActionType) { + mockCallbackListener.onActionButtonClicked(noticeType) + } +} diff --git a/app/src/main/java/org/oppia/android/app/splash/SplashActivity.kt b/app/src/main/java/org/oppia/android/app/splash/SplashActivity.kt index 1952e72ae3b..f9310821ddb 100644 --- a/app/src/main/java/org/oppia/android/app/splash/SplashActivity.kt +++ b/app/src/main/java/org/oppia/android/app/splash/SplashActivity.kt @@ -16,6 +16,16 @@ import org.oppia.android.app.notice.GeneralAvailabilityUpgradeNoticeClosedListen import org.oppia.android.util.logging.CurrentAppScreenNameIntentDecorator.decorateWithScreenName import javax.inject.Inject +/** Enum class for the various deprecation notice actions available to the user. */ +enum class DeprecationNoticeActionType { + /** Action for when the user presses the 'Close' option on a deprecation dialog. */ + CLOSE, + /** Action for when the user presses the 'Dismiss' option on a deprecation dialog. */ + DISMISS, + /** Action for when the user presses the 'Update' option on a deprecation dialog. */ + UPDATE +} + /** * An activity that shows a temporary loading page until the app is fully loaded then navigates to * the profile selection screen. @@ -47,10 +57,12 @@ class SplashActivity : override fun createFragmentComponent(fragment: Fragment): FragmentComponent { val builderInjector = activityComponent as FragmentComponentBuilderInjector - return builderInjector.getFragmentComponentBuilderProvider().get().setFragment(fragment).build() + return builderInjector.getFragmentComponentBuilderProvider().get() + .setFragment(fragment).build() } - override fun onCloseAppButtonClicked() = splashActivityPresenter.handleOnCloseAppButtonClicked() + override fun onCloseAppButtonClicked() = splashActivityPresenter + .handleOnDeprecationNoticeCloseAppButtonClicked() override fun onBetaNoticeOkayButtonClicked(permanentlyDismiss: Boolean) = splashActivityPresenter.handleOnBetaNoticeOkayButtonClicked(permanentlyDismiss) diff --git a/app/src/main/java/org/oppia/android/app/splash/SplashActivityPresenter.kt b/app/src/main/java/org/oppia/android/app/splash/SplashActivityPresenter.kt index cfdf37874bf..dd926f56612 100644 --- a/app/src/main/java/org/oppia/android/app/splash/SplashActivityPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/splash/SplashActivityPresenter.kt @@ -1,5 +1,8 @@ package org.oppia.android.app.splash +import android.content.ActivityNotFoundException +import android.content.Intent +import android.net.Uri import androidx.appcompat.app.AppCompatActivity import androidx.databinding.DataBindingUtil import androidx.fragment.app.DialogFragment @@ -64,12 +67,46 @@ class SplashActivityPresenter @Inject constructor( subscribeToOnboardingFlow() } - fun handleOnCloseAppButtonClicked() { + /** Handles cases where the user clicks the close app option on a deprecation notice dialog. */ + fun handleOnDeprecationNoticeCloseAppButtonClicked() { // If the app close button is clicked for the deprecation notice, finish the activity to close // the app. activity.finish() } + /** Handles cases where the user clicks the update option on a deprecation notice dialog. */ + fun handleOnDeprecationNoticeUpdateButtonClicked() { + // If the Update button is clicked for the deprecation notice, launch the Play Store and open + // the Oppia app's page. + val packageName = activity.packageName + + try { + activity.startActivity( + Intent(Intent.ACTION_VIEW, Uri.parse("market://details?id=$packageName")) + ) + } catch (e: ActivityNotFoundException) { + activity.startActivity( + Intent( + Intent.ACTION_VIEW, + Uri.parse( + "https://play.google.com/store/apps/details?id=$packageName" + ) + ) + ) + } + + // Finish splash activity to close the app in anticipation of an update. + activity.finish() + } + + /** Handles cases where the user dismisses the deprecation notice dialog. */ + fun handleOnDeprecationNoticeDialogDismissed() { + // If the Dismiss button is clicked for the deprecation notice, the dialog is automatically + // dismissed. Navigate to profile chooser activity. + activity.startActivity(ProfileChooserActivity.createProfileChooserActivity(activity)) + activity.finish() + } + /** Handles cases when the user dismisses the beta notice dialog. */ fun handleOnBetaNoticeOkayButtonClicked(permanentlyDismiss: Boolean) { if (permanentlyDismiss) { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 79ec1534ff1..97547bc1638 100755 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -472,9 +472,48 @@ completed_story_list_recyclerview_tag Please select all correct choices. - Unsupported app version - This version of the app is no longer supported. Please update it through the Play Store. - Close app + + Unsupported app version + + + This version of the app is no longer supported. Please update it through the Play Store. + + + Close app + + + App update required + + + A new version of %s is now available. The new version is more secure, and improves your learning experience.\n\nThis version is no longer supported. To continue using the app, please update to the latest version. + + + Update + + + Close app + + + New update available + + + A new version of %s is now available. We recommend that you update the app for bug fixes and a better learning experience. + + + Dismiss + + + Update + + + Update your Android OS + + + We recommend updating your Android OS to take advantage of %s\'s new features and lessons.\n\nVisit your phone\'s Settings app to update your OS. + + + Dismiss + Developer Build Alpha Beta diff --git a/app/src/sharedTest/java/org/oppia/android/app/notice/BUILD.bazel b/app/src/sharedTest/java/org/oppia/android/app/notice/BUILD.bazel index 20d4828910e..d0733552b3b 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/notice/BUILD.bazel +++ b/app/src/sharedTest/java/org/oppia/android/app/notice/BUILD.bazel @@ -80,4 +80,88 @@ app_test( ], ) +app_test( + name = "ForcedAppDeprecationNoticeDialogFragmentTest", + processed_src = test_with_resources("ForcedAppDeprecationNoticeDialogFragmentTest.kt"), + test_class = "org.oppia.android.app.notice.ForcedAppDeprecationNoticeDialogFragmentTest", + deps = [ + ":dagger", + "//app", + "//app/src/main/java/org/oppia/android/app/application:application_component", + "//app/src/main/java/org/oppia/android/app/application:application_injector", + "//app/src/main/java/org/oppia/android/app/application:application_injector_provider", + "//app/src/main/java/org/oppia/android/app/application:common_application_modules", + "//app/src/main/java/org/oppia/android/app/application/testing:testing_build_flavor_module", + "//app/src/main/java/org/oppia/android/app/notice/testing:forced_app_deprecation_notice_dialog_fragment_test_activity", + "//app/src/main/java/org/oppia/android/app/translation/testing:test_module", + "//testing", + "//testing/src/main/java/org/oppia/android/testing/junit:initialize_default_locale_rule", + "//testing/src/main/java/org/oppia/android/testing/robolectric:test_module", + "//testing/src/main/java/org/oppia/android/testing/threading:test_module", + "//testing/src/main/java/org/oppia/android/testing/time:test_module", + "//third_party:androidx_test_espresso_espresso-core", + "//third_party:robolectric_android-all", + "//utility/src/main/java/org/oppia/android/util/accessibility:test_module", + "//utility/src/main/java/org/oppia/android/util/caching/testing:caching_test_module", + "//utility/src/main/java/org/oppia/android/util/logging:standard_event_logging_configuration_module", + "//utility/src/main/java/org/oppia/android/util/networking:debug_module", + ], +) + +app_test( + name = "OptionalAppDeprecationNoticeDialogFragmentTest", + processed_src = test_with_resources("OptionalAppDeprecationNoticeDialogFragmentTest.kt"), + test_class = "org.oppia.android.app.notice.OptionalAppDeprecationNoticeDialogFragmentTest", + deps = [ + ":dagger", + "//app", + "//app/src/main/java/org/oppia/android/app/application:application_component", + "//app/src/main/java/org/oppia/android/app/application:application_injector", + "//app/src/main/java/org/oppia/android/app/application:application_injector_provider", + "//app/src/main/java/org/oppia/android/app/application:common_application_modules", + "//app/src/main/java/org/oppia/android/app/application/testing:testing_build_flavor_module", + "//app/src/main/java/org/oppia/android/app/notice/testing:optional_app_deprecation_notice_dialog_fragment_test_activity", + "//app/src/main/java/org/oppia/android/app/translation/testing:test_module", + "//testing", + "//testing/src/main/java/org/oppia/android/testing/junit:initialize_default_locale_rule", + "//testing/src/main/java/org/oppia/android/testing/robolectric:test_module", + "//testing/src/main/java/org/oppia/android/testing/threading:test_module", + "//testing/src/main/java/org/oppia/android/testing/time:test_module", + "//third_party:androidx_test_espresso_espresso-core", + "//third_party:robolectric_android-all", + "//utility/src/main/java/org/oppia/android/util/accessibility:test_module", + "//utility/src/main/java/org/oppia/android/util/caching/testing:caching_test_module", + "//utility/src/main/java/org/oppia/android/util/logging:standard_event_logging_configuration_module", + "//utility/src/main/java/org/oppia/android/util/networking:debug_module", + ], +) + +app_test( + name = "OsDeprecationNoticeDialogFragmentTest", + processed_src = test_with_resources("OsDeprecationNoticeDialogFragmentTest.kt"), + test_class = "org.oppia.android.app.notice.OsDeprecationNoticeDialogFragmentTest", + deps = [ + ":dagger", + "//app", + "//app/src/main/java/org/oppia/android/app/application:application_component", + "//app/src/main/java/org/oppia/android/app/application:application_injector", + "//app/src/main/java/org/oppia/android/app/application:application_injector_provider", + "//app/src/main/java/org/oppia/android/app/application:common_application_modules", + "//app/src/main/java/org/oppia/android/app/application/testing:testing_build_flavor_module", + "//app/src/main/java/org/oppia/android/app/notice/testing:os_deprecation_notice_dialog_fragment_test_activity", + "//app/src/main/java/org/oppia/android/app/translation/testing:test_module", + "//testing", + "//testing/src/main/java/org/oppia/android/testing/junit:initialize_default_locale_rule", + "//testing/src/main/java/org/oppia/android/testing/robolectric:test_module", + "//testing/src/main/java/org/oppia/android/testing/threading:test_module", + "//testing/src/main/java/org/oppia/android/testing/time:test_module", + "//third_party:androidx_test_espresso_espresso-core", + "//third_party:robolectric_android-all", + "//utility/src/main/java/org/oppia/android/util/accessibility:test_module", + "//utility/src/main/java/org/oppia/android/util/caching/testing:caching_test_module", + "//utility/src/main/java/org/oppia/android/util/logging:standard_event_logging_configuration_module", + "//utility/src/main/java/org/oppia/android/util/networking:debug_module", + ], +) + dagger_rules() diff --git a/app/src/sharedTest/java/org/oppia/android/app/notice/ForcedAppDeprecationNoticeDialogFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/notice/ForcedAppDeprecationNoticeDialogFragmentTest.kt new file mode 100644 index 00000000000..d0b1aa4fc7e --- /dev/null +++ b/app/src/sharedTest/java/org/oppia/android/app/notice/ForcedAppDeprecationNoticeDialogFragmentTest.kt @@ -0,0 +1,274 @@ +package org.oppia.android.app.notice + +import android.app.Application +import android.content.Context +import android.view.View +import androidx.appcompat.app.AppCompatActivity +import androidx.test.core.app.ActivityScenario +import androidx.test.core.app.ApplicationProvider +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.action.ViewActions +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.matcher.RootMatchers.isDialog +import androidx.test.espresso.matcher.ViewMatchers.isDisplayed +import androidx.test.espresso.matcher.ViewMatchers.withText +import androidx.test.ext.junit.runners.AndroidJUnit4 +import dagger.BindsInstance +import dagger.Component +import org.hamcrest.Matcher +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.Mockito.verify +import org.mockito.junit.MockitoJUnit +import org.mockito.junit.MockitoRule +import org.oppia.android.R +import org.oppia.android.app.activity.ActivityComponent +import org.oppia.android.app.activity.ActivityComponentFactory +import org.oppia.android.app.activity.route.ActivityRouterModule +import org.oppia.android.app.application.ApplicationComponent +import org.oppia.android.app.application.ApplicationInjector +import org.oppia.android.app.application.ApplicationInjectorProvider +import org.oppia.android.app.application.ApplicationModule +import org.oppia.android.app.application.ApplicationStartupListenerModule +import org.oppia.android.app.application.testing.TestingBuildFlavorModule +import org.oppia.android.app.devoptions.DeveloperOptionsModule +import org.oppia.android.app.devoptions.DeveloperOptionsStarterModule +import org.oppia.android.app.notice.testing.ForcedAppDeprecationNoticeDialogFragmentTestActivity +import org.oppia.android.app.player.state.itemviewmodel.SplitScreenInteractionModule +import org.oppia.android.app.shim.ViewBindingShimModule +import org.oppia.android.app.splash.DeprecationNoticeActionType +import org.oppia.android.app.translation.testing.ActivityRecreatorTestModule +import org.oppia.android.data.backends.gae.NetworkConfigProdModule +import org.oppia.android.data.backends.gae.NetworkModule +import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule +import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule +import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule +import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule +import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule +import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule +import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule +import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule +import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule +import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule +import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule +import org.oppia.android.domain.exploration.ExplorationProgressModule +import org.oppia.android.domain.exploration.ExplorationStorageModule +import org.oppia.android.domain.hintsandsolution.HintsAndSolutionConfigFastShowTestModule +import org.oppia.android.domain.hintsandsolution.HintsAndSolutionProdModule +import org.oppia.android.domain.onboarding.ExpirationMetaDataRetrieverModule +import org.oppia.android.domain.oppialogger.LogStorageModule +import org.oppia.android.domain.oppialogger.LoggingIdentifierModule +import org.oppia.android.domain.oppialogger.analytics.ApplicationLifecycleModule +import org.oppia.android.domain.oppialogger.analytics.CpuPerformanceSnapshotterModule +import org.oppia.android.domain.oppialogger.logscheduler.MetricLogSchedulerModule +import org.oppia.android.domain.oppialogger.loguploader.LogReportWorkerModule +import org.oppia.android.domain.platformparameter.PlatformParameterModule +import org.oppia.android.domain.platformparameter.PlatformParameterSingletonModule +import org.oppia.android.domain.question.QuestionModule +import org.oppia.android.domain.topic.PrimeTopicAssetsControllerModule +import org.oppia.android.domain.workmanager.WorkManagerConfigurationModule +import org.oppia.android.testing.OppiaTestRule +import org.oppia.android.testing.TestImageLoaderModule +import org.oppia.android.testing.TestLogReportingModule +import org.oppia.android.testing.junit.InitializeDefaultLocaleRule +import org.oppia.android.testing.robolectric.RobolectricModule +import org.oppia.android.testing.threading.TestCoroutineDispatchers +import org.oppia.android.testing.threading.TestDispatcherModule +import org.oppia.android.testing.time.FakeOppiaClockModule +import org.oppia.android.util.accessibility.AccessibilityTestModule +import org.oppia.android.util.caching.AssetModule +import org.oppia.android.util.caching.testing.CachingTestModule +import org.oppia.android.util.gcsresource.GcsResourceModule +import org.oppia.android.util.locale.LocaleProdModule +import org.oppia.android.util.logging.EventLoggingConfigurationModule +import org.oppia.android.util.logging.LoggerModule +import org.oppia.android.util.logging.SyncStatusModule +import org.oppia.android.util.logging.firebase.FirebaseLogUploaderModule +import org.oppia.android.util.networking.NetworkConnectionDebugUtilModule +import org.oppia.android.util.networking.NetworkConnectionUtilDebugModule +import org.oppia.android.util.parser.html.HtmlParserEntityTypeModule +import org.oppia.android.util.parser.image.ImageParsingModule +import org.robolectric.annotation.Config +import org.robolectric.annotation.LooperMode +import javax.inject.Inject +import javax.inject.Singleton + +/** Tests for [ForcedAppDeprecationNoticeDialogFragment]. */ +// FunctionName: test names are conventionally named with underscores. +@Suppress("FunctionName") +@RunWith(AndroidJUnit4::class) +@Config( + application = ForcedAppDeprecationNoticeDialogFragmentTest.TestApplication::class, + qualifiers = "port-xxhdpi" +) +@LooperMode(LooperMode.Mode.PAUSED) +class ForcedAppDeprecationNoticeDialogFragmentTest { + @get:Rule + val initializeDefaultLocaleRule = InitializeDefaultLocaleRule() + + @get:Rule + val oppiaTestRule = OppiaTestRule() + + @field:[Rule JvmField] val mockitoRule: MockitoRule = MockitoJUnit.rule() + + @Inject + lateinit var testCoroutineDispatchers: TestCoroutineDispatchers + + @Mock + lateinit var mockDeprecationNoticeActionListener: DeprecationNoticeActionListener + + @Inject + lateinit var context: Context + + @Before + fun setUp() { + setUpTestApplicationComponent() + } + + @Test + fun testFragment_hasExpectedTitle() { + launchForcedAppDeprecationNoticeDialogFragmentTestActivity { + onDialogView(withText(R.string.forced_app_update_dialog_title)).check(matches(isDisplayed())) + } + } + + @Test + fun testFragment_hasExpectedContentMessageTextUnderTitle() { + launchForcedAppDeprecationNoticeDialogFragmentTestActivity { + val appName = context.resources.getString(R.string.app_name) + val expectedString = context.resources.getString( + R.string.forced_app_update_dialog_message, + appName + ) + onDialogView(withText(expectedString)).check(matches(isDisplayed())) + } + } + + @Test + fun testFragment_hasUpdateButton() { + launchForcedAppDeprecationNoticeDialogFragmentTestActivity { + onDialogView(withText(R.string.forced_app_update_dialog_update_button_text)) + .check(matches(isDisplayed())) + } + } + + @Test + fun testFragment_clickOnUpdateButton_callsCallbackListener_withUpdateDeprecationActionType() { + launchForcedAppDeprecationNoticeDialogFragmentTestActivity { + clickOnDialogView(withText(R.string.forced_app_update_dialog_update_button_text)) + + verify(mockDeprecationNoticeActionListener) + .onActionButtonClicked(DeprecationNoticeActionType.UPDATE) + } + } + + @Test + fun testFragment_hasCloseAppButton() { + launchForcedAppDeprecationNoticeDialogFragmentTestActivity { + onDialogView(withText(R.string.forced_app_update_dialog_close_button_text)) + .check(matches(isDisplayed())) + } + } + + @Test + fun testFragment_clockOnCloseAppButton_callsCallbackListener_withCloseDeprecationActionType() { + launchForcedAppDeprecationNoticeDialogFragmentTestActivity { + clickOnDialogView(withText(R.string.forced_app_update_dialog_close_button_text)) + + verify(mockDeprecationNoticeActionListener) + .onActionButtonClicked(DeprecationNoticeActionType.CLOSE) + } + } + + private fun launchForcedAppDeprecationNoticeDialogFragmentTestActivity( + testBlock: () -> Unit + ) { + // Launch the test activity, but make sure that it's properly set up & time is given for it to + // initialize. + ActivityScenario.launch( + ForcedAppDeprecationNoticeDialogFragmentTestActivity::class.java + ).use { scenario -> + scenario.onActivity { it.mockCallbackListener = mockDeprecationNoticeActionListener } + testCoroutineDispatchers.runCurrent() + testBlock() + } + } + + private fun clickOnDialogView(matcher: Matcher) { + onDialogView(matcher).perform(ViewActions.click()) + testCoroutineDispatchers.runCurrent() + } + + private fun setUpTestApplicationComponent() { + ApplicationProvider.getApplicationContext().inject(this) + } + + private companion object { + private fun onDialogView(matcher: Matcher) = onView(matcher).inRoot(isDialog()) + } + + @Singleton + @Component( + modules = [ + RobolectricModule::class, PlatformParameterModule::class, + TestDispatcherModule::class, ApplicationModule::class, LoggerModule::class, + ContinueModule::class, FractionInputModule::class, ItemSelectionInputModule::class, + MultipleChoiceInputModule::class, NumberWithUnitsRuleModule::class, + NumericInputRuleModule::class, TextInputRuleModule::class, DragDropSortInputModule::class, + ImageClickInputModule::class, InteractionsModule::class, GcsResourceModule::class, + TestImageLoaderModule::class, ImageParsingModule::class, HtmlParserEntityTypeModule::class, + QuestionModule::class, TestLogReportingModule::class, AccessibilityTestModule::class, + LogStorageModule::class, PrimeTopicAssetsControllerModule::class, + ExpirationMetaDataRetrieverModule::class, ViewBindingShimModule::class, + RatioInputModule::class, ApplicationStartupListenerModule::class, + HintsAndSolutionConfigFastShowTestModule::class, HintsAndSolutionProdModule::class, + WorkManagerConfigurationModule::class, LogReportWorkerModule::class, + FirebaseLogUploaderModule::class, FakeOppiaClockModule::class, + DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, + ExplorationStorageModule::class, NetworkConnectionUtilDebugModule::class, + NetworkConnectionDebugUtilModule::class, NetworkModule::class, NetworkConfigProdModule::class, + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + PlatformParameterSingletonModule::class, NumericExpressionInputModule::class, + AlgebraicExpressionInputModule::class, MathEquationInputModule::class, + SplitScreenInteractionModule::class, LoggingIdentifierModule::class, + ApplicationLifecycleModule::class, SyncStatusModule::class, TestingBuildFlavorModule::class, + CachingTestModule::class, MetricLogSchedulerModule::class, + EventLoggingConfigurationModule::class, ActivityRouterModule::class, + CpuPerformanceSnapshotterModule::class, ExplorationProgressModule::class + ] + ) + + interface TestApplicationComponent : ApplicationComponent { + @Component.Builder + interface Builder { + @BindsInstance + fun setApplication(application: Application): Builder + + fun build(): TestApplicationComponent + } + + fun inject(test: ForcedAppDeprecationNoticeDialogFragmentTest) + } + + class TestApplication : Application(), ActivityComponentFactory, ApplicationInjectorProvider { + private val component: TestApplicationComponent by lazy { + DaggerForcedAppDeprecationNoticeDialogFragmentTest_TestApplicationComponent.builder() + .setApplication(this) + .build() + } + + fun inject(test: ForcedAppDeprecationNoticeDialogFragmentTest) = component.inject(test) + + override fun createActivityComponent(activity: AppCompatActivity): ActivityComponent { + return component.getActivityComponentBuilderProvider().get().setActivity(activity).build() + } + + override fun getApplicationInjector(): ApplicationInjector = component + } +} diff --git a/app/src/sharedTest/java/org/oppia/android/app/notice/OptionalAppDeprecationNoticeDialogFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/notice/OptionalAppDeprecationNoticeDialogFragmentTest.kt new file mode 100644 index 00000000000..c9545e4e324 --- /dev/null +++ b/app/src/sharedTest/java/org/oppia/android/app/notice/OptionalAppDeprecationNoticeDialogFragmentTest.kt @@ -0,0 +1,276 @@ +package org.oppia.android.app.notice + +import android.app.Application +import android.content.Context +import android.view.View +import androidx.appcompat.app.AppCompatActivity +import androidx.test.core.app.ActivityScenario +import androidx.test.core.app.ApplicationProvider +import androidx.test.espresso.Espresso +import androidx.test.espresso.action.ViewActions +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.matcher.RootMatchers.isDialog +import androidx.test.espresso.matcher.ViewMatchers.isDisplayed +import androidx.test.espresso.matcher.ViewMatchers.withText +import androidx.test.ext.junit.runners.AndroidJUnit4 +import dagger.BindsInstance +import dagger.Component +import org.hamcrest.Matcher +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.Mockito.verify +import org.mockito.junit.MockitoJUnit +import org.mockito.junit.MockitoRule +import org.oppia.android.R +import org.oppia.android.app.activity.ActivityComponent +import org.oppia.android.app.activity.ActivityComponentFactory +import org.oppia.android.app.activity.route.ActivityRouterModule +import org.oppia.android.app.application.ApplicationComponent +import org.oppia.android.app.application.ApplicationInjector +import org.oppia.android.app.application.ApplicationInjectorProvider +import org.oppia.android.app.application.ApplicationModule +import org.oppia.android.app.application.ApplicationStartupListenerModule +import org.oppia.android.app.application.testing.TestingBuildFlavorModule +import org.oppia.android.app.devoptions.DeveloperOptionsModule +import org.oppia.android.app.devoptions.DeveloperOptionsStarterModule +import org.oppia.android.app.notice.testing.OptionalAppDeprecationNoticeDialogFragmentTestActivity +import org.oppia.android.app.player.state.itemviewmodel.SplitScreenInteractionModule +import org.oppia.android.app.shim.ViewBindingShimModule +import org.oppia.android.app.splash.DeprecationNoticeActionType +import org.oppia.android.app.translation.testing.ActivityRecreatorTestModule +import org.oppia.android.data.backends.gae.NetworkConfigProdModule +import org.oppia.android.data.backends.gae.NetworkModule +import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule +import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule +import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule +import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule +import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule +import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule +import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule +import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule +import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule +import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule +import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule +import org.oppia.android.domain.exploration.ExplorationProgressModule +import org.oppia.android.domain.exploration.ExplorationStorageModule +import org.oppia.android.domain.hintsandsolution.HintsAndSolutionConfigFastShowTestModule +import org.oppia.android.domain.hintsandsolution.HintsAndSolutionProdModule +import org.oppia.android.domain.onboarding.ExpirationMetaDataRetrieverModule +import org.oppia.android.domain.oppialogger.LogStorageModule +import org.oppia.android.domain.oppialogger.LoggingIdentifierModule +import org.oppia.android.domain.oppialogger.analytics.ApplicationLifecycleModule +import org.oppia.android.domain.oppialogger.analytics.CpuPerformanceSnapshotterModule +import org.oppia.android.domain.oppialogger.logscheduler.MetricLogSchedulerModule +import org.oppia.android.domain.oppialogger.loguploader.LogReportWorkerModule +import org.oppia.android.domain.platformparameter.PlatformParameterModule +import org.oppia.android.domain.platformparameter.PlatformParameterSingletonModule +import org.oppia.android.domain.question.QuestionModule +import org.oppia.android.domain.topic.PrimeTopicAssetsControllerModule +import org.oppia.android.domain.workmanager.WorkManagerConfigurationModule +import org.oppia.android.testing.OppiaTestRule +import org.oppia.android.testing.TestImageLoaderModule +import org.oppia.android.testing.TestLogReportingModule +import org.oppia.android.testing.junit.InitializeDefaultLocaleRule +import org.oppia.android.testing.robolectric.RobolectricModule +import org.oppia.android.testing.threading.TestCoroutineDispatchers +import org.oppia.android.testing.threading.TestDispatcherModule +import org.oppia.android.testing.time.FakeOppiaClockModule +import org.oppia.android.util.accessibility.AccessibilityTestModule +import org.oppia.android.util.caching.AssetModule +import org.oppia.android.util.caching.testing.CachingTestModule +import org.oppia.android.util.gcsresource.GcsResourceModule +import org.oppia.android.util.locale.LocaleProdModule +import org.oppia.android.util.logging.EventLoggingConfigurationModule +import org.oppia.android.util.logging.LoggerModule +import org.oppia.android.util.logging.SyncStatusModule +import org.oppia.android.util.logging.firebase.FirebaseLogUploaderModule +import org.oppia.android.util.networking.NetworkConnectionDebugUtilModule +import org.oppia.android.util.networking.NetworkConnectionUtilDebugModule +import org.oppia.android.util.parser.html.HtmlParserEntityTypeModule +import org.oppia.android.util.parser.image.ImageParsingModule +import org.robolectric.annotation.Config +import org.robolectric.annotation.LooperMode +import javax.inject.Inject +import javax.inject.Singleton + +/** Tests for [ForcedAppDeprecationNoticeDialogFragment]. */ +// FunctionName: test names are conventionally named with underscores. +@Suppress("FunctionName") +@RunWith(AndroidJUnit4::class) +@Config( + application = OptionalAppDeprecationNoticeDialogFragmentTest.TestApplication::class, + qualifiers = "port-xxhdpi" +) +@LooperMode(LooperMode.Mode.PAUSED) +class OptionalAppDeprecationNoticeDialogFragmentTest { + @get:Rule + val initializeDefaultLocaleRule = InitializeDefaultLocaleRule() + + @get:Rule + val oppiaTestRule = OppiaTestRule() + + @field:[Rule JvmField] val mockitoRule: MockitoRule = MockitoJUnit.rule() + + @Inject + lateinit var testCoroutineDispatchers: TestCoroutineDispatchers + + @Mock + lateinit var mockDeprecationNoticeActionListener: DeprecationNoticeActionListener + + @Inject + lateinit var context: Context + + @Before + fun setUp() { + setUpTestApplicationComponent() + } + + @Test + fun testFragment_hasExpectedTitle() { + launchOptionalAppDeprecationNoticeDialogFragmentTestActivity { + onDialogView(withText(R.string.optional_app_update_dialog_title)) + .check(matches(isDisplayed())) + } + } + + @Test + fun testFragment_hasExpectedContentMessageTextUnderTitle() { + launchOptionalAppDeprecationNoticeDialogFragmentTestActivity { + val appName = context.resources.getString(R.string.app_name) + val expectedString = context.resources.getString( + R.string.optional_app_update_dialog_message, + appName + ) + onDialogView(withText(expectedString)).check(matches(isDisplayed())) + } + } + + @Test + fun testFragment_hasUpdateButton() { + launchOptionalAppDeprecationNoticeDialogFragmentTestActivity { + onDialogView(withText(R.string.optional_app_update_dialog_update_button_text)) + .check(matches(isDisplayed())) + } + } + + @Test + fun testFragment_clickOnUpdateButton_callsCallbackListener_withUpdateDeprecationActionType() { + launchOptionalAppDeprecationNoticeDialogFragmentTestActivity { + clickOnDialogView(withText(R.string.optional_app_update_dialog_update_button_text)) + + verify(mockDeprecationNoticeActionListener) + .onActionButtonClicked(DeprecationNoticeActionType.UPDATE) + } + } + + @Test + fun testFragment_hasDismissButton() { + launchOptionalAppDeprecationNoticeDialogFragmentTestActivity { + onDialogView(withText(R.string.optional_app_update_dialog_dismiss_button_text)) + .check(matches(isDisplayed())) + } + } + + @Test + fun testFragment_clickOnDismissButton_callsCallbackListener_withDismissDeprecationActionType() { + launchOptionalAppDeprecationNoticeDialogFragmentTestActivity { + clickOnDialogView(withText(R.string.optional_app_update_dialog_dismiss_button_text)) + + verify(mockDeprecationNoticeActionListener) + .onActionButtonClicked(DeprecationNoticeActionType.DISMISS) + } + } + + private fun launchOptionalAppDeprecationNoticeDialogFragmentTestActivity( + testBlock: () -> Unit + ) { + // Launch the test activity, but make sure that it's properly set up & time is given for it to + // initialize. + ActivityScenario.launch( + OptionalAppDeprecationNoticeDialogFragmentTestActivity::class.java + ).use { scenario -> + scenario.onActivity { it.mockCallbackListener = mockDeprecationNoticeActionListener } + testCoroutineDispatchers.runCurrent() + testBlock() + } + } + + private fun clickOnDialogView(matcher: Matcher) { + onDialogView(matcher).perform(ViewActions.click()) + testCoroutineDispatchers.runCurrent() + } + + private fun setUpTestApplicationComponent() { + ApplicationProvider.getApplicationContext().inject(this) + } + + private companion object { + private fun onDialogView(matcher: Matcher) = Espresso.onView(matcher) + .inRoot(isDialog()) + } + + @Singleton + @Component( + modules = [ + RobolectricModule::class, PlatformParameterModule::class, + TestDispatcherModule::class, ApplicationModule::class, LoggerModule::class, + ContinueModule::class, FractionInputModule::class, ItemSelectionInputModule::class, + MultipleChoiceInputModule::class, NumberWithUnitsRuleModule::class, + NumericInputRuleModule::class, TextInputRuleModule::class, DragDropSortInputModule::class, + ImageClickInputModule::class, InteractionsModule::class, GcsResourceModule::class, + TestImageLoaderModule::class, ImageParsingModule::class, HtmlParserEntityTypeModule::class, + QuestionModule::class, TestLogReportingModule::class, AccessibilityTestModule::class, + LogStorageModule::class, PrimeTopicAssetsControllerModule::class, + ExpirationMetaDataRetrieverModule::class, ViewBindingShimModule::class, + RatioInputModule::class, ApplicationStartupListenerModule::class, + HintsAndSolutionConfigFastShowTestModule::class, HintsAndSolutionProdModule::class, + WorkManagerConfigurationModule::class, LogReportWorkerModule::class, + FirebaseLogUploaderModule::class, FakeOppiaClockModule::class, + DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, + ExplorationStorageModule::class, NetworkConnectionUtilDebugModule::class, + NetworkConnectionDebugUtilModule::class, NetworkModule::class, NetworkConfigProdModule::class, + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + PlatformParameterSingletonModule::class, NumericExpressionInputModule::class, + AlgebraicExpressionInputModule::class, MathEquationInputModule::class, + SplitScreenInteractionModule::class, LoggingIdentifierModule::class, + ApplicationLifecycleModule::class, SyncStatusModule::class, TestingBuildFlavorModule::class, + CachingTestModule::class, MetricLogSchedulerModule::class, + EventLoggingConfigurationModule::class, ActivityRouterModule::class, + CpuPerformanceSnapshotterModule::class, ExplorationProgressModule::class + ] + ) + + interface TestApplicationComponent : ApplicationComponent { + @Component.Builder + interface Builder { + @BindsInstance + fun setApplication(application: Application): Builder + + fun build(): TestApplicationComponent + } + + fun inject(test: OptionalAppDeprecationNoticeDialogFragmentTest) + } + + class TestApplication : Application(), ActivityComponentFactory, ApplicationInjectorProvider { + private val component: TestApplicationComponent by lazy { + DaggerOptionalAppDeprecationNoticeDialogFragmentTest_TestApplicationComponent.builder() + .setApplication(this) + .build() + } + + fun inject(test: OptionalAppDeprecationNoticeDialogFragmentTest) = component.inject(test) + + override fun createActivityComponent(activity: AppCompatActivity): ActivityComponent { + return component.getActivityComponentBuilderProvider().get().setActivity(activity).build() + } + + override fun getApplicationInjector(): ApplicationInjector = component + } +} diff --git a/app/src/sharedTest/java/org/oppia/android/app/notice/OsDeprecationNoticeDialogFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/notice/OsDeprecationNoticeDialogFragmentTest.kt new file mode 100644 index 00000000000..becd031ecdb --- /dev/null +++ b/app/src/sharedTest/java/org/oppia/android/app/notice/OsDeprecationNoticeDialogFragmentTest.kt @@ -0,0 +1,258 @@ +package org.oppia.android.app.notice + +import android.app.Application +import android.content.Context +import android.view.View +import androidx.appcompat.app.AppCompatActivity +import androidx.test.core.app.ActivityScenario +import androidx.test.core.app.ApplicationProvider +import androidx.test.espresso.Espresso +import androidx.test.espresso.action.ViewActions +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.matcher.RootMatchers.isDialog +import androidx.test.espresso.matcher.ViewMatchers.isDisplayed +import androidx.test.espresso.matcher.ViewMatchers.withText +import androidx.test.ext.junit.runners.AndroidJUnit4 +import dagger.BindsInstance +import dagger.Component +import org.hamcrest.Matcher +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.Mockito.verify +import org.mockito.junit.MockitoJUnit +import org.mockito.junit.MockitoRule +import org.oppia.android.R +import org.oppia.android.app.activity.ActivityComponent +import org.oppia.android.app.activity.ActivityComponentFactory +import org.oppia.android.app.activity.route.ActivityRouterModule +import org.oppia.android.app.application.ApplicationComponent +import org.oppia.android.app.application.ApplicationInjector +import org.oppia.android.app.application.ApplicationInjectorProvider +import org.oppia.android.app.application.ApplicationModule +import org.oppia.android.app.application.ApplicationStartupListenerModule +import org.oppia.android.app.application.testing.TestingBuildFlavorModule +import org.oppia.android.app.devoptions.DeveloperOptionsModule +import org.oppia.android.app.devoptions.DeveloperOptionsStarterModule +import org.oppia.android.app.notice.testing.OsDeprecationNoticeDialogFragmentTestActivity +import org.oppia.android.app.player.state.itemviewmodel.SplitScreenInteractionModule +import org.oppia.android.app.shim.ViewBindingShimModule +import org.oppia.android.app.splash.DeprecationNoticeActionType +import org.oppia.android.app.translation.testing.ActivityRecreatorTestModule +import org.oppia.android.data.backends.gae.NetworkConfigProdModule +import org.oppia.android.data.backends.gae.NetworkModule +import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule +import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule +import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule +import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule +import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule +import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule +import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule +import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule +import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule +import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule +import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule +import org.oppia.android.domain.exploration.ExplorationProgressModule +import org.oppia.android.domain.exploration.ExplorationStorageModule +import org.oppia.android.domain.hintsandsolution.HintsAndSolutionConfigFastShowTestModule +import org.oppia.android.domain.hintsandsolution.HintsAndSolutionProdModule +import org.oppia.android.domain.onboarding.ExpirationMetaDataRetrieverModule +import org.oppia.android.domain.oppialogger.LogStorageModule +import org.oppia.android.domain.oppialogger.LoggingIdentifierModule +import org.oppia.android.domain.oppialogger.analytics.ApplicationLifecycleModule +import org.oppia.android.domain.oppialogger.analytics.CpuPerformanceSnapshotterModule +import org.oppia.android.domain.oppialogger.logscheduler.MetricLogSchedulerModule +import org.oppia.android.domain.oppialogger.loguploader.LogReportWorkerModule +import org.oppia.android.domain.platformparameter.PlatformParameterModule +import org.oppia.android.domain.platformparameter.PlatformParameterSingletonModule +import org.oppia.android.domain.question.QuestionModule +import org.oppia.android.domain.topic.PrimeTopicAssetsControllerModule +import org.oppia.android.domain.workmanager.WorkManagerConfigurationModule +import org.oppia.android.testing.OppiaTestRule +import org.oppia.android.testing.TestImageLoaderModule +import org.oppia.android.testing.TestLogReportingModule +import org.oppia.android.testing.junit.InitializeDefaultLocaleRule +import org.oppia.android.testing.robolectric.RobolectricModule +import org.oppia.android.testing.threading.TestCoroutineDispatchers +import org.oppia.android.testing.threading.TestDispatcherModule +import org.oppia.android.testing.time.FakeOppiaClockModule +import org.oppia.android.util.accessibility.AccessibilityTestModule +import org.oppia.android.util.caching.AssetModule +import org.oppia.android.util.caching.testing.CachingTestModule +import org.oppia.android.util.gcsresource.GcsResourceModule +import org.oppia.android.util.locale.LocaleProdModule +import org.oppia.android.util.logging.EventLoggingConfigurationModule +import org.oppia.android.util.logging.LoggerModule +import org.oppia.android.util.logging.SyncStatusModule +import org.oppia.android.util.logging.firebase.FirebaseLogUploaderModule +import org.oppia.android.util.networking.NetworkConnectionDebugUtilModule +import org.oppia.android.util.networking.NetworkConnectionUtilDebugModule +import org.oppia.android.util.parser.html.HtmlParserEntityTypeModule +import org.oppia.android.util.parser.image.ImageParsingModule +import org.robolectric.annotation.Config +import org.robolectric.annotation.LooperMode +import javax.inject.Inject +import javax.inject.Singleton + +/** Tests for [ForcedAppDeprecationNoticeDialogFragment]. */ +// FunctionName: test names are conventionally named with underscores. +@Suppress("FunctionName") +@RunWith(AndroidJUnit4::class) +@Config( + application = OsDeprecationNoticeDialogFragmentTest.TestApplication::class, + qualifiers = "port-xxhdpi" +) +@LooperMode(LooperMode.Mode.PAUSED) +class OsDeprecationNoticeDialogFragmentTest { + @get:Rule + val initializeDefaultLocaleRule = InitializeDefaultLocaleRule() + + @get:Rule + val oppiaTestRule = OppiaTestRule() + + @field:[Rule JvmField] val mockitoRule: MockitoRule = MockitoJUnit.rule() + + @Inject + lateinit var testCoroutineDispatchers: TestCoroutineDispatchers + + @Mock + lateinit var mockDeprecationNoticeActionListener: DeprecationNoticeActionListener + + @Inject + lateinit var context: Context + + @Before + fun setUp() { + setUpTestApplicationComponent() + } + + @Test + fun testFragment_hasExpectedTitle() { + launchOsDeprecationNoticeDialogFragmentTestActivity { + onDialogView(withText(R.string.os_deprecation_dialog_title)) + .check(matches(isDisplayed())) + } + } + + @Test + fun testFragment_hasExpectedContentMessageTextUnderTitle() { + launchOsDeprecationNoticeDialogFragmentTestActivity { + val appName = context.resources.getString(R.string.app_name) + val expectedString = context.resources.getString( + R.string.os_deprecation_dialog_message, + appName + ) + onDialogView(withText(expectedString)).check(matches(isDisplayed())) + } + } + + @Test + fun testFragment_hasDismissButton() { + launchOsDeprecationNoticeDialogFragmentTestActivity { + onDialogView(withText(R.string.os_deprecation_dialog_dismiss_button_text)) + .check(matches(isDisplayed())) + } + } + + @Test + fun testFragment_clickOnDismissButton_callsCallbackListener_withDismissDeprecationActionType() { + launchOsDeprecationNoticeDialogFragmentTestActivity { + clickOnDialogView(withText(R.string.os_deprecation_dialog_dismiss_button_text)) + + verify(mockDeprecationNoticeActionListener) + .onActionButtonClicked(DeprecationNoticeActionType.DISMISS) + } + } + + private fun launchOsDeprecationNoticeDialogFragmentTestActivity( + testBlock: () -> Unit + ) { + // Launch the test activity, but make sure that it's properly set up & time is given for it to + // initialize. + ActivityScenario.launch( + OsDeprecationNoticeDialogFragmentTestActivity::class.java + ).use { scenario -> + scenario.onActivity { it.mockCallbackListener = mockDeprecationNoticeActionListener } + testCoroutineDispatchers.runCurrent() + testBlock() + } + } + + private fun clickOnDialogView(matcher: Matcher) { + onDialogView(matcher).perform(ViewActions.click()) + testCoroutineDispatchers.runCurrent() + } + + private fun setUpTestApplicationComponent() { + ApplicationProvider.getApplicationContext().inject(this) + } + + private companion object { + private fun onDialogView(matcher: Matcher) = Espresso.onView(matcher) + .inRoot(isDialog()) + } + + @Singleton + @Component( + modules = [ + RobolectricModule::class, PlatformParameterModule::class, + TestDispatcherModule::class, ApplicationModule::class, LoggerModule::class, + ContinueModule::class, FractionInputModule::class, ItemSelectionInputModule::class, + MultipleChoiceInputModule::class, NumberWithUnitsRuleModule::class, + NumericInputRuleModule::class, TextInputRuleModule::class, DragDropSortInputModule::class, + ImageClickInputModule::class, InteractionsModule::class, GcsResourceModule::class, + TestImageLoaderModule::class, ImageParsingModule::class, HtmlParserEntityTypeModule::class, + QuestionModule::class, TestLogReportingModule::class, AccessibilityTestModule::class, + LogStorageModule::class, PrimeTopicAssetsControllerModule::class, + ExpirationMetaDataRetrieverModule::class, ViewBindingShimModule::class, + RatioInputModule::class, ApplicationStartupListenerModule::class, + HintsAndSolutionConfigFastShowTestModule::class, HintsAndSolutionProdModule::class, + WorkManagerConfigurationModule::class, LogReportWorkerModule::class, + FirebaseLogUploaderModule::class, FakeOppiaClockModule::class, + DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, + ExplorationStorageModule::class, NetworkConnectionUtilDebugModule::class, + NetworkConnectionDebugUtilModule::class, NetworkModule::class, NetworkConfigProdModule::class, + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + PlatformParameterSingletonModule::class, NumericExpressionInputModule::class, + AlgebraicExpressionInputModule::class, MathEquationInputModule::class, + SplitScreenInteractionModule::class, LoggingIdentifierModule::class, + ApplicationLifecycleModule::class, SyncStatusModule::class, TestingBuildFlavorModule::class, + CachingTestModule::class, MetricLogSchedulerModule::class, + EventLoggingConfigurationModule::class, ActivityRouterModule::class, + CpuPerformanceSnapshotterModule::class, ExplorationProgressModule::class + ] + ) + + interface TestApplicationComponent : ApplicationComponent { + @Component.Builder + interface Builder { + @BindsInstance + fun setApplication(application: Application): Builder + + fun build(): TestApplicationComponent + } + + fun inject(test: OsDeprecationNoticeDialogFragmentTest) + } + + class TestApplication : Application(), ActivityComponentFactory, ApplicationInjectorProvider { + private val component: TestApplicationComponent by lazy { + DaggerOsDeprecationNoticeDialogFragmentTest_TestApplicationComponent.builder() + .setApplication(this) + .build() + } + + fun inject(test: OsDeprecationNoticeDialogFragmentTest) = component.inject(test) + + override fun createActivityComponent(activity: AppCompatActivity): ActivityComponent { + return component.getActivityComponentBuilderProvider().get().setActivity(activity).build() + } + + override fun getApplicationInjector(): ApplicationInjector = component + } +} diff --git a/scripts/assets/test_file_exemptions.textproto b/scripts/assets/test_file_exemptions.textproto index 699f8b9a9cd..382bd7c1efa 100644 --- a/scripts/assets/test_file_exemptions.textproto +++ b/scripts/assets/test_file_exemptions.textproto @@ -235,8 +235,15 @@ exempted_file_path: "app/src/main/java/org/oppia/android/app/mydownloads/MyDownl exempted_file_path: "app/src/main/java/org/oppia/android/app/mydownloads/MyDownloadsViewPagerAdapter.kt" exempted_file_path: "app/src/main/java/org/oppia/android/app/mydownloads/UpdatesTabFragment.kt" exempted_file_path: "app/src/main/java/org/oppia/android/app/mydownloads/UpdatesTabFragmentPresenter.kt" +exempted_file_path: "app/src/main/java/org/oppia/android/app/notice/DeprecationNoticeActionListener.kt" +exempted_file_path: "app/src/main/java/org/oppia/android/app/notice/ForcedAppDeprecationNoticeDialogFragmentPresenter.kt" +exempted_file_path: "app/src/main/java/org/oppia/android/app/notice/OptionalAppDeprecationNoticeDialogFragmentPresenter.kt" +exempted_file_path: "app/src/main/java/org/oppia/android/app/notice/OsDeprecationNoticeDialogFragmentPresenter.kt" exempted_file_path: "app/src/main/java/org/oppia/android/app/notice/testing/BetaNoticeDialogFragmentTestActivity.kt" +exempted_file_path: "app/src/main/java/org/oppia/android/app/notice/testing/ForcedAppDeprecationNoticeDialogFragmentTestActivity.kt" exempted_file_path: "app/src/main/java/org/oppia/android/app/notice/testing/GeneralAvailabilityUpgradeNoticeDialogFragmentTestActivity.kt" +exempted_file_path: "app/src/main/java/org/oppia/android/app/notice/testing/OptionalAppDeprecationNoticeDialogFragmentTestActivity.kt" +exempted_file_path: "app/src/main/java/org/oppia/android/app/notice/testing/OsDeprecationNoticeDialogFragmentTestActivity.kt" exempted_file_path: "app/src/main/java/org/oppia/android/app/onboarding/OnboadingSlideViewModel.kt" exempted_file_path: "app/src/main/java/org/oppia/android/app/onboarding/OnboardingActivityPresenter.kt" exempted_file_path: "app/src/main/java/org/oppia/android/app/onboarding/OnboardingFragmentPresenter.kt"