From 11bd95b0843ca2717701ab7010942fc2509d31d7 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Wed, 1 Sep 2021 14:53:59 -0700 Subject: [PATCH] Fix #3753: Fix some translatable/untranslatable strings (#3754) * Fix some translatable/untranslatable strings. * Fix untranslatable/untranslated inconsistency. --- .github/CODEOWNERS | 1 + app/BUILD.bazel | 7 +- .../android/app/help/faq/FAQListViewModel.kt | 43 ++-- .../faqItemViewModel/FAQContentViewModel.kt | 7 +- .../android/app/home/WelcomeViewModel.kt | 2 +- .../app/onboarding/OnboadingSlideViewModel.kt | 9 +- .../profile/PinPasswordActivityPresenter.kt | 3 +- .../android/app/utility/datetime/BUILD.bazel | 33 ++++ .../app/utility/datetime/DateTimeUtil.kt | 29 +++ app/src/main/res/values/array.xml | 27 --- app/src/main/res/values/faqs.xml | 41 ++-- app/src/main/res/values/strings.xml | 32 ++- .../res/values/third_party_dependencies.xml | 28 +-- .../main/res/values/untranslated_strings.xml | 4 + .../android/app/faq/FAQListFragmentTest.kt | 32 ++- .../app/onboarding/OnboardingFragmentTest.kt | 17 +- .../app/profile/PinPasswordActivityTest.kt | 9 +- .../app/utility/datetime/DateTimeUtilTest.kt | 183 ++++++++++++++++++ .../file_content_validation_checks.textproto | 45 +++-- .../assets/kdoc_validity_exemptions.textproto | 1 - .../file_content_validation_checks.proto | 2 +- .../regex/RegexPatternValidationCheck.kt | 79 ++++---- .../regex/RegexPatternValidationCheckTest.kt | 172 ++++++++++++---- utility/BUILD.bazel | 2 - .../oppia/android/util/datetime/BUILD.bazel | 18 -- .../android/util/datetime/DateTimeUtil.kt | 26 --- utility/src/main/res/values/strings.xml | 7 - .../android/util/datetime/DateTimeUtilTest.kt | 118 ----------- 28 files changed, 605 insertions(+), 372 deletions(-) create mode 100644 app/src/main/java/org/oppia/android/app/utility/datetime/BUILD.bazel create mode 100644 app/src/main/java/org/oppia/android/app/utility/datetime/DateTimeUtil.kt delete mode 100644 app/src/main/res/values/array.xml create mode 100644 app/src/main/res/values/untranslated_strings.xml create mode 100644 app/src/test/java/org/oppia/android/app/utility/datetime/DateTimeUtilTest.kt delete mode 100644 utility/src/main/java/org/oppia/android/util/datetime/BUILD.bazel delete mode 100644 utility/src/main/java/org/oppia/android/util/datetime/DateTimeUtil.kt delete mode 100644 utility/src/main/res/values/strings.xml delete mode 100644 utility/src/test/java/org/oppia/android/util/datetime/DateTimeUtilTest.kt diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index df464bc8636..0e7e70d0913 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -56,6 +56,7 @@ gradlew.bat @BenHenning # App UI strings. /app/src/main/res/values*/strings.xml @BenHenning +/app/src/main/res/values*/untranslated_strings.xml @BenHenning # Proguard configuration. *.pro @BenHenning diff --git a/app/BUILD.bazel b/app/BUILD.bazel index 08c1423991d..02534a05c75 100644 --- a/app/BUILD.bazel +++ b/app/BUILD.bazel @@ -28,7 +28,9 @@ exports_files(["src/main/AndroidManifest.xml"]) # NOTE: if the file is added here make sure to remove this from the other sub lists # of EXCLUDED_APP_LIB_FILES. # keep sorted -MIGRATED_SOURCE_FILES = [ +MIGRATED_SOURCE_FILES = glob([ + "src/main/java/org/oppia/android/app/utility/datetime/*.kt", +]) + [ "src/main/java/org/oppia/android/app/viewmodel/ObservableArrayList.kt", "src/main/java/org/oppia/android/app/viewmodel/ObservableViewModel.kt", ] @@ -502,7 +504,7 @@ android_library( ["src/main/res/**"], exclude = DATABINDING_LAYOUTS, ), - visibility = ["//visibility:private"], + visibility = ["//app:__subpackages__"], deps = ["//third_party:com_google_android_material_material"], ) @@ -606,6 +608,7 @@ kt_android_library( "//app/src/main/java/org/oppia/android/app/viewmodel:observable_array_list", "//app/src/main/java/org/oppia/android/app/viewmodel:observable_view_model", "//app/src/main/java/org/oppia/android/app/viewmodel:view_model_provider", + "//app/src/main/java/org/oppia/android/app/utility/datetime:date_time_util", "//domain", "//third_party:androidx_core_core", "//third_party:androidx_databinding_databinding-common", diff --git a/app/src/main/java/org/oppia/android/app/help/faq/FAQListViewModel.kt b/app/src/main/java/org/oppia/android/app/help/faq/FAQListViewModel.kt index 0b74868eb41..49247adea02 100644 --- a/app/src/main/java/org/oppia/android/app/help/faq/FAQListViewModel.kt +++ b/app/src/main/java/org/oppia/android/app/help/faq/FAQListViewModel.kt @@ -6,32 +6,41 @@ import org.oppia.android.app.help.faq.faqItemViewModel.FAQContentViewModel import org.oppia.android.app.help.faq.faqItemViewModel.FAQHeaderViewModel import org.oppia.android.app.help.faq.faqItemViewModel.FAQItemViewModel import org.oppia.android.app.viewmodel.ObservableViewModel +import java.util.Locale import javax.inject.Inject /** View model in [FAQListFragment]. */ class FAQListViewModel @Inject constructor( val activity: AppCompatActivity ) : ObservableViewModel() { - private val arrayList = ArrayList() - val faqItemList: List by lazy { - getRecyclerViewItemList() + computeFaqViewModelList() + } + + private fun computeFaqViewModelList(): List { + val questions = retrieveQuestions() + val faqs = questions.zip(retrieveAnswers()).mapIndexed { index, (question, answer) -> + FAQContentViewModel(activity, question, answer, showDivider = index != questions.lastIndex) + } + return listOf(FAQHeaderViewModel()) + faqs } - private fun getRecyclerViewItemList(): ArrayList { - val faqHeaderViewModel = FAQHeaderViewModel() - arrayList.add(faqHeaderViewModel) - val questions: Array = activity.resources.getStringArray(R.array.faq_questions) - val answers: Array = activity.resources.getStringArray(R.array.faq_answers) - questions.forEachIndexed { index, question -> - val faqContentViewModel = FAQContentViewModel(activity, question, answers[index]) - if (questions[questions.size - 1] == question) { - faqContentViewModel.showDivider.set(false) - } else { - faqContentViewModel.showDivider.set(true) - } - arrayList.add(faqContentViewModel) + private fun retrieveQuestionsOrAnswers(questionsOrAnswers: Array): List { + val appName = activity.resources.getString(R.string.app_name) + return questionsOrAnswers.mapIndexed { index, questionOrAnswer -> + if (index == QUESTION_INDEX_WITH_OPPIA_REFERENCE) { + String.format(Locale.getDefault(), questionOrAnswer, appName) + } else questionOrAnswer } - return arrayList + } + + private fun retrieveQuestions(): List = + retrieveQuestionsOrAnswers(activity.resources.getStringArray(R.array.faq_questions)) + + private fun retrieveAnswers(): List = + retrieveQuestionsOrAnswers(activity.resources.getStringArray(R.array.faq_answers)) + + private companion object { + private const val QUESTION_INDEX_WITH_OPPIA_REFERENCE = 3 } } diff --git a/app/src/main/java/org/oppia/android/app/help/faq/faqItemViewModel/FAQContentViewModel.kt b/app/src/main/java/org/oppia/android/app/help/faq/faqItemViewModel/FAQContentViewModel.kt index 9dbf5fc7790..75d556a0a9b 100644 --- a/app/src/main/java/org/oppia/android/app/help/faq/faqItemViewModel/FAQContentViewModel.kt +++ b/app/src/main/java/org/oppia/android/app/help/faq/faqItemViewModel/FAQContentViewModel.kt @@ -1,19 +1,16 @@ package org.oppia.android.app.help.faq.faqItemViewModel import androidx.appcompat.app.AppCompatActivity -import androidx.databinding.ObservableField import org.oppia.android.app.help.faq.RouteToFAQSingleListener /** Content view model for the recycler view in [FAQFragment]. */ class FAQContentViewModel( private val activity: AppCompatActivity, val question: String, - val answer: String + val answer: String, + val showDivider: Boolean ) : FAQItemViewModel() { - /** Used to control visibility of divider. */ - val showDivider = ObservableField(true) - fun clickOnFAQQuestion() { val routeToFAQSingleListener = activity as RouteToFAQSingleListener routeToFAQSingleListener.onRouteToFAQSingle(question, answer) diff --git a/app/src/main/java/org/oppia/android/app/home/WelcomeViewModel.kt b/app/src/main/java/org/oppia/android/app/home/WelcomeViewModel.kt index 3fafd5f0320..bd261278ba4 100644 --- a/app/src/main/java/org/oppia/android/app/home/WelcomeViewModel.kt +++ b/app/src/main/java/org/oppia/android/app/home/WelcomeViewModel.kt @@ -2,7 +2,7 @@ package org.oppia.android.app.home import androidx.fragment.app.Fragment import androidx.lifecycle.ViewModel -import org.oppia.android.util.datetime.DateTimeUtil +import org.oppia.android.app.utility.datetime.DateTimeUtil import org.oppia.android.util.system.OppiaClock import java.util.Objects diff --git a/app/src/main/java/org/oppia/android/app/onboarding/OnboadingSlideViewModel.kt b/app/src/main/java/org/oppia/android/app/onboarding/OnboadingSlideViewModel.kt index bfde018c382..0886853ee4e 100644 --- a/app/src/main/java/org/oppia/android/app/onboarding/OnboadingSlideViewModel.kt +++ b/app/src/main/java/org/oppia/android/app/onboarding/OnboadingSlideViewModel.kt @@ -14,7 +14,7 @@ class OnboardingSlideViewModel(val context: Context, viewPagerSlide: ViewPagerSl OnboardingViewPagerViewModel() { val slideImage = ObservableField(R.drawable.ic_portrait_onboarding_0) val title = - ObservableField(context.resources.getString(R.string.onboarding_slide_0_title)) + ObservableField(getOnboardingSlide0Title()) val description = ObservableField(context.resources.getString(R.string.onboarding_slide_0_description)) private val orientation = Resources.getSystem().configuration.orientation @@ -35,7 +35,7 @@ class OnboardingSlideViewModel(val context: Context, viewPagerSlide: ViewPagerSl } else if (orientation == Configuration.ORIENTATION_PORTRAIT) { slideImage.set(R.drawable.ic_portrait_onboarding_0) } - title.set(context.resources.getString(R.string.onboarding_slide_0_title)) + title.set(getOnboardingSlide0Title()) description.set(context.resources.getString(R.string.onboarding_slide_0_description)) } ViewPagerSlide.SLIDE_1 -> { @@ -66,4 +66,9 @@ class OnboardingSlideViewModel(val context: Context, viewPagerSlide: ViewPagerSl } } } + + private fun getOnboardingSlide0Title(): String { + val appName = context.resources.getString(R.string.app_name) + return context.resources.getString(R.string.onboarding_slide_0_title, appName) + } } diff --git a/app/src/main/java/org/oppia/android/app/profile/PinPasswordActivityPresenter.kt b/app/src/main/java/org/oppia/android/app/profile/PinPasswordActivityPresenter.kt index 96ab59568eb..d360dc47a39 100644 --- a/app/src/main/java/org/oppia/android/app/profile/PinPasswordActivityPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/profile/PinPasswordActivityPresenter.kt @@ -148,10 +148,11 @@ class PinPasswordActivityPresenter @Inject constructor( } private fun showAdminForgotPin() { + val appName = activity.resources.getString(R.string.app_name) pinViewModel.showAdminPinForgotPasswordPopUp.set(true) alertDialog = AlertDialog.Builder(activity, R.style.AlertDialogTheme) .setTitle(R.string.pin_password_forgot_title) - .setMessage(R.string.pin_password_forgot_message) + .setMessage(activity.resources.getString(R.string.pin_password_forgot_message, appName)) .setNegativeButton(R.string.admin_settings_cancel) { dialog, _ -> pinViewModel.showAdminPinForgotPasswordPopUp.set(false) dialog.dismiss() diff --git a/app/src/main/java/org/oppia/android/app/utility/datetime/BUILD.bazel b/app/src/main/java/org/oppia/android/app/utility/datetime/BUILD.bazel new file mode 100644 index 00000000000..2d8f423c5e3 --- /dev/null +++ b/app/src/main/java/org/oppia/android/app/utility/datetime/BUILD.bazel @@ -0,0 +1,33 @@ +""" +General purposes utilities to manage date and time in user-facing strings. +""" + +load("@dagger//:workspace_defs.bzl", "dagger_rules") +load("@io_bazel_rules_kotlin//kotlin:kotlin.bzl", "kt_android_library") + +# Resource shim needed so that DateTimeUtil can build in both Gradle & Bazel. +genrule( + name = "update_DateTimeUtil", + srcs = ["DateTimeUtil.kt"], + outs = ["DateTimeUtil_updated.kt"], + cmd = """ + cat $(SRCS) | + sed 's/import org.oppia.android.R/import org.oppia.android.app.R/g' > $(OUTS) + """, +) + +kt_android_library( + name = "date_time_util", + srcs = [ + "DateTimeUtil_updated.kt", + ], + visibility = ["//app:__subpackages__"], + deps = [ + ":dagger", + "//app:resources", + "//third_party:javax_inject_javax_inject", + "//utility/src/main/java/org/oppia/android/util/system:oppia_clock", + ], +) + +dagger_rules() diff --git a/app/src/main/java/org/oppia/android/app/utility/datetime/DateTimeUtil.kt b/app/src/main/java/org/oppia/android/app/utility/datetime/DateTimeUtil.kt new file mode 100644 index 00000000000..b8b8ca80df8 --- /dev/null +++ b/app/src/main/java/org/oppia/android/app/utility/datetime/DateTimeUtil.kt @@ -0,0 +1,29 @@ +package org.oppia.android.app.utility.datetime + +import android.content.Context +import org.oppia.android.R +import org.oppia.android.util.system.OppiaClock +import java.util.Calendar +import javax.inject.Inject +import javax.inject.Singleton + +/** Utility to manage date and time for user-facing strings. */ +@Singleton +class DateTimeUtil @Inject constructor( + private val context: Context, + private val oppiaClock: OppiaClock +) { + /** + * Returns a user-readable string based on the time of day (to be concatenated as part of a + * greeting for the user). + */ + fun getGreetingMessage(): String { + val calender = oppiaClock.getCurrentCalendar() + return when (calender.get(Calendar.HOUR_OF_DAY)) { + in 4..11 -> context.getString(R.string.home_screen_good_morning_greeting_fragment) + in 12..16 -> context.getString(R.string.home_screen_good_afternoon_greeting_fragment) + in 17 downTo 3 -> context.getString(R.string.home_screen_good_evening_greeting_fragment) + else -> context.getString(R.string.home_screen_good_evening_greeting_fragment) + } + } +} diff --git a/app/src/main/res/values/array.xml b/app/src/main/res/values/array.xml deleted file mode 100644 index 03481b21086..00000000000 --- a/app/src/main/res/values/array.xml +++ /dev/null @@ -1,27 +0,0 @@ - - - - - @string/faq_question_1 - @string/faq_question_2 - @string/faq_question_3 - @string/faq_question_4 - @string/faq_question_5 - @string/faq_question_6 - @string/faq_question_7 - @string/faq_question_8 - @string/faq_question_9 - - - - @string/faq_answer_1 - @string/faq_answer_2 - @string/faq_answer_3 - @string/faq_answer_4 - @string/faq_answer_5 - @string/faq_answer_6 - @string/faq_answer_7 - @string/faq_answer_8 - @string/faq_answer_9 - - diff --git a/app/src/main/res/values/faqs.xml b/app/src/main/res/values/faqs.xml index 4fc629a7489..f6879492939 100644 --- a/app/src/main/res/values/faqs.xml +++ b/app/src/main/res/values/faqs.xml @@ -1,21 +1,26 @@ - How can I create a new profile? - How can I delete a profile? - How can I change my email/phone number? - What is Oppia? - Who is an Administrator? - Why is the Exploration player not loading? - Why is my audio not playing? - How do I download a Topic? - I can\'t find my question here. What now? - If it is your first time creating a profile and not have a PIN:

