diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index a9aaf5ce694..0c77e9bcc31 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -150,9 +150,6 @@ config/kitkat_main_dex_class_list.txt @BenHenning # Global domain module code ownership. /domain/**/*.kt @BenHenning -# Domain test resources. -/domain/src/test/res/values/strings.xml @BenHenning - # Questions support. /domain/src/*/java/org/oppia/android/domain/question/ @BenHenning @@ -181,6 +178,9 @@ config/kitkat_main_dex_class_list.txt @BenHenning # Global utility module code ownership. /utility/**/*.kt @BenHenning +# Utility test resources. +/utility/src/test/res/values/strings.xml @BenHenning + # Accessibility utilities. /utility/src/*/java/org/oppia/android/util/accessibility/ @rt4914 diff --git a/app/BUILD.bazel b/app/BUILD.bazel index 6f57ec2d58f..749d0b4e908 100644 --- a/app/BUILD.bazel +++ b/app/BUILD.bazel @@ -110,6 +110,7 @@ LISTENERS = [ "src/main/java/org/oppia/android/app/help/LoadFaqListFragmentListener.kt", "src/main/java/org/oppia/android/app/help/LoadLicenseListFragmentListener.kt", "src/main/java/org/oppia/android/app/help/LoadLicenseTextViewerFragmentListener.kt", + "src/main/java/org/oppia/android/app/help/LoadPoliciesFragmentListener.kt", "src/main/java/org/oppia/android/app/help/LoadThirdPartyDependencyListFragmentListener.kt", "src/main/java/org/oppia/android/app/help/RouteToFAQListListener.kt", "src/main/java/org/oppia/android/app/help/RouteToThirdPartyDependencyListListener.kt", @@ -143,6 +144,7 @@ LISTENERS = [ "src/main/java/org/oppia/android/app/player/state/listener/RouteToHintsAndSolutionListener.kt", "src/main/java/org/oppia/android/app/player/state/listener/StateKeyboardButtonListener.kt", "src/main/java/org/oppia/android/app/player/state/listener/SubmitNavigationButtonListener.kt", + "src/main/java/org/oppia/android/app/policies/RouteToPoliciesListener.kt", "src/main/java/org/oppia/android/app/profile/RouteToAdminPinListener.kt", "src/main/java/org/oppia/android/app/profileprogress/ProfilePictureClickListener.kt", "src/main/java/org/oppia/android/app/profileprogress/RouteToCompletedStoryListListener.kt", @@ -603,6 +605,7 @@ kt_android_library( deps = [ ":dagger", "//domain/src/main/java/org/oppia/android/domain/audio:cellular_audio_dialog_controller", + "//model/src/main/proto:arguments_java_proto_lite", "//model/src/main/proto:question_java_proto_lite", "//model/src/main/proto:topic_java_proto_lite", "//third_party:androidx_recyclerview_recyclerview", @@ -890,9 +893,10 @@ TEST_DEPS = [ "//utility/src/main/java/org/oppia/android/util/logging/firebase:debug_module", "//utility/src/main/java/org/oppia/android/util/math:math_expression_parser", "//utility/src/main/java/org/oppia/android/util/networking:debug_module", - "//utility/src/main/java/org/oppia/android/util/parser/html:custom_bullet_span", "//utility/src/main/java/org/oppia/android/util/parser/html:html_parser", "//utility/src/main/java/org/oppia/android/util/parser/html:html_parser_entity_type_module", + "//utility/src/main/java/org/oppia/android/util/parser/html:list_item_leading_margin_span", + "//utility/src/main/java/org/oppia/android/util/parser/html:policy_type", "//utility/src/main/java/org/oppia/android/util/parser/image:glide_image_loader", "//utility/src/main/java/org/oppia/android/util/parser/image:glide_image_loader_module", "//utility/src/main/java/org/oppia/android/util/parser/image:image_parsing_module", diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 89dc7903f41..186e5830ae5 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -194,10 +194,12 @@ + + @@ -276,6 +278,9 @@ + "), parseResultTextView) + htmlParser.parseOppiaHtml( + newText.replace("\n", "
"), + parseResultTextView + ) } else newText } diff --git a/app/src/main/java/org/oppia/android/app/fragment/FragmentComponentImpl.kt b/app/src/main/java/org/oppia/android/app/fragment/FragmentComponentImpl.kt index 5ec6f40e761..c5c4d2289ac 100644 --- a/app/src/main/java/org/oppia/android/app/fragment/FragmentComponentImpl.kt +++ b/app/src/main/java/org/oppia/android/app/fragment/FragmentComponentImpl.kt @@ -47,6 +47,7 @@ import org.oppia.android.app.player.state.itemviewmodel.InteractionViewModelModu import org.oppia.android.app.player.stopplaying.ProgressDatabaseFullDialogFragment import org.oppia.android.app.player.stopplaying.StopExplorationDialogFragment import org.oppia.android.app.player.stopplaying.UnsavedExplorationDialogFragment +import org.oppia.android.app.policies.PoliciesFragment import org.oppia.android.app.profile.AdminSettingsDialogFragment import org.oppia.android.app.profile.ProfileChooserFragment import org.oppia.android.app.profile.ResetPinDialogFragment @@ -135,6 +136,7 @@ interface FragmentComponentImpl : FragmentComponent, ViewComponentBuilderInjecto fun inject(onboardingFragment: OnboardingFragment) fun inject(ongoingTopicListFragment: OngoingTopicListFragment) fun inject(optionFragment: OptionsFragment) + fun inject(policiesFragment: PoliciesFragment) fun inject(profileAndDeviceIdFragment: ProfileAndDeviceIdFragment) fun inject(profileChooserFragment: ProfileChooserFragment) fun inject(profileEditDeletionDialogFragment: ProfileEditDeletionDialogFragment) diff --git a/app/src/main/java/org/oppia/android/app/help/HelpActivity.kt b/app/src/main/java/org/oppia/android/app/help/HelpActivity.kt index 25d53ed8946..789f4910fc9 100644 --- a/app/src/main/java/org/oppia/android/app/help/HelpActivity.kt +++ b/app/src/main/java/org/oppia/android/app/help/HelpActivity.kt @@ -11,7 +11,12 @@ import org.oppia.android.app.help.faq.FAQListActivity import org.oppia.android.app.help.faq.RouteToFAQSingleListener import org.oppia.android.app.help.faq.faqsingle.FAQSingleActivity import org.oppia.android.app.help.thirdparty.ThirdPartyDependencyListActivity +import org.oppia.android.app.model.PoliciesActivityParams +import org.oppia.android.app.model.PolicyPage +import org.oppia.android.app.policies.PoliciesActivity +import org.oppia.android.app.policies.RouteToPoliciesListener import org.oppia.android.app.translation.AppLanguageResourceHandler +import org.oppia.android.util.extensions.getProto import org.oppia.android.util.extensions.getStringFromBundle import javax.inject.Inject @@ -21,17 +26,21 @@ const val THIRD_PARTY_DEPENDENCY_INDEX_SAVED_KEY = "HelpActivity.third_party_dependency_index" const val LICENSE_INDEX_SAVED_KEY = "HelpActivity.license_index" const val FAQ_LIST_FRAGMENT_TAG = "FAQListFragment.tag" +const val POLICIES_ARGUMENT_PROTO = "PoliciesActivity.policy_page" +const val POLICIES_FRAGMENT_TAG = "PoliciesFragment.tag" const val THIRD_PARTY_DEPENDENCY_LIST_FRAGMENT_TAG = "ThirdPartyDependencyListFragment.tag" const val LICENSE_LIST_FRAGMENT_TAG = "LicenseListFragment.tag" const val LICENSE_TEXT_FRAGMENT_TAG = "LicenseTextFragment.tag" -/** The help page activity for FAQs and third-party dependencies. */ +/** The help page activity for FAQs, third-party dependencies and policies page. */ class HelpActivity : InjectableAppCompatActivity(), RouteToFAQListListener, RouteToFAQSingleListener, + RouteToPoliciesListener, RouteToThirdPartyDependencyListListener, LoadFaqListFragmentListener, + LoadPoliciesFragmentListener, LoadThirdPartyDependencyListFragmentListener, LoadLicenseListFragmentListener, LoadLicenseTextViewerFragmentListener { @@ -59,12 +68,17 @@ class HelpActivity : val selectedLicenseIndex = savedInstanceState?.getInt(LICENSE_INDEX_SAVED_KEY) ?: 0 selectedHelpOptionsTitle = savedInstanceState?.getStringFromBundle(HELP_OPTIONS_TITLE_SAVED_KEY) ?: resourceHandler.getStringInLocale(R.string.faq_activity_title) + val policiesActivityParams = savedInstanceState?.getProto( + POLICIES_ARGUMENT_PROTO, + PoliciesActivityParams.getDefaultInstance() + ) helpActivityPresenter.handleOnCreate( selectedHelpOptionsTitle, isFromNavigationDrawer, selectedFragment, selectedDependencyIndex, - selectedLicenseIndex + selectedLicenseIndex, + policiesActivityParams ) title = resourceHandler.getStringInLocale(R.string.menu_help) } @@ -121,4 +135,12 @@ class HelpActivity : override fun onRouteToFAQSingle(question: String, answer: String) { startActivity(FAQSingleActivity.createFAQSingleActivityIntent(this, question, answer)) } + + override fun onRouteToPolicies(policyPage: PolicyPage) { + startActivity(PoliciesActivity.createPoliciesActivityIntent(this, policyPage)) + } + + override fun loadPoliciesFragment(policyPage: PolicyPage) { + helpActivityPresenter.handleLoadPoliciesFragment(policyPage) + } } diff --git a/app/src/main/java/org/oppia/android/app/help/HelpActivityPresenter.kt b/app/src/main/java/org/oppia/android/app/help/HelpActivityPresenter.kt index 7c2debc1189..29f2aa8123b 100644 --- a/app/src/main/java/org/oppia/android/app/help/HelpActivityPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/help/HelpActivityPresenter.kt @@ -16,7 +16,12 @@ import org.oppia.android.app.help.faq.FAQListFragment import org.oppia.android.app.help.thirdparty.LicenseListFragment import org.oppia.android.app.help.thirdparty.LicenseTextViewerFragment import org.oppia.android.app.help.thirdparty.ThirdPartyDependencyListFragment +import org.oppia.android.app.model.PoliciesActivityParams +import org.oppia.android.app.model.PoliciesFragmentArguments +import org.oppia.android.app.model.PolicyPage +import org.oppia.android.app.policies.PoliciesFragment import org.oppia.android.app.translation.AppLanguageResourceHandler +import org.oppia.android.util.extensions.putProto import javax.inject.Inject /** The presenter for [HelpActivity]. */ @@ -32,18 +37,24 @@ class HelpActivityPresenter @Inject constructor( private lateinit var selectedHelpOptionTitle: String private var selectedDependencyIndex: Int? = null private var selectedLicenseIndex: Int? = null + private var internalPolicyPage: PolicyPage = PolicyPage.POLICY_PAGE_UNSPECIFIED fun handleOnCreate( helpOptionsTitle: String, isFromNavigationDrawer: Boolean, selectedFragment: String, dependencyIndex: Int, - licenseIndex: Int + licenseIndex: Int, + policiesActivityParams: PoliciesActivityParams? ) { selectedFragmentTag = selectedFragment selectedDependencyIndex = dependencyIndex selectedLicenseIndex = licenseIndex selectedHelpOptionTitle = helpOptionsTitle + if (policiesActivityParams != null) { + internalPolicyPage = policiesActivityParams.policyPage + } + if (isFromNavigationDrawer) { activity.setContentView(R.layout.help_activity) setUpToolbar() @@ -141,6 +152,12 @@ class HelpActivityPresenter @Inject constructor( outState.putString(SELECTED_FRAGMENT_SAVED_KEY, selectedFragmentTag) selectedDependencyIndex?.let { outState.putInt(THIRD_PARTY_DEPENDENCY_INDEX_SAVED_KEY, it) } selectedLicenseIndex?.let { outState.putInt(LICENSE_INDEX_SAVED_KEY, it) } + val policiesActivityParams = + PoliciesActivityParams + .newBuilder() + .setPolicyPage(internalPolicyPage) + .build() + outState.putProto(POLICIES_ARGUMENT_PROTO, policiesActivityParams) } private fun setUpToolbar() { @@ -194,6 +211,7 @@ class HelpActivityPresenter @Inject constructor( ) { when (selectedFragment) { FAQ_LIST_FRAGMENT_TAG -> handleLoadFAQListFragment() + POLICIES_FRAGMENT_TAG -> handleLoadPoliciesFragment(internalPolicyPage) THIRD_PARTY_DEPENDENCY_LIST_FRAGMENT_TAG -> handleLoadThirdPartyDependencyListFragment() LICENSE_LIST_FRAGMENT_TAG -> handleLoadLicenseListFragment(dependencyIndex) LICENSE_TEXT_FRAGMENT_TAG -> handleLoadLicenseTextViewerFragment( @@ -296,4 +314,38 @@ class HelpActivityPresenter @Inject constructor( private fun getMultipaneOptionsFragment(): Fragment? { return activity.supportFragmentManager.findFragmentById(R.id.multipane_options_container) } + + fun handleLoadPoliciesFragment(policyPage: PolicyPage) { + internalPolicyPage = policyPage + selectPoliciesFragment(policyPage) + + val policiesFragmentArguments = + PoliciesFragmentArguments + .newBuilder() + .setPolicyPage(policyPage) + .build() + val previousFragment = getMultipaneOptionsFragment() + if (previousFragment != null) { + activity.supportFragmentManager.beginTransaction().remove(previousFragment).commitNow() + } + activity.supportFragmentManager.beginTransaction().add( + R.id.multipane_options_container, + PoliciesFragment.newInstance(policiesFragmentArguments) + ).commitNow() + } + + private fun selectPoliciesFragment(policyPage: PolicyPage) { + when (policyPage) { + PolicyPage.PRIVACY_POLICY -> setMultipaneContainerTitle( + resourceHandler.getStringInLocale(R.string.privacy_policy_title) + ) + PolicyPage.TERMS_OF_SERVICE -> setMultipaneContainerTitle( + resourceHandler.getStringInLocale(R.string.terms_of_service_title) + ) + else -> { } + } + setMultipaneBackButtonVisibility(View.GONE) + selectedFragmentTag = POLICIES_FRAGMENT_TAG + selectedHelpOptionTitle = getMultipaneContainerTitle() + } } diff --git a/app/src/main/java/org/oppia/android/app/help/HelpItemViewModel.kt b/app/src/main/java/org/oppia/android/app/help/HelpItemViewModel.kt index 71b84a96068..a2472a4e3e1 100644 --- a/app/src/main/java/org/oppia/android/app/help/HelpItemViewModel.kt +++ b/app/src/main/java/org/oppia/android/app/help/HelpItemViewModel.kt @@ -2,6 +2,8 @@ package org.oppia.android.app.help import androidx.appcompat.app.AppCompatActivity import org.oppia.android.R +import org.oppia.android.app.model.PolicyPage +import org.oppia.android.app.policies.RouteToPoliciesListener import org.oppia.android.app.translation.AppLanguageResourceHandler import org.oppia.android.app.viewmodel.ObservableViewModel @@ -34,6 +36,23 @@ class HelpItemViewModel( routeToThirdPartyDependencyListListener.onRouteToThirdPartyDependencyList() } } + resourceHandler.getStringInLocale(R.string.privacy_policy_title) -> { + loadPolicyPage(PolicyPage.PRIVACY_POLICY) + } + resourceHandler.getStringInLocale(R.string.terms_of_service_title) -> { + loadPolicyPage(PolicyPage.TERMS_OF_SERVICE) + } + } + } + + private fun loadPolicyPage(policyPage: PolicyPage) { + if (isMultipane) { + val loadPoliciesFragmentListener = activity as + LoadPoliciesFragmentListener + loadPoliciesFragmentListener.loadPoliciesFragment(policyPage) + } else { + val routeToPoliciesListener = activity as RouteToPoliciesListener + routeToPoliciesListener.onRouteToPolicies(policyPage) } } } diff --git a/app/src/main/java/org/oppia/android/app/help/HelpItems.kt b/app/src/main/java/org/oppia/android/app/help/HelpItems.kt index 3ad3827b5c3..a8f376f90ab 100644 --- a/app/src/main/java/org/oppia/android/app/help/HelpItems.kt +++ b/app/src/main/java/org/oppia/android/app/help/HelpItems.kt @@ -3,5 +3,9 @@ package org.oppia.android.app.help /** Enum class containing the items for the Recycler view of [HelpActivity]. */ enum class HelpItems { FAQ, - THIRD_PARTY; + THIRD_PARTY, + /** Corresponds to the Privacy Policy page. */ + PRIVACY_POLICY, + /** Corresponds to the Terms of Service page. */ + TERMS_OF_SERVICE } diff --git a/app/src/main/java/org/oppia/android/app/help/HelpListViewModel.kt b/app/src/main/java/org/oppia/android/app/help/HelpListViewModel.kt index 4612cee6e1b..3684a6b4e29 100644 --- a/app/src/main/java/org/oppia/android/app/help/HelpListViewModel.kt +++ b/app/src/main/java/org/oppia/android/app/help/HelpListViewModel.kt @@ -18,22 +18,24 @@ class HelpListViewModel @Inject constructor( private fun getRecyclerViewItemList(): ArrayList { for (item in HelpItems.values()) { - val category: String - val helpItemViewModel: HelpItemViewModel - when (item) { - HelpItems.FAQ -> { - category = resourceHandler.getStringInLocale(R.string.frequently_asked_questions_FAQ) - helpItemViewModel = - HelpItemViewModel(activity, category, isMultipane.get()!!, resourceHandler) - } - HelpItems.THIRD_PARTY -> { - category = - resourceHandler.getStringInLocale(R.string.third_party_dependency_list_activity_title) - helpItemViewModel = - HelpItemViewModel(activity, category, isMultipane.get()!!, resourceHandler) - } + val category = when (item) { + HelpItems.FAQ -> resourceHandler.getStringInLocale(R.string.frequently_asked_questions_FAQ) + HelpItems.THIRD_PARTY -> resourceHandler.getStringInLocale( + R.string.third_party_dependency_list_activity_title + ) + HelpItems.PRIVACY_POLICY -> resourceHandler.getStringInLocale( + R.string.privacy_policy_title + ) + HelpItems.TERMS_OF_SERVICE -> resourceHandler.getStringInLocale( + R.string.terms_of_service_title + ) } - arrayList.add(helpItemViewModel) + arrayList += HelpItemViewModel( + activity, + category, + isMultipane.get() ?: false, + resourceHandler + ) } return arrayList } diff --git a/app/src/main/java/org/oppia/android/app/help/LoadPoliciesFragmentListener.kt b/app/src/main/java/org/oppia/android/app/help/LoadPoliciesFragmentListener.kt new file mode 100644 index 00000000000..3062f60138d --- /dev/null +++ b/app/src/main/java/org/oppia/android/app/help/LoadPoliciesFragmentListener.kt @@ -0,0 +1,12 @@ +package org.oppia.android.app.help + +import org.oppia.android.app.model.PolicyPage + +/** + * Listener for when a selection should result in displaying a policy page (e.g. the Privacy Policy) + * on tablet. + */ +interface LoadPoliciesFragmentListener { + /** Called when the user wants to view an app policy. */ + fun loadPoliciesFragment(policyPage: PolicyPage) +} diff --git a/app/src/main/java/org/oppia/android/app/help/faq/faqsingle/FAQSingleActivityPresenter.kt b/app/src/main/java/org/oppia/android/app/help/faq/faqsingle/FAQSingleActivityPresenter.kt index 8663a9143b1..4802913476e 100644 --- a/app/src/main/java/org/oppia/android/app/help/faq/faqsingle/FAQSingleActivityPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/help/faq/faqsingle/FAQSingleActivityPresenter.kt @@ -51,7 +51,8 @@ class FAQSingleActivityPresenter @Inject constructor( resourceBucketName, entityType = "faq", entityId = "oppia", - imageCenterAlign = false + imageCenterAlign = false, + displayLocale = resourceHandler.getDisplayLocale() ).parseOppiaHtml( answer, answerTextView diff --git a/app/src/main/java/org/oppia/android/app/hintsandsolution/HintsAndSolutionDialogFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/hintsandsolution/HintsAndSolutionDialogFragmentPresenter.kt index c7c56b44972..32410bd2a0c 100644 --- a/app/src/main/java/org/oppia/android/app/hintsandsolution/HintsAndSolutionDialogFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/hintsandsolution/HintsAndSolutionDialogFragmentPresenter.kt @@ -23,7 +23,6 @@ import org.oppia.android.databinding.SolutionSummaryBinding import org.oppia.android.util.gcsresource.DefaultResourceBucketName import org.oppia.android.util.parser.html.ExplorationHtmlParserEntityType import org.oppia.android.util.parser.html.HtmlParser -import java.lang.IllegalStateException import javax.inject.Inject const val TAG_REVEAL_SOLUTION_DIALOG = "REVEAL_SOLUTION_DIALOG" @@ -218,9 +217,11 @@ class HintsAndSolutionDialogFragmentPresenter @Inject constructor( resourceBucketName, entityType, hintsViewModel.explorationId.get()!!, - /* imageCenterAlign= */ true + /* imageCenterAlign= */ true, + displayLocale = resourceHandler.getDisplayLocale() ).parseOppiaHtml( - hintsViewModel.hintsAndSolutionSummary.get()!!, binding.hintsAndSolutionSummary + hintsViewModel.hintsAndSolutionSummary.get()!!, + binding.hintsAndSolutionSummary ) if (hintsViewModel.hintCanBeRevealed.get()!!) { @@ -281,7 +282,8 @@ class HintsAndSolutionDialogFragmentPresenter @Inject constructor( binding.solutionCorrectAnswer.text = solutionViewModel.correctAnswer.get() } binding.solutionSummary.text = htmlParserFactory.create( - resourceBucketName, entityType, viewModel.explorationId.get()!!, /* imageCenterAlign= */ true + resourceBucketName, entityType, viewModel.explorationId.get()!!, /* imageCenterAlign= */ true, + displayLocale = resourceHandler.getDisplayLocale() ).parseOppiaHtml( solutionViewModel.solutionSummary.get()!!, binding.solutionSummary ) diff --git a/app/src/main/java/org/oppia/android/app/onboarding/OnboardingActivity.kt b/app/src/main/java/org/oppia/android/app/onboarding/OnboardingActivity.kt index 9f17483389b..1b4ce35d8fb 100644 --- a/app/src/main/java/org/oppia/android/app/onboarding/OnboardingActivity.kt +++ b/app/src/main/java/org/oppia/android/app/onboarding/OnboardingActivity.kt @@ -5,18 +5,23 @@ import android.content.Intent import android.os.Bundle import org.oppia.android.app.activity.ActivityComponentImpl import org.oppia.android.app.activity.InjectableAppCompatActivity +import org.oppia.android.app.model.PolicyPage +import org.oppia.android.app.policies.PoliciesActivity +import org.oppia.android.app.policies.RouteToPoliciesListener import org.oppia.android.app.profile.ProfileChooserActivity import javax.inject.Inject /** Activity that contains the onboarding flow for learners. */ -class OnboardingActivity : InjectableAppCompatActivity(), RouteToProfileListListener { +class OnboardingActivity : + InjectableAppCompatActivity(), + RouteToProfileListListener, + RouteToPoliciesListener { @Inject lateinit var onboardingActivityPresenter: OnboardingActivityPresenter companion object { fun createOnboardingActivity(context: Context): Intent { - val intent = Intent(context, OnboardingActivity::class.java) - return intent + return Intent(context, OnboardingActivity::class.java) } } @@ -30,4 +35,8 @@ class OnboardingActivity : InjectableAppCompatActivity(), RouteToProfileListList startActivity(ProfileChooserActivity.createProfileChooserActivity(this)) finish() } + + override fun onRouteToPolicies(policyPage: PolicyPage) { + startActivity(PoliciesActivity.createPoliciesActivityIntent(this, policyPage)) + } } diff --git a/app/src/main/java/org/oppia/android/app/onboarding/OnboardingFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/onboarding/OnboardingFragmentPresenter.kt index f359a4348c7..04b07ccedc8 100644 --- a/app/src/main/java/org/oppia/android/app/onboarding/OnboardingFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/onboarding/OnboardingFragmentPresenter.kt @@ -10,12 +10,16 @@ import androidx.fragment.app.Fragment import androidx.viewpager2.widget.ViewPager2 import org.oppia.android.R import org.oppia.android.app.fragment.FragmentScope +import org.oppia.android.app.model.PolicyPage +import org.oppia.android.app.policies.RouteToPoliciesListener import org.oppia.android.app.recyclerview.BindableAdapter import org.oppia.android.app.translation.AppLanguageResourceHandler import org.oppia.android.app.viewmodel.ViewModelProvider import org.oppia.android.databinding.OnboardingFragmentBinding import org.oppia.android.databinding.OnboardingSlideBinding import org.oppia.android.databinding.OnboardingSlideFinalBinding +import org.oppia.android.util.parser.html.HtmlParser +import org.oppia.android.util.parser.html.PolicyType import org.oppia.android.util.statusbar.StatusBarColor import javax.inject.Inject @@ -26,12 +30,13 @@ class OnboardingFragmentPresenter @Inject constructor( private val fragment: Fragment, private val viewModelProvider: ViewModelProvider, private val viewModelProviderFinalSlide: ViewModelProvider, - private val resourceHandler: AppLanguageResourceHandler -) : OnboardingNavigationListener { + private val resourceHandler: AppLanguageResourceHandler, + private val htmlParserFactory: HtmlParser.Factory +) : OnboardingNavigationListener, HtmlParser.PolicyOppiaTagActionListener { private val dotsList = ArrayList() private lateinit var binding: OnboardingFragmentBinding - fun handleCreateView(inflater: LayoutInflater, container: ViewGroup?): View? { + fun handleCreateView(inflater: LayoutInflater, container: ViewGroup?): View { binding = OnboardingFragmentBinding.inflate( inflater, container, @@ -112,12 +117,43 @@ class OnboardingFragmentPresenter @Inject constructor( .registerViewDataBinder( viewType = ViewType.ONBOARDING_FINAL_SLIDE, inflateDataBinding = OnboardingSlideFinalBinding::inflate, - setViewModel = OnboardingSlideFinalBinding::setViewModel, + setViewModel = this::bindOnboardingSlideFinal, transformViewModel = { it as OnboardingSlideFinalViewModel } ) .build() } + private fun bindOnboardingSlideFinal( + binding: OnboardingSlideFinalBinding, + model: OnboardingSlideFinalViewModel + ) { + binding.viewModel = model + + val completeString: String = + resourceHandler.getStringInLocaleWithWrapping( + R.string.agree_to_terms, + resourceHandler.getStringInLocale(R.string.app_name) + ) + binding.slideTermsOfServiceAndPrivacyPolicyLinksTextView.text = htmlParserFactory.create( + policyOppiaTagActionListener = this, + displayLocale = resourceHandler.getDisplayLocale() + ).parseOppiaHtml( + completeString, + binding.slideTermsOfServiceAndPrivacyPolicyLinksTextView, + supportsLinks = true, + supportsConceptCards = false + ) + } + + override fun onPolicyPageLinkClicked(policyType: PolicyType) { + when (policyType) { + PolicyType.PRIVACY_POLICY -> + (activity as RouteToPoliciesListener).onRouteToPolicies(PolicyPage.PRIVACY_POLICY) + PolicyType.TERMS_OF_SERVICE -> + (activity as RouteToPoliciesListener).onRouteToPolicies(PolicyPage.TERMS_OF_SERVICE) + } + } + private fun getOnboardingSlideFinalViewModel(): OnboardingSlideFinalViewModel { return viewModelProviderFinalSlide.getForFragment( fragment, diff --git a/app/src/main/java/org/oppia/android/app/player/state/StatePlayerRecyclerViewAssembler.kt b/app/src/main/java/org/oppia/android/app/player/state/StatePlayerRecyclerViewAssembler.kt index 714335ff4d6..58453675b35 100644 --- a/app/src/main/java/org/oppia/android/app/player/state/StatePlayerRecyclerViewAssembler.kt +++ b/app/src/main/java/org/oppia/android/app/player/state/StatePlayerRecyclerViewAssembler.kt @@ -937,7 +937,8 @@ class StatePlayerRecyclerViewAssembler private constructor( entityType, contentViewModel.gcsEntityId, imageCenterAlign = true, - customOppiaTagActionListener = customTagListener + customOppiaTagActionListener = customTagListener, + displayLocale = resourceHandler.getDisplayLocale() ).parseOppiaHtml( contentViewModel.htmlContent.toString(), binding.contentTextView, @@ -971,7 +972,8 @@ class StatePlayerRecyclerViewAssembler private constructor( entityType, feedbackViewModel.gcsEntityId, imageCenterAlign = true, - customOppiaTagActionListener = customTagListener + customOppiaTagActionListener = customTagListener, + displayLocale = resourceHandler.getDisplayLocale() ).parseOppiaHtml( feedbackViewModel.htmlContent.toString(), binding.feedbackTextView, @@ -1079,7 +1081,8 @@ class StatePlayerRecyclerViewAssembler private constructor( entityType, submittedAnswerViewModel.gcsEntityId, imageCenterAlign = false, - customOppiaTagActionListener = customTagListener + customOppiaTagActionListener = customTagListener, + displayLocale = resourceHandler.getDisplayLocale() ) submittedAnswerViewModel.setSubmittedAnswer( htmlParser.parseOppiaHtml( @@ -1154,7 +1157,8 @@ class StatePlayerRecyclerViewAssembler private constructor( entityType, gcsEntityId, imageCenterAlign = false, - customOppiaTagActionListener = customTagListener + customOppiaTagActionListener = customTagListener, + displayLocale = resourceHandler.getDisplayLocale() ).parseOppiaHtml( viewModel, binding.submittedAnswerContentTextView, diff --git a/app/src/main/java/org/oppia/android/app/policies/PoliciesActivity.kt b/app/src/main/java/org/oppia/android/app/policies/PoliciesActivity.kt new file mode 100644 index 00000000000..27db5c803ce --- /dev/null +++ b/app/src/main/java/org/oppia/android/app/policies/PoliciesActivity.kt @@ -0,0 +1,48 @@ +package org.oppia.android.app.policies + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import org.oppia.android.app.activity.ActivityComponentImpl +import org.oppia.android.app.activity.InjectableAppCompatActivity +import org.oppia.android.app.model.PoliciesActivityParams +import org.oppia.android.app.model.PolicyPage +import org.oppia.android.util.extensions.getProtoExtra +import org.oppia.android.util.extensions.putProtoExtra +import javax.inject.Inject + +/** Activity for displaying the app policies. */ +class PoliciesActivity : InjectableAppCompatActivity() { + + @Inject + lateinit var policiesActivityPresenter: PoliciesActivityPresenter + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + (activityComponent as ActivityComponentImpl).inject(this) + + policiesActivityPresenter.handleOnCreate( + intent.getProtoExtra( + POLICIES_ACTIVITY_POLICY_PAGE_PARAMS_PROTO, + PoliciesActivityParams.getDefaultInstance() + ) + ) + } + + companion object { + /** Argument key for policy page in [PoliciesActivity]. */ + const val POLICIES_ACTIVITY_POLICY_PAGE_PARAMS_PROTO = "PoliciesActivity.policy_page" + + /** Returns the [Intent] for opening [PoliciesActivity] for the specified [policyPage]. */ + fun createPoliciesActivityIntent(context: Context, policyPage: PolicyPage): Intent { + val policiesActivityParams = + PoliciesActivityParams + .newBuilder() + .setPolicyPage(policyPage) + .build() + return Intent(context, PoliciesActivity::class.java).also { + it.putProtoExtra(POLICIES_ACTIVITY_POLICY_PAGE_PARAMS_PROTO, policiesActivityParams) + } + } + } +} diff --git a/app/src/main/java/org/oppia/android/app/policies/PoliciesActivityPresenter.kt b/app/src/main/java/org/oppia/android/app/policies/PoliciesActivityPresenter.kt new file mode 100644 index 00000000000..917fe24f75a --- /dev/null +++ b/app/src/main/java/org/oppia/android/app/policies/PoliciesActivityPresenter.kt @@ -0,0 +1,64 @@ +package org.oppia.android.app.policies + +import android.view.View +import androidx.appcompat.app.AppCompatActivity +import androidx.appcompat.widget.Toolbar +import org.oppia.android.R +import org.oppia.android.app.model.PoliciesActivityParams +import org.oppia.android.app.model.PoliciesFragmentArguments +import org.oppia.android.app.model.PolicyPage +import org.oppia.android.app.translation.AppLanguageResourceHandler +import javax.inject.Inject + +/** The presenter for [PoliciesActivity]. */ +class PoliciesActivityPresenter @Inject constructor( + private val activity: AppCompatActivity, + private val resourceHandler: AppLanguageResourceHandler +) { + + /** Handles onCreate() method of the [PoliciesActivity]. */ + fun handleOnCreate(policiesActivityParams: PoliciesActivityParams) { + activity.setContentView(R.layout.policies_activity) + val toolbar = setUpToolbar(policiesActivityParams.policyPage) + activity.supportActionBar!!.setDisplayHomeAsUpEnabled(true) + + toolbar.setNavigationOnClickListener { + activity.finish() + } + + if (getPoliciesFragment() == null) { + val policiesFragmentArguments = + PoliciesFragmentArguments + .newBuilder() + .setPolicyPage(policiesActivityParams.policyPage) + .build() + activity.supportFragmentManager.beginTransaction().add( + R.id.policies_fragment_placeholder, + PoliciesFragment.newInstance(policiesFragmentArguments) + ).commitNow() + } + } + + private fun setUpToolbar(policyPage: PolicyPage): Toolbar { + val toolbar = activity.findViewById(R.id.policies_activity_toolbar) as Toolbar + + toolbar.title = when (policyPage) { + PolicyPage.PRIVACY_POLICY -> + resourceHandler.getStringInLocale(R.string.privacy_policy_title) + PolicyPage.TERMS_OF_SERVICE -> + resourceHandler.getStringInLocale(R.string.terms_of_service_title) + PolicyPage.POLICY_PAGE_UNSPECIFIED, + PolicyPage.UNRECOGNIZED -> "" + } + activity.setSupportActionBar(toolbar) + return toolbar + } + + private fun getPoliciesFragment(): PoliciesFragment? { + return activity + .supportFragmentManager + .findFragmentById( + R.id.policies_fragment_placeholder + ) as? PoliciesFragment + } +} diff --git a/app/src/main/java/org/oppia/android/app/policies/PoliciesFragment.kt b/app/src/main/java/org/oppia/android/app/policies/PoliciesFragment.kt new file mode 100644 index 00000000000..06cd8bf8b67 --- /dev/null +++ b/app/src/main/java/org/oppia/android/app/policies/PoliciesFragment.kt @@ -0,0 +1,53 @@ +package org.oppia.android.app.policies + +import android.content.Context +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import org.oppia.android.app.fragment.FragmentComponentImpl +import org.oppia.android.app.fragment.InjectableFragment +import org.oppia.android.app.model.PoliciesFragmentArguments +import org.oppia.android.util.extensions.getProto +import org.oppia.android.util.extensions.putProto +import javax.inject.Inject + +private const val POLICIES_FRAGMENT_POLICY_PAGE_ARGUMENT_PROTO = "PoliciesFragment.policy_page" + +/** Fragment that contains policies flow of the app. */ +class PoliciesFragment : InjectableFragment() { + @Inject + lateinit var policiesFragmentPresenter: PoliciesFragmentPresenter + + companion object { + /** Returns instance of [PoliciesFragment]. */ + fun newInstance(policiesFragmentArguments: PoliciesFragmentArguments): PoliciesFragment { + val args = Bundle() + args.putProto(POLICIES_FRAGMENT_POLICY_PAGE_ARGUMENT_PROTO, policiesFragmentArguments) + val fragment = PoliciesFragment() + fragment.arguments = args + return fragment + } + } + + override fun onAttach(context: Context) { + super.onAttach(context) + (fragmentComponent as FragmentComponentImpl).inject(this) + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + val args = checkNotNull(arguments) { + "Expected arguments to be passed to PoliciesFragment" + } + val policies = + args.getProto( + POLICIES_FRAGMENT_POLICY_PAGE_ARGUMENT_PROTO, + PoliciesFragmentArguments.getDefaultInstance() + ) + return policiesFragmentPresenter.handleCreateView(inflater, container, policies) + } +} diff --git a/app/src/main/java/org/oppia/android/app/policies/PoliciesFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/policies/PoliciesFragmentPresenter.kt new file mode 100644 index 00000000000..e42e38497ae --- /dev/null +++ b/app/src/main/java/org/oppia/android/app/policies/PoliciesFragmentPresenter.kt @@ -0,0 +1,83 @@ +package org.oppia.android.app.policies + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import org.oppia.android.R +import org.oppia.android.app.fragment.FragmentScope +import org.oppia.android.app.model.PoliciesFragmentArguments +import org.oppia.android.app.model.PolicyPage +import org.oppia.android.app.translation.AppLanguageResourceHandler +import org.oppia.android.databinding.PoliciesFragmentBinding +import org.oppia.android.util.parser.html.HtmlParser +import javax.inject.Inject + +/** The presenter for [PoliciesFragment]. */ +@FragmentScope +class PoliciesFragmentPresenter @Inject constructor( + private val htmlParserFactory: HtmlParser.Factory, + private val resourceHandler: AppLanguageResourceHandler +) { + + /** Handles onCreate() method of the [PoliciesFragment]. */ + fun handleCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + policiesFragmentArguments: PoliciesFragmentArguments + ): View { + val binding = PoliciesFragmentBinding.inflate( + inflater, + container, + /* attachToRoot= */ false + ) + setUpContentForTextViews(policiesFragmentArguments.policyPage, binding) + + return binding.root + } + + private fun setUpContentForTextViews( + policyPage: PolicyPage, + binding: PoliciesFragmentBinding + ) { + var policyDescription = "" + var policyWebLink = "" + + if (policyPage == PolicyPage.PRIVACY_POLICY) { + policyDescription = + resourceHandler.getStringInLocale(R.string.privacy_policy_content) + policyWebLink = resourceHandler.getStringInLocale(R.string.privacy_policy_web_link) + } else if (policyPage == PolicyPage.TERMS_OF_SERVICE) { + policyDescription = + resourceHandler.getStringInLocale(R.string.terms_of_service_content) + policyWebLink = resourceHandler.getStringInLocale(R.string.terms_of_service_web_link) + } + + binding.policyDescriptionTextView.text = htmlParserFactory.create( + gcsResourceName = "", + entityType = "", + entityId = "", + imageCenterAlign = false, + customOppiaTagActionListener = null, + resourceHandler.getDisplayLocale() + ).parseOppiaHtml( + policyDescription, + binding.policyDescriptionTextView, + supportsLinks = true, + supportsConceptCards = false + ) + + binding.policyWebLinkTextView.text = htmlParserFactory.create( + gcsResourceName = "", + entityType = "", + entityId = "", + imageCenterAlign = false, + customOppiaTagActionListener = null, + resourceHandler.getDisplayLocale() + ).parseOppiaHtml( + policyWebLink, + binding.policyWebLinkTextView, + supportsLinks = true, + supportsConceptCards = false + ) + } +} diff --git a/app/src/main/java/org/oppia/android/app/policies/RouteToPoliciesListener.kt b/app/src/main/java/org/oppia/android/app/policies/RouteToPoliciesListener.kt new file mode 100644 index 00000000000..f3ed295ea19 --- /dev/null +++ b/app/src/main/java/org/oppia/android/app/policies/RouteToPoliciesListener.kt @@ -0,0 +1,9 @@ +package org.oppia.android.app.policies + +import org.oppia.android.app.model.PolicyPage + +/** Listener for when a selection should result in displaying a policy page (e.g. the Privacy Policy). */ +interface RouteToPoliciesListener { + /** Called when the user wants to view an app policy. */ + fun onRouteToPolicies(policyPage: PolicyPage) +} diff --git a/app/src/main/java/org/oppia/android/app/resumelesson/ResumeLessonFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/resumelesson/ResumeLessonFragmentPresenter.kt index 348db4bc672..61208c665a7 100644 --- a/app/src/main/java/org/oppia/android/app/resumelesson/ResumeLessonFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/resumelesson/ResumeLessonFragmentPresenter.kt @@ -12,6 +12,7 @@ import org.oppia.android.app.home.RouteToExplorationListener import org.oppia.android.app.model.ChapterSummary import org.oppia.android.app.model.ExplorationCheckpoint import org.oppia.android.app.model.ProfileId +import org.oppia.android.app.translation.AppLanguageResourceHandler import org.oppia.android.app.viewmodel.ViewModelProvider import org.oppia.android.databinding.ResumeLessonFragmentBinding import org.oppia.android.domain.exploration.ExplorationDataController @@ -32,6 +33,7 @@ class ResumeLessonFragmentPresenter @Inject constructor( private val explorationDataController: ExplorationDataController, private val htmlParserFactory: HtmlParser.Factory, @DefaultResourceBucketName private val resourceBucketName: String, + private val appLanguageResourceHandler: AppLanguageResourceHandler, private val oppiaLogger: OppiaLogger ) { @@ -121,7 +123,8 @@ class ResumeLessonFragmentPresenter @Inject constructor( resourceBucketName, resumeLessonViewModel.entityType, explorationId, - imageCenterAlign = true + imageCenterAlign = true, + displayLocale = appLanguageResourceHandler.getDisplayLocale() ).parseOppiaHtml( resumeLessonViewModel.chapterSummary.get()!!.summary, binding.resumeLessonChapterDescriptionTextView diff --git a/app/src/main/java/org/oppia/android/app/shim/ViewBindingShimImpl.kt b/app/src/main/java/org/oppia/android/app/shim/ViewBindingShimImpl.kt index fcb28a4b22e..cd577465e07 100644 --- a/app/src/main/java/org/oppia/android/app/shim/ViewBindingShimImpl.kt +++ b/app/src/main/java/org/oppia/android/app/shim/ViewBindingShimImpl.kt @@ -14,6 +14,7 @@ import org.oppia.android.app.home.promotedlist.PromotedStoryViewModel import org.oppia.android.app.model.WrittenTranslationContext import org.oppia.android.app.player.state.itemviewmodel.DragDropInteractionContentViewModel import org.oppia.android.app.player.state.itemviewmodel.SelectionInteractionContentViewModel +import org.oppia.android.app.translation.AppLanguageResourceHandler import org.oppia.android.databinding.ComingSoonTopicViewBinding import org.oppia.android.databinding.DragDropInteractionItemsBinding import org.oppia.android.databinding.DragDropSingleItemBinding @@ -34,7 +35,8 @@ import javax.inject.Inject */ // TODO(#1619): Remove file post-Gradle class ViewBindingShimImpl @Inject constructor( - private val translationController: TranslationController + private val translationController: TranslationController, + private val appLanguageResourceHandler: AppLanguageResourceHandler ) : ViewBindingShim { override fun providePromotedStoryCardInflatedView( @@ -103,7 +105,8 @@ class ViewBindingShimImpl @Inject constructor( resourceBucketName, entityType, entityId, - false + false, + displayLocale = appLanguageResourceHandler.getDisplayLocale() ).parseOppiaHtml( translationController.extractString(viewModel.htmlContent, writtenTranslationContext), binding.itemSelectionContentsTextView @@ -136,7 +139,8 @@ class ViewBindingShimImpl @Inject constructor( DataBindingUtil.findBinding(view)!! binding.htmlContent = htmlParserFactory.create( - resourceBucketName, entityType, entityId, /* imageCenterAlign= */ false + resourceBucketName, entityType, entityId, /* imageCenterAlign= */ false, + displayLocale = appLanguageResourceHandler.getDisplayLocale() ).parseOppiaHtml( translationController.extractString(viewModel.htmlContent, writtenTranslationContext), binding.multipleChoiceContentTextView @@ -222,7 +226,8 @@ class ViewBindingShimImpl @Inject constructor( resourceBucketName, entityType, entityId, - /* imageCenterAlign= */ false + /* imageCenterAlign= */ false, + displayLocale = appLanguageResourceHandler.getDisplayLocale() ).parseOppiaHtml( viewModel, dragDropSingleItemBinding.dragDropContentTextView ) diff --git a/app/src/main/java/org/oppia/android/app/story/StoryFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/story/StoryFragmentPresenter.kt index baa1efc5b09..6710b586099 100644 --- a/app/src/main/java/org/oppia/android/app/story/StoryFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/story/StoryFragmentPresenter.kt @@ -167,9 +167,11 @@ class StoryFragmentPresenter @Inject constructor( resourceBucketName, entityType, storyItemViewModel.storyId, - imageCenterAlign = true + imageCenterAlign = true, + displayLocale = resourceHandler.getDisplayLocale() ).parseOppiaHtml( - storyItemViewModel.summary, binding.chapterSummary + storyItemViewModel.summary, + binding.chapterSummary ) if (storyItemViewModel.chapterSummary.chapterPlayState == ChapterPlayState.NOT_PLAYABLE_MISSING_PREREQUISITES diff --git a/app/src/main/java/org/oppia/android/app/testing/ListItemLeadingMarginSpanTestActivity.kt b/app/src/main/java/org/oppia/android/app/testing/ListItemLeadingMarginSpanTestActivity.kt new file mode 100644 index 00000000000..91d51d33e52 --- /dev/null +++ b/app/src/main/java/org/oppia/android/app/testing/ListItemLeadingMarginSpanTestActivity.kt @@ -0,0 +1,15 @@ +package org.oppia.android.app.testing + +import android.os.Bundle +import org.oppia.android.R +import org.oppia.android.app.activity.ActivityComponentImpl +import org.oppia.android.app.activity.InjectableAppCompatActivity + +/** This is a dummy activity to test unordered
    and ordered
      lists leading margin span. */ +class ListItemLeadingMarginSpanTestActivity : InjectableAppCompatActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + (activityComponent as ActivityComponentImpl).inject(this) + setContentView(R.layout.test_list_item_leading_margin_activity) + } +} diff --git a/app/src/main/java/org/oppia/android/app/testing/PoliciesFragmentTestActivity.kt b/app/src/main/java/org/oppia/android/app/testing/PoliciesFragmentTestActivity.kt new file mode 100644 index 00000000000..e85b14b61f6 --- /dev/null +++ b/app/src/main/java/org/oppia/android/app/testing/PoliciesFragmentTestActivity.kt @@ -0,0 +1,49 @@ +package org.oppia.android.app.testing + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import org.oppia.android.app.activity.ActivityComponentImpl +import org.oppia.android.app.activity.InjectableAppCompatActivity +import org.oppia.android.app.model.PoliciesActivityParams +import org.oppia.android.app.model.PolicyPage +import org.oppia.android.util.extensions.getProtoExtra +import org.oppia.android.util.extensions.putProtoExtra +import javax.inject.Inject + +/** Test Activity used for testing [PoliciesFragment] */ +class PoliciesFragmentTestActivity : InjectableAppCompatActivity() { + + @Inject + lateinit var policiesFragmentTestActivityPresenter: PoliciesFragmentTestActivityPresenter + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + (activityComponent as ActivityComponentImpl).inject(this) + + policiesFragmentTestActivityPresenter.handleOnCreate( + intent.getProtoExtra( + POLICIES_FRAGMENT_TEST_POLICY_PAGE_PARAMS_PROTO, + PoliciesActivityParams.getDefaultInstance() + ) + ) + } + + companion object { + /** Argument key for policy page in [PoliciesFragmentTestActivity]. */ + const val POLICIES_FRAGMENT_TEST_POLICY_PAGE_PARAMS_PROTO = + "PoliciesFragmentTestActivity.policy_page" + + /** Returns the [Intent] for opening [PoliciesFragmentTestActivity] for the specified [policyPage]. */ + fun createPoliciesFragmentTestActivity(context: Context, policyPage: PolicyPage): Intent { + val policiesActivityParams = + PoliciesActivityParams + .newBuilder() + .setPolicyPage(policyPage) + .build() + return Intent(context, PoliciesFragmentTestActivity::class.java).also { + it.putProtoExtra(POLICIES_FRAGMENT_TEST_POLICY_PAGE_PARAMS_PROTO, policiesActivityParams) + } + } + } +} diff --git a/app/src/main/java/org/oppia/android/app/testing/PoliciesFragmentTestActivityPresenter.kt b/app/src/main/java/org/oppia/android/app/testing/PoliciesFragmentTestActivityPresenter.kt new file mode 100644 index 00000000000..acacdf432c0 --- /dev/null +++ b/app/src/main/java/org/oppia/android/app/testing/PoliciesFragmentTestActivityPresenter.kt @@ -0,0 +1,44 @@ +package org.oppia.android.app.testing + +import androidx.appcompat.app.AppCompatActivity +import org.oppia.android.R +import org.oppia.android.app.activity.ActivityScope +import org.oppia.android.app.model.PoliciesActivityParams +import org.oppia.android.app.model.PoliciesFragmentArguments +import org.oppia.android.app.policies.PoliciesFragment +import javax.inject.Inject + +/** The presenter for [PoliciesFragmentTestActivity] */ +@ActivityScope +class PoliciesFragmentTestActivityPresenter @Inject constructor( + private val activity: AppCompatActivity +) { + + /** Handles onCreate() method of the [PoliciesFragmentTestActivity]. */ + fun handleOnCreate(policiesActivityParams: PoliciesActivityParams) { + activity.setContentView(R.layout.policies_fragment_test_activity) + if (getPoliciesFragment() == null) { + val policiesFragmentArguments = + PoliciesFragmentArguments + .newBuilder() + .setPolicyPage(policiesActivityParams.policyPage) + .build() + val policiesFragment: PoliciesFragment = + PoliciesFragment.newInstance(policiesFragmentArguments) + + activity + .supportFragmentManager + .beginTransaction() + .add( + R.id.policies_fragment_placeholder, + policiesFragment + ).commitNow() + } + } + + private fun getPoliciesFragment(): PoliciesFragment? { + return activity + .supportFragmentManager + .findFragmentById(R.id.policies_fragment_placeholder) as PoliciesFragment? + } +} diff --git a/app/src/main/java/org/oppia/android/app/topic/conceptcard/ConceptCardFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/topic/conceptcard/ConceptCardFragmentPresenter.kt index 28c8d1b8415..4b731ea971f 100644 --- a/app/src/main/java/org/oppia/android/app/topic/conceptcard/ConceptCardFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/topic/conceptcard/ConceptCardFragmentPresenter.kt @@ -7,6 +7,7 @@ import androidx.fragment.app.Fragment import org.oppia.android.R import org.oppia.android.app.fragment.FragmentScope import org.oppia.android.app.model.ProfileId +import org.oppia.android.app.translation.AppLanguageResourceHandler import org.oppia.android.app.viewmodel.ViewModelProvider import org.oppia.android.databinding.ConceptCardFragmentBinding import org.oppia.android.domain.oppialogger.OppiaLogger @@ -25,7 +26,8 @@ class ConceptCardFragmentPresenter @Inject constructor( @ConceptCardHtmlParserEntityType private val entityType: String, @DefaultResourceBucketName private val resourceBucketName: String, private val viewModelProvider: ViewModelProvider, - private val translationController: TranslationController + private val translationController: TranslationController, + private val appLanguageResourceHandler: AppLanguageResourceHandler ) { /** * Sets up data binding and toolbar. @@ -70,8 +72,16 @@ class ConceptCardFragmentPresenter @Inject constructor( ephemeralConceptCard.writtenTranslationContext ) view.text = htmlParserFactory - .create(resourceBucketName, entityType, skillId, imageCenterAlign = true) - .parseOppiaHtml(explanationHtml, view) + .create( + resourceBucketName, + entityType, + skillId, + imageCenterAlign = true, + displayLocale = appLanguageResourceHandler.getDisplayLocale() + ) + .parseOppiaHtml( + explanationHtml, view + ) } ) diff --git a/app/src/main/java/org/oppia/android/app/topic/info/TopicInfoFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/topic/info/TopicInfoFragmentPresenter.kt index 9195ce8ebb5..b7c0cda88cf 100644 --- a/app/src/main/java/org/oppia/android/app/topic/info/TopicInfoFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/topic/info/TopicInfoFragmentPresenter.kt @@ -34,15 +34,6 @@ class TopicInfoFragmentPresenter @Inject constructor( private val topicInfoViewModel = getTopicInfoViewModel() private var internalProfileId: Int = -1 private lateinit var topicId: String - private val htmlParser: HtmlParser by lazy { - htmlParserFactory - .create( - resourceBucketName, - /* entityType= */ "topic", - topicId, - /* imageCenterAlign= */ true - ) - } fun handleCreateView( inflater: LayoutInflater, diff --git a/app/src/main/java/org/oppia/android/app/topic/revisioncard/RevisionCardFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/topic/revisioncard/RevisionCardFragmentPresenter.kt index f83db2ce970..35d2c496e22 100755 --- a/app/src/main/java/org/oppia/android/app/topic/revisioncard/RevisionCardFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/topic/revisioncard/RevisionCardFragmentPresenter.kt @@ -8,6 +8,7 @@ import org.oppia.android.app.fragment.FragmentScope import org.oppia.android.app.model.ProfileId import org.oppia.android.app.topic.conceptcard.ConceptCardFragment import org.oppia.android.app.topic.conceptcard.ConceptCardFragment.Companion.CONCEPT_CARD_DIALOG_FRAGMENT_TAG +import org.oppia.android.app.translation.AppLanguageResourceHandler import org.oppia.android.app.viewmodel.ViewModelProvider import org.oppia.android.databinding.RevisionCardFragmentBinding import org.oppia.android.domain.oppialogger.OppiaLogger @@ -26,7 +27,8 @@ class RevisionCardFragmentPresenter @Inject constructor( @DefaultResourceBucketName private val resourceBucketName: String, @TopicHtmlParserEntityType private val entityType: String, private val viewModelProvider: ViewModelProvider, - private val translationController: TranslationController + private val translationController: TranslationController, + private val appLanguageResourceHandler: AppLanguageResourceHandler ) : HtmlParser.CustomOppiaTagActionListener { private lateinit var profileId: ProfileId @@ -66,7 +68,8 @@ class RevisionCardFragmentPresenter @Inject constructor( ) view.text = htmlParserFactory.create( resourceBucketName, entityType, topicId, imageCenterAlign = true, - customOppiaTagActionListener = this + customOppiaTagActionListener = this, + displayLocale = appLanguageResourceHandler.getDisplayLocale() ).parseOppiaHtml( pageContentsHtml, view, supportsLinks = true, supportsConceptCards = true ) diff --git a/app/src/main/java/org/oppia/android/app/translation/AppLanguageResourceHandler.kt b/app/src/main/java/org/oppia/android/app/translation/AppLanguageResourceHandler.kt index 4887f41175a..dc31fe5ccd2 100644 --- a/app/src/main/java/org/oppia/android/app/translation/AppLanguageResourceHandler.kt +++ b/app/src/main/java/org/oppia/android/app/translation/AppLanguageResourceHandler.kt @@ -132,6 +132,7 @@ class AppLanguageResourceHandler @Inject constructor( /** See [OppiaLocale.DisplayLocale.getLayoutDirection]. */ fun getLayoutDirection(): Int = getDisplayLocale().getLayoutDirection() - private fun getDisplayLocale(): OppiaLocale.DisplayLocale = + /** Returns the current [OppiaLocale.DisplayLocale] used for resource processing. */ + fun getDisplayLocale(): OppiaLocale.DisplayLocale = appLanguageLocaleHandler.getDisplayLocale() } diff --git a/app/src/main/res/layout-land/onboarding_slide_final.xml b/app/src/main/res/layout-land/onboarding_slide_final.xml index 02e4afdb540..365ca188dc6 100644 --- a/app/src/main/res/layout-land/onboarding_slide_final.xml +++ b/app/src/main/res/layout-land/onboarding_slide_final.xml @@ -80,6 +80,21 @@ app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toEndOf="@+id/slide_image_view" app:layout_constraintTop_toBottomOf="@id/slide_description_text_view" /> + + + diff --git a/app/src/main/res/layout-sw600dp-land/onboarding_slide_final.xml b/app/src/main/res/layout-sw600dp-land/onboarding_slide_final.xml index df1ed7a87b6..2e7a4b532a8 100644 --- a/app/src/main/res/layout-sw600dp-land/onboarding_slide_final.xml +++ b/app/src/main/res/layout-sw600dp-land/onboarding_slide_final.xml @@ -75,6 +75,16 @@ app:layout_constraintStart_toStartOf="@+id/slide_title_text_view" app:layout_constraintTop_toBottomOf="@id/slide_description_text_view" /> + + + + + diff --git a/app/src/main/res/layout/onboarding_slide_final.xml b/app/src/main/res/layout/onboarding_slide_final.xml index 04b32e15546..a4dc71c819d 100644 --- a/app/src/main/res/layout/onboarding_slide_final.xml +++ b/app/src/main/res/layout/onboarding_slide_final.xml @@ -83,6 +83,20 @@ app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/slide_description_text_view" /> + + + diff --git a/app/src/main/res/layout/policies_activity.xml b/app/src/main/res/layout/policies_activity.xml new file mode 100644 index 00000000000..49f40041965 --- /dev/null +++ b/app/src/main/res/layout/policies_activity.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/policies_fragment.xml b/app/src/main/res/layout/policies_fragment.xml new file mode 100644 index 00000000000..4fe60b6af41 --- /dev/null +++ b/app/src/main/res/layout/policies_fragment.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/policies_fragment_test_activity.xml b/app/src/main/res/layout/policies_fragment_test_activity.xml new file mode 100644 index 00000000000..4a50b06e26e --- /dev/null +++ b/app/src/main/res/layout/policies_fragment_test_activity.xml @@ -0,0 +1,7 @@ + + diff --git a/app/src/main/res/layout/test_list_item_leading_margin_activity.xml b/app/src/main/res/layout/test_list_item_leading_margin_activity.xml new file mode 100644 index 00000000000..07dc89231ee --- /dev/null +++ b/app/src/main/res/layout/test_list_item_leading_margin_activity.xml @@ -0,0 +1,11 @@ + + + + + diff --git a/app/src/main/res/layout/view_event_logs_event_log_item_view.xml b/app/src/main/res/layout/view_event_logs_event_log_item_view.xml index 47849bce089..82af9772cc6 100644 --- a/app/src/main/res/layout/view_event_logs_event_log_item_view.xml +++ b/app/src/main/res/layout/view_event_logs_event_log_item_view.xml @@ -42,7 +42,7 @@ 36dp 60dp 60dp + + + 24dp + 22dp + 28dp + 60dp + 80dp diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index 55dbf12bbd3..7e1c6fc8d9f 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -442,6 +442,13 @@ 60dp 80dp + + 24dp + 22dp + 28dp + 60dp + 80dp + 80dp 60dp diff --git a/app/src/main/res/values/privacy_policy.xml b/app/src/main/res/values/privacy_policy.xml new file mode 100644 index 00000000000..68a03b643e0 --- /dev/null +++ b/app/src/main/res/values/privacy_policy.xml @@ -0,0 +1,401 @@ + + + + Oppia\'s mission is to make it easy for users to learn anything they + want to in an effective and enjoyable way. As part of achieving that + mission, Oppia (also "we" or "us") collects some information from its + users to deliver an interactive and engaging experience. This Privacy + Policy describes the types of information we collect from users of the + Oppia website (https://www.oppia.org) + (the "Site") and how Oppia collects, uses, discloses, and protects that + information. By using the Site or providing Personal Information (as + defined below) to us, you agree to this Privacy Policy. +

      + +

      + Please note that the Oppia codebase (i.e., our software code, but not + any Personal Information we have collected from our users) may be + downloaded and hosted independently by third parties. This Privacy + Policy covers only our Site and not third-party sites or services using + the Oppia codebase. If you are using such a third-party site or service, + we encourage you to review the third party\'s privacy policy for that + site or service. +

      + +

      What information do we collect and how do we use it?

      + +

      Personal Information

      + +

      + The Site is designed to be open and free, which means that you do not + have to register, create an account or sign in to use the Site. If you + do choose to register with the Site, we collect certain information that + identifies you as an individual or relates to you as an identifiable + person ("Personal Information"), including: +

        +
      • your email address;
      • +
      • your user name;
      • +
      • your photo (if you choose to upload one); and
      • +
      • + any other Personal Information you submit to us during your profile + registration process ("Profile Information") or on any forms or + other input fields on the Site. +
      • +
      +

      + +

      + If you disclose any Personal Information relating to other people to us + in connection with the Site, you represent that you have the authority + to do so and to permit us to use and disclose the information in + accordance with this Privacy Policy. +

      + +

      + We may use Personal Information: +

        +
      • + to respond to your inquiries and fulfill your requests, such as to + send you updates regarding the Site; +
      • +
      • + to send administrative information to you, such as information + regarding the Site and changes to our terms, conditions, and + policies; +
      • +
      • to send you information about the Site and other offerings;
      • +
      • to provide you with technical support and customer service;
      • +
      • + to personalize your experience on the Site by suggesting new + educational activities, including but not limited to the + Explorations, Collections and Questions accessible on the Site (the + "Lessons"), for you to try and by helping you to track your + progress; +
      • +
      • + to allow you to provide feedback to and receive feedback from + other users on Lessons; +
      • +
      • + to allow you to participate in contests and similar promotions and + to administer these activities, some of which may have additional + rules about how we use and disclose your Personal Information. We + suggest that you read any such rules carefully; +
      • +
      • to facilitate social sharing functionality; and
      • +
      • + for our business purposes, such as data analysis, audits, fraud + monitoring and prevention, developing new products, providing + technical fixes, enhancing, improving or modifying our products or + services, identifying usage trends, determining the effectiveness + of our promotional campaigns and operating and expanding our + business activities. +
      • +
      +

      + +

      Other Information

      + +

      + "Other Information" is information that does not reveal your specific + identity or does not relate to you as an identifiable person, including: +

        +
      • + "Usage Data", such as: +
          +
        • your answers to Lessons;
        • +
        • when you begin and end a Lesson;
        • +
        • when you visit the site;
        • +
        • what pages you visit while using the Site;
        • +
        • + the page from which you navigated to the Site and the page to + which you navigate when you leave the Site; +
        • +
        • + any playthrough-specific customizations to a Lesson (such as + randomly generated parameters); +
        • +
        • + any contributions you make to the Site (such as feedback on + Lessons, edits to Lessons, and Lessons created); +
        • +
        +
      • + +
      • + browser and device information, such as: +
          +
        • the IP address of the computer used to access the Site;
        • +
        • what browser or device you are using to visit the Site;
        • +
        • what operating system you are using;
        • +
        +
      • + +
      • + information collected through cookies, pixel tags and other + technologies; +
      • +
      • + demographic information and other information disclosed by you; and +
      • +
      • aggregated information
      • +
      +

      + +

      + Usage Data: + We collect information when a user, whether or not the user is signed + in, visits the Site. We use Usage Data to guide the learning experience, + to evaluate the effectiveness of contributions, to improve our + understanding of the learning process and to otherwise improve the + effectiveness of our offerings. We do not correlate Usage Data with + Personal Information unless the user has created an account and signed + in, in which case only Usage Data collected or generated by the user + related to Lessons is correlated with the user\'s account. +

      + +

      + Browser and device information: + Certain information is collected by most browsers or automatically + through your device, such as your Media Access Control (MAC) address, + computer type (Windows or Macintosh), screen resolution, operating + system name and version, device manufacturer and model, language and + Internet browser type and version. We use this information to ensure + that the Site functions properly and to better understand our users. +

      + +

      + Cookies: + We may collect Other Information through the use of cookies. Cookies + are small text files with a string of alphanumeric characters that keep + track of user activities, stored directly on the computer you are using. + Cookies allow us to collect information such as browser type, time + spent on the Site, pages visited, language preferences, and other + traffic data. +

      + +

      + If, during your visit to our Site, you do not want information + collected through the use of cookies, you may change the settings on + your browser to automatically decline cookies, to give you the choice + of declining or accepting the cookies from a particular site on a + case-by-case basis, or, in some instances, to automatically delete + cookies upon exiting the browser. You may also wish to refer to + http://www.allaboutcookies.org/manage-cookies/index.html. + If you choose not to accept cookies from our Site, you may + experience some reduced functionality or other inconvenience while + accessing the Site. +

      + +

      We do not respond to browser do-not-track signals at this time.

      + +

      + Using pixel tags and other similar technologies: + Pixel tags (also known as web beacons and clear GIFs) may be used in + connection with the Site to, among other things, track the actions of + users, measure the success of our marketing campaigns and compile + statistics about use of the Site. +

      + +

      + Google Services: + Our Site is built on Google App Engine, which uses cookies and performs + default request logging. We also use Google Analytics to better + understand how our users use our Site. Google Analytics uses cookies + and other technologies to collect this information. For further + information about the collection and use of data through Google + Analytics, please refer to: https://www.google.com/policies/privacy/partners/. +

      + +

      + Google offers the ability to opt out from tracking through Google + Analytics cookies; for more information, visit: + https://tools.google.com/dlpage/gaoptout. +

      + +

      + IP Address: + Your IP address is a number that is automatically assigned to the + computer that you are using by your Internet Service Provider. An IP + address may be identified and logged automatically in our server log + files whenever a user accesses the Site, along with the time of the + visit and the pages visited. Collecting IP addresses is standard + practice and is done automatically by many websites, applications and + other services. We use IP addresses for purposes such as calculating + usage levels, diagnosing server problems and administering the Site. +

      + +

      + From you: + Information such as your preferred means of communication is collected + when you voluntarily disclose it. +

      + +

      + By aggregating information: + Aggregated Personal Information does not personally identify you or any + other user of the Site. +

      + +

      + We may use and disclose Other Information for any purpose, except where + we are required to do otherwise under applicable law. If we combine + Other Information with Personal Information, we will treat the combined + information as Personal Information as long as it is combined. If we + are required to treat Other Information as Personal Information under + applicable law, then we may use it for the purposes for which we use + and disclose Personal Information as detailed in this Privacy Policy. +

      + +

      How is your information shared or disclosed?

      + +

      + Your Personal Information may be disclosed: +

        +
      • + to our service providers (including Google) who provide services + such as website hosting, data analysis, information technology and + related infrastructure provision, customer service, email delivery, + auditing and other services; +
      • +
      • to third-party sponsors of contests and similar promotions;
      • +
      • + to identify you to anyone to whom you send messages or feedback + through the Site; +
      • +
      • + by you, on or through your profile page (including your Profile + Information), and your messages, edits and contributions to the + Site and feedback to us or other users. Please note that any + information you choose to post or disclose will become public and + may be available to other users and the general public. We urge you + to be thoughtful when deciding to disclose any information on the + Site. +
      • +
      • + to your friends associated with your social media account + (including, but not limited to, your Google Plus account), to other + Site users and to your social media account provider, in connection + with your social sharing activity. By connecting your Site account + and your social media account, you authorize us to share + information with your social media account provider, and you + understand that the use of the information we share will be + governed by the social media site\'s privacy policy. +
      • +
      +

      + +

      + We also reserve the right to disclose Personal Information or other + information: (a) to comply with applicable legal requirements (for + example, responding to subpoenas); (b) to respond to requests from + public and government authorities; (c) to enforce our Terms of Use; + (d) to protect our, your, and/or third parties\' rights, privacy, safety + or property; (e) to allow us to pursue available remedies or limit the + damages that we may sustain; and (f) in connection with a + reorganization, merger, sale, joint venture, assignment, transfer, or + other disposition of all or any portion of our business, assets or + stock (including in connection with any bankruptcy or similar + proceedings). +

      + +

      How do we protect Personal Information?

      +

      + Oppia is committed to protecting your information. We seek to use + reasonable organizational, technical and administrative measures to + protect Personal Information that we maintain within our organization. + We also seek to use third-party service providers capable of protecting + the information they maintain or process for us. Unfortunately, no + data transmission or storage system can be guaranteed to be 100% + secure. If you have reason to believe that your data has been + compromised or your use of the Site is no longer secure, please + immediately notify us of the problem by contacting us at the following + email address: admin@oppia.org. +

      + +

      Choices and access

      +

      + If you register and provide Oppia with Personal Information, you may + update or delete your Personal Information at any time by reviewing + your Profile Information and preferences on your account settings page + or by contacting us at admin@oppia.org. The only exception is that we + cannot allow users to change their email address or username. +

      + +

      + If you no longer want to receive marketing-related emails from us on a + going-forward basis, you may opt out by following the instructions + contained in each such email. We will try to comply with your request + as soon as reasonably practicable. Please note that if you opt out, we + may still send you important administrative messages, from which you + cannot opt out. +

      + +

      + If you are a resident of California, under 18, and a registered user of + the Site, you may ask us to remove content or information that you have + posted to the Site by writing to admin@oppia.org. Please note that your + request does not ensure complete or comprehensive removal of the + content or information, as, for example, some of your content may have + been reposted by another user. +

      + +

      Use by children

      +

      + The Site is not directed to individuals under the age of thirteen (13). + If you are under 13, we do not want your Personal Information and you + should not provide it to us. If you are a legal guardian and believe + that your child who is under 13 has provided us with Personal + Information, please contact us at admin@oppia.org to have your child\'s + information removed. +

      + +

      Jurisdictional issues

      +

      + The Site is controlled and operated by us from the United States, and + is not intended to subject us to the laws or jurisdiction of any state, + country or territory other than that of the United States. Your + Personal Information may be stored and processed in any country where + we or our service providers have facilities, and by using the Site you + consent to the transfer of information to countries outside of your + country of residence, including the United States, which may have + different data protection rules than those of your country. In certain + circumstances, courts, law enforcement agencies, regulatory agencies or + security authorities in those other countries may be entitled to access + your Personal Information. +

      + +

      Links to third-party sites

      +

      + Our Site and content may feature links to third party websites that + offer goods, services or information. When you click on one of these + links, you will be leaving our Site and will no longer be subject to + this Privacy Policy. We are not responsible for the information or other + practices of the other sites that you visit and urge you to review + their privacy policies before you provide them with any personally + identifiable information. Third party sites or services may collect and + use information about you in a way that is different from this Privacy + Policy. +

      + +

      Changes and updates to this Privacy Policy

      +

      + Oppia will continue to evolve and we may update our Privacy Policy from + time to time to reflect our practices. The "Last updated" legend at the + bottom of this Privacy Policy indicates when this Privacy Policy was + last revised, and any changes will become effective immediately upon + posting of the Privacy Policy on the Site. Your continued use of the + Site following these changes constitutes your agreement to the revised + Privacy Policy. +

      + +

      Contacting Us

      +

      + If you have questions about this Privacy Policy or would like to + provide feedback, please send an e-mail to admin@oppia.org. +

      + + Last updated: 24 May 2018 +]]>
      +
      diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 8851ba79912..dbb8d131dbb 100755 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -526,6 +526,14 @@ Good morning, Good afternoon, Good evening, + + Policy Page + Privacy Policy + this page for the latest version of this privacy policy.]]> + + Terms of Service + Terms of Service and Privacy Policy.]]> + this page for the latest version of these terms.]]> How can I create a new profile? How can I delete a profile? @@ -536,14 +544,14 @@ 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:


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

    3. 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.

      ]]>
      + 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:

      1. The profile cannot be recovered.
      2. 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:

      1. Enter your new email and tap Save.
      2. 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:

      1. Enter your new phone number and tap Verify.
      2. 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:

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

    5. Ask the Administrator to check their device and internet connection:

    6. Get the Administrator to troubleshoot using the steps above

    7. Let us know if you still have issues with loading:

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


      Check to see if the app is up to date:

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

    11. Check your internet connection:

    12. 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.

    13. Ask the Administrator to check their device and internet connection:

    14. Get the Administrator to troubleshoot using the steps above

    15. Let us know if you still have issues with loading:

    16. Report a problem by contacting us at admin@oppia.org.
    17. ]]>
      - 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 the Exploration Player is not loading


      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.

      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.

      ]]>
      Profile Edit Fragment Test Activity Administrator Controls Fragment Test Activity diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index d7237b66bd2..68d1bd223a4 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -256,6 +256,10 @@ @color/component_color_shared_text_view_heading_text_color + + @@ -311,10 +315,14 @@ viewStart - + +