1. From the Profile Chooser, tap on Set up Multiple Profiles.

2. Create a PIN and Save.

3. Fill in all fields for the profile.

  1. (Optional) Upload a photo.
  2. Enter a name.
  3. (Optional) Assign a 3-digit PIN.

4. Tap Create. This profile is added to your Profile Chooser!

If you have created a profile before and have a PIN:

1. From the Profile Chooser, tap on Add Profile.

2. Enter your PIN and tap Submit.

3. Fill in all fields for the profile.

  1. (Optional) Upload a photo.
  2. Enter a name.
  3. (Optional) Assign a 3-digit PIN.

4. Tap Create. This profile is added to your Profile Chooser!

Note: Only the Administrator is able to manage profiles.

]]>
- Once a profile is deleted:


  • The profile cannot be recovered.
  • Profile information such as name, photos, and progress will be permanently deleted.

  • To delete a profile (excluding the Administrator\'s):

    1. From the Administrator\'s Home Page, tap on the menu button on the top left.

    2. Tap on Administrator Controls.

    3. Tap on Edit Profiles.

    4. Tap on the Profile you would like to delete.

    5. At the bottom of the screen, tap Profile Deletion.

    6. Tap Delete to confirm deletion.


    Note: Only the Administrator is able to manage profiles.

    ]]>
    - To change your email/phone number:

    1. From the Administrator\'s Home Page, tap on the menu button on the top left.

    2. Tap on Administrator Controls.

    3. Tap on Edit Account.


    If you want to change your email:

    4. Enter your new email and tap Save.

    5. A confirmation link is sent to confirm your new email. The link will expire after 24 hours and must be clicked on to be associated with your account.


    If changing your phone number:

    4. Enter your new phone number and tap Verify.

    5. A code is sent to confirm your new number. The code will expire after 5 minutes and must be entered in the new screen to be associated with your account.

    ]]>
    - Oppia \"O-pee-yah\" (Finnish) - \"to learn\"


    Oppia\'s mission is to help anyone learn anything they want in an effective and enjoyable way.


    By creating a set of free, high-quality, demonstrably effective lessons with the help of educators from around the world, Oppia aims to provide students with quality education — regardless of where they are or what traditional resources they have access to.


    As a student, you can begin your learning adventure by browsing the topics listed on the Home Page!

    ]]>
    - An Administrator is the main user that manages profiles and settings for every profile on their account. They are most likely your parent, teacher, or guardian that created this profile for you.


    Administrators have the ability to manage profiles, assign PINs, and change other settings under their account. Depending on your profile, Administrator permissions may be required for certain features such as downloading Topics, changing your PIN, and more.


    To see who your Administrator is, go to the Profile Chooser. The first profile listed and has \"Administrator\" written under their name is the Administrator.

    ]]>
    - If the Exploration Player is not loading


    Check to see if the app is up to date:

    1. Go to the Play Store and make sure the app is updated to its latest version


    Check your internet connection:

  • If your internet connection is slow, try re-connecting to your Wi-Fi network or connecting to a different network.

  • Ask the Administrator to check their device and internet connection:

  • Get the Administrator to troubleshoot using the steps above

  • Let us know if you still have issues with loading:

  • Report a problem by contacting us at admin@oppia.org.
  • ]]>
    - If your audio is not playing


    Check to see if the app is up to date:

  • Go to the Play Store and make sure the app is updated to its latest version

  • Check your internet connection:

  • If your internet connection is slow, try re-connecting to your Wi-Fi network or connecting to a different network. Slow internet may cause the audio to load irregularly, making it difficult to play.

  • Ask the Administrator to check their device and internet connection:

  • Get the Administrator to troubleshoot using the steps above

  • Let us know if you still have issues with loading:

  • Report a problem by contacting us at admin@oppia.org.
  • ]]>
    - To download an Exploration:

    1. From the Home Page, tap on a Topic or Exploration.

    2. From that Topic Page, tap on the Info tab.

    3. Tap on Download Topic.

    4. Depending on your app settings, you may need Administrator approval or stable Wifi connection to complete your download. If needed, once these requirements are satisfied, the Topic has been downloaded onto the device and can be used offline by all profiles.

    ]]> - If you cannot find your question or would like to report a bug, contact us at admin@oppia.org.

    ]]>
    + + @string/faq_question_1 + @string/faq_question_2 + @string/faq_question_3 + @string/faq_question_4 + @string/faq_question_5 + @string/faq_question_6 + @string/faq_question_7 + @string/faq_question_8 + @string/faq_question_9 + + + + @string/faq_answer_1 + @string/faq_answer_2 + @string/faq_answer_3 + @string/faq_answer_4 + @string/faq_answer_5 + @string/faq_answer_6 + @string/faq_answer_7 + @string/faq_answer_8 + @string/faq_answer_9 +
    diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c4ec41529b6..742c1f99308 100755 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,6 +1,5 @@ - Oppia Navigation header Home Options @@ -13,8 +12,6 @@ Administrator Controls Navigation Menu Open Navigation Menu Close - Welcome to Oppia! - Welcome back to Oppia! Play audio Pause audio OK @@ -66,7 +63,7 @@ Frequently Asked Questions FAQs (Frequently Asked Questions) PIN verification - Oppia Introduction + Introduction Frequently Asked Questions (FAQs) Info Lessons @@ -249,7 +246,7 @@ Current profile picture Edit profile picture - Welcome to Oppia! + Welcome to %s! Learn anything you want in an effective and enjoyable way. Add users to your account. Share the experience and create up to 10 profiles. @@ -272,7 +269,7 @@ Close PIN change is successful Forgot PIN? - To reset your PIN, please uninstall Oppia and then reinstall it.\n\nKeep in mind that if the device has not been online, you may lose user progress on multiple accounts. + To reset your PIN, please uninstall %s and then reinstall it.\n\nKeep in mind that if the device has not been online, you may lose user progress on multiple accounts. Go to the play store Show/Hide password icon Password shown icon @@ -503,4 +500,27 @@ Resume Lesson Continue Start Over + + Good morning, + Good afternoon, + Good evening, + + How can I create a new profile? + How can I delete a profile? + How can I change my email/phone number? + What is %s? + Who is an Administrator? + Why is the Exploration player not loading? + Why is my audio not playing? + How do I download a Topic? + I can\'t find my question here. What now? + If it is your first time creating a profile and not have a PIN:

    1. From the Profile Chooser, tap on Set up Multiple Profiles.

    2. Create a PIN and Save.

    3. Fill in all fields for the profile.

    1. (Optional) Upload a photo.
    2. Enter a name.
    3. (Optional) Assign a 3-digit PIN.

    4. Tap Create. This profile is added to your Profile Chooser!

    If you have created a profile before and have a PIN:

    1. From the Profile Chooser, tap on Add Profile.

    2. Enter your PIN and tap Submit.

    3. Fill in all fields for the profile.

    1. (Optional) Upload a photo.
    2. Enter a name.
    3. (Optional) Assign a 3-digit PIN.

    4. Tap Create. This profile is added to your Profile Chooser!

    Note: Only the Administrator is able to manage profiles.

    ]]>
    + Once a profile is deleted:


  • The profile cannot be recovered.
  • Profile information such as name, photos, and progress will be permanently deleted.

  • To delete a profile (excluding the Administrator\'s):

    1. From the Administrator\'s Home Page, tap on the menu button on the top left.

    2. Tap on Administrator Controls.

    3. Tap on Edit Profiles.

    4. Tap on the Profile you would like to delete.

    5. At the bottom of the screen, tap Profile Deletion.

    6. Tap Delete to confirm deletion.


    Note: Only the Administrator is able to manage profiles.

    ]]>
    + To change your email/phone number:

    1. From the Administrator\'s Home Page, tap on the menu button on the top left.

    2. Tap on Administrator Controls.

    3. Tap on Edit Account.


    If you want to change your email:

    4. Enter your new email and tap Save.

    5. A confirmation link is sent to confirm your new email. The link will expire after 24 hours and must be clicked on to be associated with your account.


    If changing your phone number:

    4. Enter your new phone number and tap Verify.

    5. A code is sent to confirm your new number. The code will expire after 5 minutes and must be entered in the new screen to be associated with your account.

    ]]>
    + %1$s \"O-pee-yah\" (Finnish) - \"to learn\"


    %1$s\'s mission is to help anyone learn anything they want in an effective and enjoyable way.


    By creating a set of free, high-quality, demonstrably effective lessons with the help of educators from around the world, %1$s aims to provide students with quality education — regardless of where they are or what traditional resources they have access to.


    As a student, you can begin your learning adventure by browsing the topics listed on the Home Page!

    ]]>
    + An Administrator is the main user that manages profiles and settings for every profile on their account. They are most likely your parent, teacher, or guardian that created this profile for you.


    Administrators have the ability to manage profiles, assign PINs, and change other settings under their account. Depending on your profile, Administrator permissions may be required for certain features such as downloading Topics, changing your PIN, and more.


    To see who your Administrator is, go to the Profile Chooser. The first profile listed and has \"Administrator\" written under their name is the Administrator.

    ]]>
    + If the Exploration Player is not loading


    Check to see if the app is up to date:

    1. Go to the Play Store and make sure the app is updated to its latest version


    Check your internet connection:

  • If your internet connection is slow, try re-connecting to your Wi-Fi network or connecting to a different network.

  • Ask the Administrator to check their device and internet connection:

  • Get the Administrator to troubleshoot using the steps above

  • Let us know if you still have issues with loading:

  • Report a problem by contacting us at admin@oppia.org.
  • ]]>
    + If your audio is not playing


    Check to see if the app is up to date:

  • Go to the Play Store and make sure the app is updated to its latest version

  • Check your internet connection:

  • If your internet connection is slow, try re-connecting to your Wi-Fi network or connecting to a different network. Slow internet may cause the audio to load irregularly, making it difficult to play.

  • Ask the Administrator to check their device and internet connection:

  • Get the Administrator to troubleshoot using the steps above

  • Let us know if you still have issues with loading:

  • Report a problem by contacting us at admin@oppia.org.
  • ]]>
    + To download an Exploration:

    1. From the Home Page, tap on a Topic or Exploration.

    2. From that Topic Page, tap on the Info tab.

    3. Tap on Download Topic.

    4. Depending on your app settings, you may need Administrator approval or stable Wifi connection to complete your download. If needed, once these requirements are satisfied, the Topic has been downloaded onto the device and can be used offline by all profiles.

    ]]> + If you cannot find your question or would like to report a bug, contact us at admin@oppia.org.

    ]]>
    diff --git a/app/src/main/res/values/third_party_dependencies.xml b/app/src/main/res/values/third_party_dependencies.xml index b97f957d4f5..6265f6f2ac5 100644 --- a/app/src/main/res/values/third_party_dependencies.xml +++ b/app/src/main/res/values/third_party_dependencies.xml @@ -1,27 +1,27 @@ - artifact.name:dependency0 - artifact.name:dependency1 - artifact.name:dependency2 - artifact.name:dependency3 + artifact.name:dependency0 + artifact.name:dependency1 + artifact.name:dependency2 + artifact.name:dependency3 @string/third_party_dependency_name_0 @string/third_party_dependency_name_1 @string/third_party_dependency_name_2 @string/third_party_dependency_name_3 - 1.1.0 - 1.2.0 - 2.1.0 - 3.0.0 + 1.1.0 + 1.2.0 + 2.1.0 + 3.0.0 @string/third_party_dependency_version_0 @string/third_party_dependency_version_1 @string/third_party_dependency_version_2 @string/third_party_dependency_version_3 - " + " Dummy License Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut @@ -60,8 +60,8 @@ at volutpat diam ut venenatis tellus in. Urna neque viverra justo nec ultrices dui sapien eget mi. - "https://example.com" - " + "https://example.com" + " This is a dummy license. Libero nunc consequat interdum varius sit amet. Lectus arcu bibendum at varius vel pharetra. Cras @@ -113,9 +113,9 @@ @array/third_party_dependency_license_names_2 @array/third_party_dependency_license_names_3 - The Apache Software License, Version 2.0 - Crashlytics Terms of Service - Simplified BSD License + The Apache Software License, Version 2.0 + Crashlytics Terms of Service + Simplified BSD License @string/license_name_0 @string/license_name_1 diff --git a/app/src/main/res/values/untranslated_strings.xml b/app/src/main/res/values/untranslated_strings.xml new file mode 100644 index 00000000000..59c00524574 --- /dev/null +++ b/app/src/main/res/values/untranslated_strings.xml @@ -0,0 +1,4 @@ + + + Oppia + diff --git a/app/src/sharedTest/java/org/oppia/android/app/faq/FAQListFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/faq/FAQListFragmentTest.kt index d4937cb6b4c..29e09b5ff14 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/faq/FAQListFragmentTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/faq/FAQListFragmentTest.kt @@ -171,13 +171,39 @@ class FAQListFragmentTest { } } + @Test + fun openFaqListActivity_selectQuestionWithOppiaInName_opensFaqSingleActivity() { + launch(FAQListActivity::class.java).use { + onView( + atPosition( + recyclerViewId = R.id.faq_fragment_recycler_view, + position = 4 + ) + ).perform(click()) + intended( + allOf( + hasExtra( + FAQSingleActivity.FAQ_SINGLE_ACTIVITY_QUESTION, + getResources().getString(R.string.faq_question_4, getAppName()) + ), + hasExtra( + FAQSingleActivity.FAQ_SINGLE_ACTIVITY_ANSWER, + getResources().getString(R.string.faq_answer_4, getAppName()) + ), + hasComponent(FAQSingleActivity::class.java.name) + ) + ) + } + } + private fun setUpTestApplicationComponent() { ApplicationProvider.getApplicationContext().inject(this) } - private fun getResources(): Resources { - return ApplicationProvider.getApplicationContext().resources - } + private fun getResources(): Resources = + ApplicationProvider.getApplicationContext().resources + + private fun getAppName(): String = getResources().getString(R.string.app_name) // TODO(#59): Figure out a way to reuse modules instead of needing to re-declare them. @Singleton diff --git a/app/src/sharedTest/java/org/oppia/android/app/onboarding/OnboardingFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/onboarding/OnboardingFragmentTest.kt index 5d1f4c5e08b..3d7de3f3d88 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/onboarding/OnboardingFragmentTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/onboarding/OnboardingFragmentTest.kt @@ -2,6 +2,7 @@ package org.oppia.android.app.onboarding import android.app.Application import android.content.Context +import android.content.res.Resources import android.view.View import androidx.appcompat.app.AppCompatActivity import androidx.test.core.app.ActivityScenario.launch @@ -137,7 +138,7 @@ class OnboardingFragmentTest { withId(R.id.slide_title_text_view), isCompletelyDisplayed() ) - ).check(matches(withText(R.string.onboarding_slide_0_title))) + ).check(matches(withText(getOnboardingSlide0Title()))) } } @@ -220,7 +221,7 @@ class OnboardingFragmentTest { withId(R.id.slide_title_text_view), isCompletelyDisplayed() ) - ).check(matches(withText(R.string.onboarding_slide_0_title))) + ).check(matches(withText(getOnboardingSlide0Title()))) } } @@ -327,7 +328,7 @@ class OnboardingFragmentTest { withId(R.id.slide_title_text_view), isCompletelyDisplayed() ) - ).check(matches(withText(R.string.onboarding_slide_0_title))) + ).check(matches(withText(getOnboardingSlide0Title()))) } } @@ -504,7 +505,7 @@ class OnboardingFragmentTest { withId(R.id.slide_title_text_view), isCompletelyDisplayed() ) - ).check(matches(withText(R.string.onboarding_slide_0_title))) + ).check(matches(withText(getOnboardingSlide0Title()))) } } @@ -627,6 +628,14 @@ class OnboardingFragmentTest { } } + private fun getResources(): Resources = + ApplicationProvider.getApplicationContext().resources + + private fun getAppName(): String = getResources().getString(R.string.app_name) + + private fun getOnboardingSlide0Title(): String = + getResources().getString(R.string.onboarding_slide_0_title, getAppName()) + private fun scrollToPosition(position: Int): ViewAction { return object : ViewAction { override fun getDescription(): String { diff --git a/app/src/sharedTest/java/org/oppia/android/app/profile/PinPasswordActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/profile/PinPasswordActivityTest.kt index 301ec6e2001..450bdaffb1d 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/profile/PinPasswordActivityTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/profile/PinPasswordActivityTest.kt @@ -308,7 +308,7 @@ class PinPasswordActivityTest { closeSoftKeyboard() ) onView(withId(R.id.forgot_pin)).perform(click()) - onView(withText(context.getString(R.string.pin_password_forgot_message))) + onView(withText(getPinPasswordForgotMessage())) .inRoot(isDialog()) .check(matches(isDisplayed())) } @@ -611,7 +611,7 @@ class PinPasswordActivityTest { closeSoftKeyboard() onView(withId(R.id.forgot_pin)).perform(click()) onView(isRoot()).perform(orientationLandscape()) - onView(withText(context.getString(R.string.pin_password_forgot_message))) + onView(withText(getPinPasswordForgotMessage())) .inRoot(isDialog()) .check(matches(isDisplayed())) } @@ -1053,6 +1053,11 @@ class PinPasswordActivityTest { } } + private fun getAppName(): String = context.resources.getString(R.string.app_name) + + private fun getPinPasswordForgotMessage(): String = + context.resources.getString(R.string.pin_password_forgot_message, getAppName()) + private fun hasErrorText(@StringRes expectedErrorTextId: Int): Matcher { return object : TypeSafeMatcher() { override fun matchesSafely(view: View): Boolean { diff --git a/app/src/test/java/org/oppia/android/app/utility/datetime/DateTimeUtilTest.kt b/app/src/test/java/org/oppia/android/app/utility/datetime/DateTimeUtilTest.kt new file mode 100644 index 00000000000..b5dc50a600f --- /dev/null +++ b/app/src/test/java/org/oppia/android/app/utility/datetime/DateTimeUtilTest.kt @@ -0,0 +1,183 @@ +package org.oppia.android.app.utility.datetime + +import android.app.Application +import android.content.Context +import androidx.appcompat.app.AppCompatActivity +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.google.common.truth.Truth.assertThat +import dagger.BindsInstance +import dagger.Component +import dagger.Module +import dagger.Provides +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.oppia.android.app.activity.ActivityComponent +import org.oppia.android.app.application.ActivityComponentFactory +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.devoptions.DeveloperOptionsModule +import org.oppia.android.app.devoptions.DeveloperOptionsStarterModule +import org.oppia.android.app.topic.PracticeTabModule +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.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.multiplechoiceinput.MultipleChoiceInputModule +import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +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.lightweightcheckpointing.ExplorationStorageModule +import org.oppia.android.domain.hintsandsolution.HintsAndSolutionConfigModule +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.platformparameter.PlatformParameterModule +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.TestLogReportingModule +import org.oppia.android.testing.robolectric.RobolectricModule +import org.oppia.android.testing.threading.TestDispatcherModule +import org.oppia.android.testing.time.FakeOppiaClock +import org.oppia.android.testing.time.FakeOppiaClockModule +import org.oppia.android.util.accessibility.AccessibilityTestModule +import org.oppia.android.util.caching.testing.CachingTestModule +import org.oppia.android.util.gcsresource.GcsResourceModule +import org.oppia.android.util.logging.EnableConsoleLog +import org.oppia.android.util.logging.EnableFileLog +import org.oppia.android.util.logging.GlobalLogLevel +import org.oppia.android.util.logging.LogLevel +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.GlideImageLoaderModule +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 + +// Time: Tue Apr 23 2019 23:22:00 +private const val EVENING_TIMESTAMP = 1556061720000 +// Time: Wed Apr 24 2019 08:22:00 +private const val MORNING_TIMESTAMP = 1556094120000 +// Time: Tue Apr 23 2019 14:22:00 +private const val AFTERNOON_TIMESTAMP = 1556029320000 + +/** Tests for [DateTimeUtil]. */ +@RunWith(AndroidJUnit4::class) +@LooperMode(LooperMode.Mode.PAUSED) +@Config(application = DateTimeUtilTest.TestApplication::class) +class DateTimeUtilTest { + + @Inject lateinit var dateTimeUtil: DateTimeUtil + @Inject lateinit var context: Context + @Inject lateinit var fakeOppiaClock: FakeOppiaClock + + @Before + fun setUp() { + setUpTestApplicationComponent() + fakeOppiaClock.setFakeTimeMode(FakeOppiaClock.FakeTimeMode.MODE_FIXED_FAKE_TIME) + } + + @Test + fun testGreetingMessageBasedOnTime_goodEveningMessageSucceeded() { + fakeOppiaClock.setCurrentTimeToSameDateTime(EVENING_TIMESTAMP) + assertThat(dateTimeUtil.getGreetingMessage()).isEqualTo("Good evening,") + } + + @Test + fun testGreetingMessageBasedOnTime_goodMorningMessageSucceeded() { + fakeOppiaClock.setCurrentTimeToSameDateTime(MORNING_TIMESTAMP) + assertThat(dateTimeUtil.getGreetingMessage()).isEqualTo("Good morning,") + } + + @Test + fun testGreetingMessageBasedOnTime_goodAfternoonMessageSucceeded() { + fakeOppiaClock.setCurrentTimeToSameDateTime(AFTERNOON_TIMESTAMP) + assertThat(dateTimeUtil.getGreetingMessage()).isEqualTo("Good afternoon,") + } + + private fun setUpTestApplicationComponent() { + ApplicationProvider.getApplicationContext().inject(this) + } + + // TODO(#89): Move this to a common test application component. + @Module + class TestModule { + // TODO(#59): Either isolate these to their own shared test module, or use the real logging + // module in tests to avoid needing to specify these settings for tests. + @EnableConsoleLog + @Provides + fun provideEnableConsoleLog(): Boolean = true + + @EnableFileLog + @Provides + fun provideEnableFileLog(): Boolean = false + + @GlobalLogLevel + @Provides + fun provideGlobalLogLevel(): LogLevel = LogLevel.VERBOSE + } + + // TODO(#89): Move this to a common test application component. + @Singleton + @Component( + modules = [ + TestModule::class, RobolectricModule::class, FakeOppiaClockModule::class, + TestLogReportingModule::class, TestDispatcherModule::class, ApplicationModule::class, + ApplicationStartupListenerModule::class, WorkManagerConfigurationModule::class, + ImageParsingModule::class, AccessibilityTestModule::class, PracticeTabModule::class, + GcsResourceModule::class, NetworkConnectionUtilDebugModule::class, LogStorageModule::class, + NetworkModule::class, PlatformParameterModule::class, HintsAndSolutionProdModule::class, + CachingTestModule::class, InteractionsModule::class, ExplorationStorageModule::class, + QuestionModule::class, NetworkConfigProdModule::class, ContinueModule::class, + FractionInputModule::class, ItemSelectionInputModule::class, MultipleChoiceInputModule::class, + NumberWithUnitsRuleModule::class, NumericInputRuleModule::class, TextInputRuleModule::class, + DragDropSortInputModule::class, ImageClickInputModule::class, RatioInputModule::class, + HintsAndSolutionConfigModule::class, ExpirationMetaDataRetrieverModule::class, + GlideImageLoaderModule::class, PrimeTopicAssetsControllerModule::class, + HtmlParserEntityTypeModule::class, NetworkConnectionDebugUtilModule::class, + DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class + ] + ) + interface TestApplicationComponent : ApplicationComponent { + @Component.Builder + interface Builder { + @BindsInstance + fun setApplication(application: Application): Builder + + fun build(): TestApplicationComponent + } + + fun inject(dateTimeUtilTest: DateTimeUtilTest) + } + + class TestApplication : Application(), ActivityComponentFactory, ApplicationInjectorProvider { + private val component: TestApplicationComponent by lazy { + DaggerDateTimeUtilTest_TestApplicationComponent.builder() + .setApplication(this) + .build() + } + + fun inject(dateTimeUtilTest: DateTimeUtilTest) { + component.inject(dateTimeUtilTest) + } + + override fun createActivityComponent(activity: AppCompatActivity): ActivityComponent { + return component.getActivityComponentBuilderProvider().get().setActivity(activity).build() + } + + override fun getApplicationInjector(): ApplicationInjector = component + } +} diff --git a/scripts/assets/file_content_validation_checks.textproto b/scripts/assets/file_content_validation_checks.textproto index 28bf0b4af67..27552f874d7 100644 --- a/scripts/assets/file_content_validation_checks.textproto +++ b/scripts/assets/file_content_validation_checks.textproto @@ -1,16 +1,16 @@ file_content_checks { - filename_regex: ".+?.kt" + file_path_regex: ".+?.kt" prohibited_content_regex: "^import .+?support.+?$" failure_message: "AndroidX should be used instead of the support library" } file_content_checks { - filename_regex: ".+?.kt" + file_path_regex: ".+?.kt" prohibited_content_regex: "CoroutineWorker" failure_message: "For stable tests, prefer using ListenableWorker with an Oppia-managed dispatcher." exempted_file_name: "scripts/src/javatests/org/oppia/android/scripts/regex/RegexPatternValidationCheckTest.kt" } file_content_checks { - filename_regex: ".+?.kt" + file_path_regex: ".+?.kt" prohibited_content_regex: "SettableFuture" failure_message: "SettableFuture should only be used in pre-approved locations since it's easy to potentially mess up & lead to a hanging ListenableFuture." exempted_file_name: "domain/src/main/java/org/oppia/android/domain/oppialogger/loguploader/LogUploadWorker.kt" @@ -18,27 +18,27 @@ file_content_checks { exempted_file_name: "scripts/src/javatests/org/oppia/android/scripts/regex/RegexPatternValidationCheckTest.kt" } file_content_checks { - filename_regex: ".+?.xml" + file_path_regex: ".+?.xml" prohibited_content_regex: "android:gravity=\"left\"" failure_message: "Use android:gravity=\"start\", instead, for proper RTL support" } file_content_checks { - filename_regex: ".+?.xml" + file_path_regex: ".+?.xml" prohibited_content_regex: "android:gravity=\"right\"" failure_message: "Use android:gravity=\"end\", instead, for proper RTL support" } file_content_checks { - filename_regex: ".+?.xml" + file_path_regex: ".+?.xml" prohibited_content_regex: "android:layout_gravity=\"left\"" failure_message: "Use android:layout_gravity=\"start\", instead, for proper RTL support" } file_content_checks { - filename_regex: ".+?.xml" + file_path_regex: ".+?.xml" prohibited_content_regex: "android:layout_gravity=\"right\"" failure_message: "Use android:layout_gravity=\"end\", instead, for proper RTL support" } file_content_checks { - filename_regex: ".+?.xml" + file_path_regex: ".+?.xml" prohibited_content_regex: "paddingLeft|paddingRight|drawableLeft|drawableRight|layout_alignLeft|layout_alignRight|layout_marginLeft|layout_marginRight|layout_alignParentLeft|layout_alignParentRight|layout_toLeftOf|layout_toRightOf|layout_constraintLeft_toLeftOf|layout_constraintLeft_toRightOf|layout_constraintRight_toLeftOf|layout_constraintRight_toRightOf|layout_goneMarginLeft|layout_goneMarginRight" failure_message: "Use start/end versions of layout properties, instead, for proper RTL support" exempted_file_name: "app/src/main/res/layout/add_profile_activity.xml" @@ -52,12 +52,12 @@ file_content_checks { exempted_file_name: "app/src/main/res/values/styles.xml" } file_content_checks { - filename_regex: ".+?.xml" + file_path_regex: ".+?.xml" prohibited_content_regex: "app:barrierDirection=\"left\"" failure_message: "Use app:barrierDirection=\"start\", instead, for proper RTL support" } file_content_checks { - filename_regex: ".+?.xml" + file_path_regex: ".+?.xml" prohibited_content_regex: "app:barrierDirection=\"right\"" failure_message: "Use app:barrierDirection=\"end\", instead, for proper RTL support" exempted_file_name: "app/src/main/res/layout/topic_practice_subtopic.xml" @@ -66,22 +66,39 @@ file_content_checks { exempted_file_name: "app/src/main/res/layout-sw600dp-port/topic_practice_subtopic.xml" } file_content_checks { - filename_regex: ".+?.xml" + file_path_regex: ".+?.xml" prohibited_content_regex: "motion:dragDirection=\"left\"" failure_message: "Use motion:dragDirection=\"start\", instead, for proper RTL support" } file_content_checks { - filename_regex: ".+?.xml" + file_path_regex: ".+?.xml" prohibited_content_regex: "motion:dragDirection=\"right\"" failure_message: "Use motion:dragDirection=\"end\", instead, for proper RTL support" } file_content_checks { - filename_regex: ".+?.xml" + file_path_regex: ".+?.xml" prohibited_content_regex: "motion:touchAnchorSide=\"left\"" failure_message: "Use motion:touchAnchorSide=\"start\", instead, for proper RTL support" } file_content_checks { - filename_regex: ".+?.xml" + file_path_regex: ".+?.xml" prohibited_content_regex: "motion:touchAnchorSide=\"right\"" failure_message: "Use motion:touchAnchorSide=\"end\", instead, for proper RTL support" } +file_content_checks { + file_path_regex: "app/src/main/res/values/strings.xml" + prohibited_content_regex: "Oppia" + failure_message: "Oppia should never used directly in a string (since it shouldn't be translated). Instead, use a parameter & insert the string retrieved from app_name." +} +file_content_checks { + file_path_regex: "app/src/main/res/values/strings.xml" + prohibited_content_regex: "translatable=\"false\"" + failure_message: "Untranslatable strings should go in untranslated_strings.xml, instead." + exempted_file_name: "app/src/main/res/values/untranslated_strings.xml" +} +file_content_checks { + file_path_regex: ".+?.xml" + prohibited_content_regex: "" + failure_message: "All strings outside strings.xml must be marked as not translatable, or moved to strings.xml." + exempted_file_name: "app/src/main/res/values/strings.xml" +} diff --git a/scripts/assets/kdoc_validity_exemptions.textproto b/scripts/assets/kdoc_validity_exemptions.textproto index 4ce7de64e58..f3c9f62453c 100644 --- a/scripts/assets/kdoc_validity_exemptions.textproto +++ b/scripts/assets/kdoc_validity_exemptions.textproto @@ -398,7 +398,6 @@ exempted_file_path: "testing/src/main/java/org/oppia/android/testing/threading/T exempted_file_path: "testing/src/main/java/org/oppia/android/testing/threading/TestCoroutineDispatchersEspressoImpl.kt" exempted_file_path: "testing/src/main/java/org/oppia/android/testing/threading/TestCoroutineDispatchersRobolectricImpl.kt" exempted_file_path: "testing/src/test/java/org/oppia/android/testing/threading/TestCoroutineDispatcherTestBase.kt" -exempted_file_path: "utility/src/main/java/org/oppia/android/util/datetime/DateTimeUtil.kt" exempted_file_path: "utility/src/main/java/org/oppia/android/util/logging/EventBundleCreator.kt" exempted_file_path: "utility/src/main/java/org/oppia/android/util/logging/LogLevel.kt" exempted_file_path: "utility/src/main/java/org/oppia/android/util/parser/html/ConceptCardTagHandler.kt" diff --git a/scripts/src/java/org/oppia/android/scripts/proto/file_content_validation_checks.proto b/scripts/src/java/org/oppia/android/scripts/proto/file_content_validation_checks.proto index 772b1668475..8e439361a63 100644 --- a/scripts/src/java/org/oppia/android/scripts/proto/file_content_validation_checks.proto +++ b/scripts/src/java/org/oppia/android/scripts/proto/file_content_validation_checks.proto @@ -14,7 +14,7 @@ message FileContentChecks { // Check to ensure if any prohibited file content is used in the codebase. message FileContentCheck { // Matches to filename in which to do the content check. - string filename_regex = 1; + string file_path_regex = 1; // Regex which should not be contained in the file. string prohibited_content_regex = 2; diff --git a/scripts/src/java/org/oppia/android/scripts/regex/RegexPatternValidationCheck.kt b/scripts/src/java/org/oppia/android/scripts/regex/RegexPatternValidationCheck.kt index 4cc6bac07f5..e3826a45d1f 100644 --- a/scripts/src/java/org/oppia/android/scripts/regex/RegexPatternValidationCheck.kt +++ b/scripts/src/java/org/oppia/android/scripts/regex/RegexPatternValidationCheck.kt @@ -25,6 +25,7 @@ import java.io.FileInputStream fun main(vararg args: String) { // Path of the repo to be analyzed. val repoPath = "${args[0]}/" + val repoRoot = File(repoPath) // A list of all files in the repo to be analyzed. val searchFiles = RepositoryFile.collectSearchFiles(repoPath) @@ -33,7 +34,7 @@ fun main(vararg args: String) { val hasFilenameCheckFailure = retrieveFilenameChecks() .fold(initial = false) { isFailing, filenameCheck -> val checkFailed = checkProhibitedFileNamePattern( - repoPath, + repoRoot, searchFiles, filenameCheck, ) @@ -44,7 +45,7 @@ fun main(vararg args: String) { val hasFileContentCheckFailure = retrieveFileContentChecks() .fold(initial = false) { isFailing, fileContentCheck -> val checkFailed = checkProhibitedContent( - repoPath, + repoRoot, searchFiles, fileContentCheck ) @@ -74,7 +75,7 @@ private fun retrieveFilenameChecks(): List { return loadProto( "filename_pattern_validation_checks.pb", FilenameChecks.getDefaultInstance() - ).getFilenameChecksList() + ).filenameChecksList } /** @@ -86,7 +87,7 @@ private fun retrieveFileContentChecks(): List { return loadProto( "file_content_validation_checks.pb", FileContentChecks.getDefaultInstance() - ).getFileContentChecksList() + ).fileContentChecksList } /** @@ -103,80 +104,70 @@ private fun loadProto(textProtoFileName: String, proto: T): T // This cast is type-safe since proto guarantees type consistency from mergeFrom(), // and this method is bounded by the generic type T. @Suppress("UNCHECKED_CAST") - val protoObj: T = - FileInputStream(protoBinaryFile).use { - builder.mergeFrom(it) - }.build() as T - return protoObj + return FileInputStream(protoBinaryFile).use { + builder.mergeFrom(it) + }.build() as T } /** * Checks for a prohibited file naming pattern. * - * @param repoPath the path of the repo + * @param repoRoot the root directory of the repo * @param searchFiles a list of all the files which needs to be checked * @param filenameCheck proto object of FilenameCheck * @return whether the file name pattern is correct or not */ private fun checkProhibitedFileNamePattern( - repoPath: String, + repoRoot: File, searchFiles: List, filenameCheck: FilenameCheck, ): Boolean { - val prohibitedFilenameRegex = filenameCheck.getProhibitedFilenameRegex().toRegex() + val prohibitedFilenameRegex = filenameCheck.prohibitedFilenameRegex.toRegex() val matchedFiles = searchFiles.filter { file -> - return@filter RepositoryFile.retrieveRelativeFilePath(file, repoPath) !in - filenameCheck.getExemptedFileNameList() && - prohibitedFilenameRegex.matches( - RepositoryFile.retrieveRelativeFilePath( - file, - repoPath - ) - ) + val fileRelativePath = file.toRelativeString(repoRoot) + return@filter fileRelativePath !in filenameCheck.exemptedFileNameList && + prohibitedFilenameRegex.matches(fileRelativePath) } - logProhibitedFilenameFailure(filenameCheck.getFailureMessage(), matchedFiles) + logProhibitedFilenameFailure(repoRoot, filenameCheck.failureMessage, matchedFiles) return matchedFiles.isNotEmpty() } /** * Checks for a prohibited file content. * - * @param repoPath the path of the repo + * @param repoRoot the root directory of the repo * @param searchFiles a list of all the files which needs to be checked * @param fileContentCheck proto object of FileContentCheck * @return whether the file content pattern is correct or not */ private fun checkProhibitedContent( - repoPath: String, + repoRoot: File, searchFiles: List, fileContentCheck: FileContentCheck ): Boolean { - val fileNameRegex = fileContentCheck.getFilenameRegex().toRegex() - - val prohibitedContentRegex = - fileContentCheck.getProhibitedContentRegex().toRegex() + val filePathRegex = fileContentCheck.filePathRegex.toRegex() + val prohibitedContentRegex = fileContentCheck.prohibitedContentRegex.toRegex() val matchedFiles = searchFiles.filter { file -> - RepositoryFile.retrieveRelativeFilePath(file, repoPath) !in - fileContentCheck.getExemptedFileNameList() && - fileNameRegex.matches(file.name) && - File(file.toString()) - .bufferedReader() - .lineSequence().foldIndexed(initial = false) { lineIndex, isFailing, lineContent -> + val fileRelativePath = file.toRelativeString(repoRoot) + val isExempted = fileRelativePath in fileContentCheck.exemptedFileNameList + return@filter if (!isExempted && filePathRegex.matches(fileRelativePath)) { + file.useLines { lines -> + lines.foldIndexed(initial = false) { lineIndex, isFailing, lineContent -> val matches = prohibitedContentRegex.containsMatchIn(lineContent) if (matches) { logProhibitedContentFailure( - // Since, the line number starts from 1 and index starts from 0, therefore we have - // to increment index by 1, to denote the line number. - lineIndex + 1, - fileContentCheck.getFailureMessage(), - file.toString() + lineIndex + 1, // Increment by 1 since line numbers begin at 1 rather than 0. + fileContentCheck.failureMessage, + fileRelativePath ) } isFailing || matches } + } + } else false } return matchedFiles.isNotEmpty() } @@ -184,15 +175,19 @@ private fun checkProhibitedContent( /** * Logs the failures for filename pattern violation. * - * @param repoPath the path of the repo to be analyzed + * @param repoRoot the root directory of the repo * @param errorToShow the filename error to be logged * @param matchedFiles a list of all the files which had the filenaming violation */ -private fun logProhibitedFilenameFailure(errorToShow: String, matchedFiles: List) { +private fun logProhibitedFilenameFailure( + repoRoot: File, + errorToShow: String, + matchedFiles: List +) { if (matchedFiles.isNotEmpty()) { println("File name/path violation: $errorToShow") matchedFiles.forEach { - println("- $it") + println("- ${it.toRelativeString(repoRoot)}") } println() } @@ -201,7 +196,7 @@ private fun logProhibitedFilenameFailure(errorToShow: String, matchedFiles: List /** * Logs the failures for file content violation. * - * @param lineNumberthe line number at which the failure occured + * @param lineNumber the line number at which the failure occured * @param errorToShow the failure message to be logged * @param filePath the path of the file relative to the repository which failed the check */ diff --git a/scripts/src/javatests/org/oppia/android/scripts/regex/RegexPatternValidationCheckTest.kt b/scripts/src/javatests/org/oppia/android/scripts/regex/RegexPatternValidationCheckTest.kt index 2cea98cbbf1..e6f8c23870a 100644 --- a/scripts/src/javatests/org/oppia/android/scripts/regex/RegexPatternValidationCheckTest.kt +++ b/scripts/src/javatests/org/oppia/android/scripts/regex/RegexPatternValidationCheckTest.kt @@ -8,6 +8,7 @@ import org.junit.Test import org.junit.rules.TemporaryFolder import org.oppia.android.testing.assertThrows import java.io.ByteArrayOutputStream +import java.io.File import java.io.PrintStream /** Tests for [RegexPatternValidationCheck]. */ @@ -45,6 +46,13 @@ class RegexPatternValidationCheckTest { "Use motion:touchAnchorSide=\"start\", instead, for proper RTL support" private val androidTouchAnchorSideRightErrorMessage = "Use motion:touchAnchorSide=\"end\", instead, for proper RTL support" + private val oppiaCantBeTranslatedErrorMessage = + "Oppia should never used directly in a string (since it shouldn't be translated). Instead," + + " use a parameter & insert the string retrieved from app_name." + private val untranslatableStringsGoInSpecificFileErrorMessage = + "Untranslatable strings should go in untranslated_strings.xml, instead." + private val translatableStringsGoInMainFileErrorMessage = + "All strings outside strings.xml must be marked as not translatable, or moved to strings.xml." private val wikiReferenceNote = "Refer to https://github.com/oppia/oppia-android/wiki/Static-Analysis-Checks" + "#regexpatternvalidation-check for more details on how to fix this." @@ -97,7 +105,7 @@ class RegexPatternValidationCheckTest { assertThat(outContent.toString().trim()).isEqualTo( """ File name/path violation: Activities cannot be placed outside the app or testing module - - ${retrieveTestFilesPath()}/data/src/main/TestActivity.kt + - data/src/main/TestActivity.kt $wikiReferenceNote """.trimIndent() @@ -127,7 +135,7 @@ class RegexPatternValidationCheckTest { assertThat(outContent.toString().trim()) .isEqualTo( """ - ${retrieveTestFilesPath()}/TestFile.kt:1: $supportLibraryUsageErrorMessage + TestFile.kt:1: $supportLibraryUsageErrorMessage $wikiReferenceNote """.trimIndent() ) @@ -147,7 +155,7 @@ class RegexPatternValidationCheckTest { assertThat(outContent.toString().trim()) .isEqualTo( """ - ${retrieveTestFilesPath()}/TestFile.kt:1: $coroutineWorkerUsageErrorMessage + TestFile.kt:1: $coroutineWorkerUsageErrorMessage $wikiReferenceNote """.trimIndent() ) @@ -167,7 +175,7 @@ class RegexPatternValidationCheckTest { assertThat(outContent.toString().trim()) .isEqualTo( """ - ${retrieveTestFilesPath()}/TestFile.kt:1: $settableFutureUsageErrorMessage + TestFile.kt:1: $settableFutureUsageErrorMessage $wikiReferenceNote """.trimIndent() ) @@ -187,7 +195,7 @@ class RegexPatternValidationCheckTest { assertThat(outContent.toString().trim()) .isEqualTo( """ - ${retrieveTestFilesPath()}/test_layout.xml:1: $androidGravityLeftErrorMessage + test_layout.xml:1: $androidGravityLeftErrorMessage $wikiReferenceNote """.trimIndent() ) @@ -207,7 +215,7 @@ class RegexPatternValidationCheckTest { assertThat(outContent.toString().trim()) .isEqualTo( """ - ${retrieveTestFilesPath()}/test_layout.xml:1: $androidGravityRightErrorMessage + test_layout.xml:1: $androidGravityRightErrorMessage $wikiReferenceNote """.trimIndent() ) @@ -227,7 +235,7 @@ class RegexPatternValidationCheckTest { assertThat(outContent.toString().trim()) .isEqualTo( """ - ${retrieveTestFilesPath()}/test_layout.xml:1: $androidLayoutGravityLeftErrorMessage + test_layout.xml:1: $androidLayoutGravityLeftErrorMessage $wikiReferenceNote """.trimIndent() ) @@ -247,7 +255,7 @@ class RegexPatternValidationCheckTest { assertThat(outContent.toString().trim()) .isEqualTo( """ - ${retrieveTestFilesPath()}/test_layout.xml:1: $androidLayoutGravityRightErrorMessage + test_layout.xml:1: $androidLayoutGravityRightErrorMessage $wikiReferenceNote """.trimIndent() ) @@ -267,7 +275,7 @@ class RegexPatternValidationCheckTest { assertThat(outContent.toString().trim()) .isEqualTo( """ - ${retrieveTestFilesPath()}/test_layout.xml:1: $androidGenericStartEndRtlErrorMessage + test_layout.xml:1: $androidGenericStartEndRtlErrorMessage $wikiReferenceNote """.trimIndent() ) @@ -287,7 +295,7 @@ class RegexPatternValidationCheckTest { assertThat(outContent.toString().trim()) .isEqualTo( """ - ${retrieveTestFilesPath()}/test_layout.xml:1: $androidGenericStartEndRtlErrorMessage + test_layout.xml:1: $androidGenericStartEndRtlErrorMessage $wikiReferenceNote """.trimIndent() ) @@ -307,7 +315,7 @@ class RegexPatternValidationCheckTest { assertThat(outContent.toString().trim()) .isEqualTo( """ - ${retrieveTestFilesPath()}/test_layout.xml:1: $androidGenericStartEndRtlErrorMessage + test_layout.xml:1: $androidGenericStartEndRtlErrorMessage $wikiReferenceNote """.trimIndent() ) @@ -327,7 +335,7 @@ class RegexPatternValidationCheckTest { assertThat(outContent.toString().trim()) .isEqualTo( """ - ${retrieveTestFilesPath()}/test_layout.xml:1: $androidGenericStartEndRtlErrorMessage + test_layout.xml:1: $androidGenericStartEndRtlErrorMessage $wikiReferenceNote """.trimIndent() ) @@ -347,7 +355,7 @@ class RegexPatternValidationCheckTest { assertThat(outContent.toString().trim()) .isEqualTo( """ - ${retrieveTestFilesPath()}/test_layout.xml:1: $androidGenericStartEndRtlErrorMessage + test_layout.xml:1: $androidGenericStartEndRtlErrorMessage $wikiReferenceNote """.trimIndent() ) @@ -367,7 +375,7 @@ class RegexPatternValidationCheckTest { assertThat(outContent.toString().trim()) .isEqualTo( """ - ${retrieveTestFilesPath()}/test_layout.xml:1: $androidGenericStartEndRtlErrorMessage + test_layout.xml:1: $androidGenericStartEndRtlErrorMessage $wikiReferenceNote """.trimIndent() ) @@ -387,7 +395,7 @@ class RegexPatternValidationCheckTest { assertThat(outContent.toString().trim()) .isEqualTo( """ - ${retrieveTestFilesPath()}/test_layout.xml:1: $androidGenericStartEndRtlErrorMessage + test_layout.xml:1: $androidGenericStartEndRtlErrorMessage $wikiReferenceNote """.trimIndent() ) @@ -407,7 +415,7 @@ class RegexPatternValidationCheckTest { assertThat(outContent.toString().trim()) .isEqualTo( """ - ${retrieveTestFilesPath()}/test_layout.xml:1: $androidGenericStartEndRtlErrorMessage + test_layout.xml:1: $androidGenericStartEndRtlErrorMessage $wikiReferenceNote """.trimIndent() ) @@ -427,7 +435,7 @@ class RegexPatternValidationCheckTest { assertThat(outContent.toString().trim()) .isEqualTo( """ - ${retrieveTestFilesPath()}/test_layout.xml:1: $androidGenericStartEndRtlErrorMessage + test_layout.xml:1: $androidGenericStartEndRtlErrorMessage $wikiReferenceNote """.trimIndent() ) @@ -447,7 +455,7 @@ class RegexPatternValidationCheckTest { assertThat(outContent.toString().trim()) .isEqualTo( """ - ${retrieveTestFilesPath()}/test_layout.xml:1: $androidGenericStartEndRtlErrorMessage + test_layout.xml:1: $androidGenericStartEndRtlErrorMessage $wikiReferenceNote """.trimIndent() ) @@ -467,7 +475,7 @@ class RegexPatternValidationCheckTest { assertThat(outContent.toString().trim()) .isEqualTo( """ - ${retrieveTestFilesPath()}/test_layout.xml:1: $androidGenericStartEndRtlErrorMessage + test_layout.xml:1: $androidGenericStartEndRtlErrorMessage $wikiReferenceNote """.trimIndent() ) @@ -487,7 +495,7 @@ class RegexPatternValidationCheckTest { assertThat(outContent.toString().trim()) .isEqualTo( """ - ${retrieveTestFilesPath()}/test_layout.xml:1: $androidGenericStartEndRtlErrorMessage + test_layout.xml:1: $androidGenericStartEndRtlErrorMessage $wikiReferenceNote """.trimIndent() ) @@ -507,7 +515,7 @@ class RegexPatternValidationCheckTest { assertThat(outContent.toString().trim()) .isEqualTo( """ - ${retrieveTestFilesPath()}/test_layout.xml:1: $androidGenericStartEndRtlErrorMessage + test_layout.xml:1: $androidGenericStartEndRtlErrorMessage $wikiReferenceNote """.trimIndent() ) @@ -527,7 +535,7 @@ class RegexPatternValidationCheckTest { assertThat(outContent.toString().trim()) .isEqualTo( """ - ${retrieveTestFilesPath()}/test_layout.xml:1: $androidGenericStartEndRtlErrorMessage + test_layout.xml:1: $androidGenericStartEndRtlErrorMessage $wikiReferenceNote """.trimIndent() ) @@ -547,7 +555,7 @@ class RegexPatternValidationCheckTest { assertThat(outContent.toString().trim()) .isEqualTo( """ - ${retrieveTestFilesPath()}/test_layout.xml:1: $androidGenericStartEndRtlErrorMessage + test_layout.xml:1: $androidGenericStartEndRtlErrorMessage $wikiReferenceNote """.trimIndent() ) @@ -567,7 +575,7 @@ class RegexPatternValidationCheckTest { assertThat(outContent.toString().trim()) .isEqualTo( """ - ${retrieveTestFilesPath()}/test_layout.xml:1: $androidGenericStartEndRtlErrorMessage + test_layout.xml:1: $androidGenericStartEndRtlErrorMessage $wikiReferenceNote """.trimIndent() ) @@ -587,7 +595,7 @@ class RegexPatternValidationCheckTest { assertThat(outContent.toString().trim()) .isEqualTo( """ - ${retrieveTestFilesPath()}/test_layout.xml:1: $androidGenericStartEndRtlErrorMessage + test_layout.xml:1: $androidGenericStartEndRtlErrorMessage $wikiReferenceNote """.trimIndent() ) @@ -607,7 +615,7 @@ class RegexPatternValidationCheckTest { assertThat(outContent.toString().trim()) .isEqualTo( """ - ${retrieveTestFilesPath()}/test_layout.xml:1: $androidGenericStartEndRtlErrorMessage + test_layout.xml:1: $androidGenericStartEndRtlErrorMessage $wikiReferenceNote """.trimIndent() ) @@ -627,7 +635,7 @@ class RegexPatternValidationCheckTest { assertThat(outContent.toString().trim()) .isEqualTo( """ - ${retrieveTestFilesPath()}/test_layout.xml:1: $androidBarrierDirectionLeftErrorMessage + test_layout.xml:1: $androidBarrierDirectionLeftErrorMessage $wikiReferenceNote """.trimIndent() ) @@ -647,7 +655,7 @@ class RegexPatternValidationCheckTest { assertThat(outContent.toString().trim()) .isEqualTo( """ - ${retrieveTestFilesPath()}/test_layout.xml:1: $androidBarrierDirectionRightErrorMessage + test_layout.xml:1: $androidBarrierDirectionRightErrorMessage $wikiReferenceNote """.trimIndent() ) @@ -667,7 +675,7 @@ class RegexPatternValidationCheckTest { assertThat(outContent.toString().trim()) .isEqualTo( """ - ${retrieveTestFilesPath()}/test_layout.xml:1: $androidDragDirectionLeftErrorMessage + test_layout.xml:1: $androidDragDirectionLeftErrorMessage $wikiReferenceNote """.trimIndent() ) @@ -687,7 +695,7 @@ class RegexPatternValidationCheckTest { assertThat(outContent.toString().trim()) .isEqualTo( """ - ${retrieveTestFilesPath()}/test_layout.xml:1: $androidDragDirectionRightErrorMessage + test_layout.xml:1: $androidDragDirectionRightErrorMessage $wikiReferenceNote """.trimIndent() ) @@ -707,7 +715,7 @@ class RegexPatternValidationCheckTest { assertThat(outContent.toString().trim()) .isEqualTo( """ - ${retrieveTestFilesPath()}/test_layout.xml:1: $androidTouchAnchorSideLeftErrorMessage + test_layout.xml:1: $androidTouchAnchorSideLeftErrorMessage $wikiReferenceNote """.trimIndent() ) @@ -727,12 +735,99 @@ class RegexPatternValidationCheckTest { assertThat(outContent.toString().trim()) .isEqualTo( """ - ${retrieveTestFilesPath()}/test_layout.xml:1: $androidTouchAnchorSideRightErrorMessage + test_layout.xml:1: $androidTouchAnchorSideRightErrorMessage $wikiReferenceNote """.trimIndent() ) } + @Test + fun testFileContent_oppiaInString_inPrimaryStringsFile_fileContentIsNotCorrect() { + val prohibitedContent = "String with Oppia in it" + tempFolder.newFolder("testfiles", "app", "src", "main", "res", "values") + val stringFilePath = "app/src/main/res/values/strings.xml" + tempFolder.newFile("testfiles/$stringFilePath").writeText(prohibitedContent) + + val exception = assertThrows(Exception::class) { + runScript() + } + + assertThat(exception).hasMessageThat().contains(REGEX_CHECK_FAILED_OUTPUT_INDICATOR) + assertThat(outContent.toString().trim()) + .isEqualTo( + """ + $stringFilePath:1: $oppiaCantBeTranslatedErrorMessage + $wikiReferenceNote + """.trimIndent() + ) + } + + @Test + fun testFileContent_untranslatableString_inPrimaryStringsFile_fileContentIsNotCorrect() { + val prohibitedContent = "Something" + tempFolder.newFolder("testfiles", "app", "src", "main", "res", "values") + val stringFilePath = "app/src/main/res/values/strings.xml" + tempFolder.newFile("testfiles/$stringFilePath").writeText(prohibitedContent) + + val exception = assertThrows(Exception::class) { + runScript() + } + + assertThat(exception).hasMessageThat().contains(REGEX_CHECK_FAILED_OUTPUT_INDICATOR) + assertThat(outContent.toString().trim()) + .isEqualTo( + """ + $stringFilePath:1: $untranslatableStringsGoInSpecificFileErrorMessage + $wikiReferenceNote + """.trimIndent() + ) + } + + @Test + fun testFileContent_untranslatableString_inUntranslatedStringsFile_fileContentIsCorrect() { + val prohibitedContent = "Something" + tempFolder.newFolder("testfiles", "app", "src", "main", "res", "values") + val stringFilePath = "app/src/main/res/values/untranslated_strings.xml" + tempFolder.newFile("testfiles/$stringFilePath").writeText(prohibitedContent) + + runScript() + + assertThat(outContent.toString().trim()).isEqualTo(REGEX_CHECK_PASSED_OUTPUT_INDICATOR) + } + + @Test + fun testFileContent_translatableString_outsidePrimaryStringsFile_fileContentIsNotCorrect() { + val prohibitedContent = "Translatable" + tempFolder.newFolder("testfiles", "app", "src", "main", "res", "values") + val stringFilePath = "app/src/main/res/values/untranslated_strings.xml" + tempFolder.newFile("testfiles/$stringFilePath").writeText(prohibitedContent) + + val exception = assertThrows(Exception::class) { + runScript() + } + + assertThat(exception).hasMessageThat().contains(REGEX_CHECK_FAILED_OUTPUT_INDICATOR) + assertThat(outContent.toString().trim()) + .isEqualTo( + """ + $stringFilePath:1: $translatableStringsGoInMainFileErrorMessage + $wikiReferenceNote + """.trimIndent() + ) + } + + @Test + fun testFileContent_untranslatableString_outsidePrimaryStringsFile_fileContentIsCorrect() { + val prohibitedContent = "Translatable" + tempFolder.newFolder("testfiles", "app", "src", "main", "res", "values") + val stringFilePath = "app/src/main/res/values/strings.xml" + tempFolder.newFile("testfiles/$stringFilePath").writeText(prohibitedContent) + + runScript() + + assertThat(outContent.toString().trim()).isEqualTo(REGEX_CHECK_PASSED_OUTPUT_INDICATOR) + } + @Test fun testFilenameAndContent_useProhibitedFileName_useProhibitedFileContent_multipleFailures() { tempFolder.newFolder("testfiles", "data", "src", "main") @@ -748,21 +843,16 @@ class RegexPatternValidationCheckTest { assertThat(outContent.toString().trim()).isEqualTo( """ File name/path violation: Activities cannot be placed outside the app or testing module - - ${retrieveTestFilesPath()}/data/src/main/TestActivity.kt - - ${retrieveTestFilesPath()}/data/src/main/TestActivity.kt:1: AndroidX should be used instead of the support library + - data/src/main/TestActivity.kt + + data/src/main/TestActivity.kt:1: AndroidX should be used instead of the support library $wikiReferenceNote """.trimIndent() ) } - /** Retrieves the absolute path of testfiles directory. */ - private fun retrieveTestFilesPath(): String { - return "${tempFolder.root}/testfiles" - } - /** Runs the regex_pattern_validation_check. */ private fun runScript() { - main(retrieveTestFilesPath()) + main(File(tempFolder.root, "testfiles").absolutePath) } } diff --git a/utility/BUILD.bazel b/utility/BUILD.bazel index 2e534732f9a..68333f33a93 100644 --- a/utility/BUILD.bazel +++ b/utility/BUILD.bazel @@ -31,7 +31,6 @@ android_library( custom_package = "org.oppia.android.util", manifest = "src/main/AndroidManifest.xml", resource_files = glob(["src/main/res/**/*.xml"]), - visibility = ["//visibility:public"], ) # Library for general-purpose utilities. @@ -59,7 +58,6 @@ kt_android_library( "//third_party:glide_compiler", "//third_party:org_jetbrains_kotlinx_kotlinx-coroutines-core", "//utility/src/main/java/org/oppia/android/util/caching:assets", - "//utility/src/main/java/org/oppia/android/util/datetime:date_time_util", "//utility/src/main/java/org/oppia/android/util/gcsresource:annotations", "//utility/src/main/java/org/oppia/android/util/gcsresource:prod_module", "//utility/src/main/java/org/oppia/android/util/logging:console_logger", diff --git a/utility/src/main/java/org/oppia/android/util/datetime/BUILD.bazel b/utility/src/main/java/org/oppia/android/util/datetime/BUILD.bazel deleted file mode 100644 index d08132561c2..00000000000 --- a/utility/src/main/java/org/oppia/android/util/datetime/BUILD.bazel +++ /dev/null @@ -1,18 +0,0 @@ -""" -General purposes utilities to manage Date and Time. -""" - -load("@io_bazel_rules_kotlin//kotlin:kotlin.bzl", "kt_android_library") - -kt_android_library( - name = "date_time_util", - srcs = [ - "DateTimeUtil.kt", - ], - visibility = ["//:oppia_api_visibility"], - deps = [ - "//third_party:javax_inject_javax_inject", - "//utility:resources", - "//utility/src/main/java/org/oppia/android/util/system:oppia_clock", - ], -) diff --git a/utility/src/main/java/org/oppia/android/util/datetime/DateTimeUtil.kt b/utility/src/main/java/org/oppia/android/util/datetime/DateTimeUtil.kt deleted file mode 100644 index a33df2ab20b..00000000000 --- a/utility/src/main/java/org/oppia/android/util/datetime/DateTimeUtil.kt +++ /dev/null @@ -1,26 +0,0 @@ -package org.oppia.android.util.datetime - -import android.content.Context -import org.oppia.android.util.R -import org.oppia.android.util.system.OppiaClock -import java.util.Calendar -import javax.inject.Inject -import javax.inject.Singleton - -/** Utility to manage date and time. */ -@Singleton -class DateTimeUtil @Inject constructor( - private val context: Context, - private val oppiaClock: OppiaClock -) { - - fun getGreetingMessage(): String { - val calender = oppiaClock.getCurrentCalendar() - return when (calender.get(Calendar.HOUR_OF_DAY)) { - in 4..11 -> context.getString(R.string.good_morning) - in 12..16 -> context.getString(R.string.good_afternoon) - in 17 downTo 3 -> context.getString(R.string.good_evening) - else -> context.getString(R.string.good_evening) - } - } -} diff --git a/utility/src/main/res/values/strings.xml b/utility/src/main/res/values/strings.xml deleted file mode 100644 index b677409ff1d..00000000000 --- a/utility/src/main/res/values/strings.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - Good morning, - Good afternoon, - Good evening, - diff --git a/utility/src/test/java/org/oppia/android/util/datetime/DateTimeUtilTest.kt b/utility/src/test/java/org/oppia/android/util/datetime/DateTimeUtilTest.kt deleted file mode 100644 index d49843c4512..00000000000 --- a/utility/src/test/java/org/oppia/android/util/datetime/DateTimeUtilTest.kt +++ /dev/null @@ -1,118 +0,0 @@ -package org.oppia.android.util.datetime - -import android.app.Application -import android.content.Context -import androidx.test.core.app.ApplicationProvider -import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.google.common.truth.Truth.assertThat -import dagger.BindsInstance -import dagger.Component -import dagger.Module -import dagger.Provides -import org.junit.Before -import org.junit.Test -import org.junit.runner.RunWith -import org.oppia.android.testing.robolectric.RobolectricModule -import org.oppia.android.testing.time.FakeOppiaClock -import org.oppia.android.testing.time.FakeOppiaClockModule -import org.oppia.android.util.logging.EnableConsoleLog -import org.oppia.android.util.logging.EnableFileLog -import org.oppia.android.util.logging.GlobalLogLevel -import org.oppia.android.util.logging.LogLevel -import org.robolectric.annotation.Config -import org.robolectric.annotation.LooperMode -import javax.inject.Inject -import javax.inject.Singleton - -// Time: Tue Apr 23 2019 23:22:00 -private const val EVENING_TIMESTAMP = 1556061720000 -// Time: Wed Apr 24 2019 08:22:00 -private const val MORNING_TIMESTAMP = 1556094120000 -// Time: Tue Apr 23 2019 14:22:00 -private const val AFTERNOON_TIMESTAMP = 1556029320000 - -/** Tests for [DateTimeUtil]. */ -@RunWith(AndroidJUnit4::class) -@LooperMode(LooperMode.Mode.PAUSED) -@Config(manifest = Config.NONE) -class DateTimeUtilTest { - - @Inject lateinit var dateTimeUtil: DateTimeUtil - @Inject lateinit var context: Context - @Inject lateinit var fakeOppiaClock: FakeOppiaClock - - @Before - fun setUp() { - setUpTestApplicationComponent() - fakeOppiaClock.setFakeTimeMode(FakeOppiaClock.FakeTimeMode.MODE_FIXED_FAKE_TIME) - } - - private fun setUpTestApplicationComponent() { - DaggerDateTimeUtilTest_TestApplicationComponent.builder() - .setApplication(ApplicationProvider.getApplicationContext()) - .build() - .inject(this) - } - - @Test - fun testGreetingMessageBasedOnTime_goodEveningMessageSucceeded() { - fakeOppiaClock.setCurrentTimeToSameDateTime(EVENING_TIMESTAMP) - assertThat(dateTimeUtil.getGreetingMessage()).isEqualTo("Good evening,") - } - - @Test - fun testGreetingMessageBasedOnTime_goodMorningMessageSucceeded() { - fakeOppiaClock.setCurrentTimeToSameDateTime(MORNING_TIMESTAMP) - assertThat(dateTimeUtil.getGreetingMessage()).isEqualTo("Good morning,") - } - - @Test - fun testGreetingMessageBasedOnTime_goodAfternoonMessageSucceeded() { - fakeOppiaClock.setCurrentTimeToSameDateTime(AFTERNOON_TIMESTAMP) - assertThat(dateTimeUtil.getGreetingMessage()).isEqualTo("Good afternoon,") - } - - // TODO(#89): Move this to a common test application component. - @Module - class TestModule { - @Provides - @Singleton - fun provideContext(application: Application): Context { - return application - } - - // TODO(#59): Either isolate these to their own shared test module, or use the real logging - // module in tests to avoid needing to specify these settings for tests. - @EnableConsoleLog - @Provides - fun provideEnableConsoleLog(): Boolean = true - - @EnableFileLog - @Provides - fun provideEnableFileLog(): Boolean = false - - @GlobalLogLevel - @Provides - fun provideGlobalLogLevel(): LogLevel = LogLevel.VERBOSE - } - - // TODO(#89): Move this to a common test application component. - @Singleton - @Component( - modules = [ - TestModule::class, - RobolectricModule::class, FakeOppiaClockModule::class - ] - ) - interface TestApplicationComponent { - @Component.Builder - interface Builder { - @BindsInstance - fun setApplication(application: Application): Builder - - fun build(): TestApplicationComponent - } - - fun inject(dateTimeUtilTest: DateTimeUtilTest) - } -}