=
getSpans(/* start= */ 0, /* end= */ length, spanClass.javaObjectType)
diff --git a/utility/src/test/java/org/oppia/android/util/parser/html/LiTagHandlerTest.kt b/utility/src/test/java/org/oppia/android/util/parser/html/LiTagHandlerTest.kt
index 13246b7d2d8..3a2f2085326 100644
--- a/utility/src/test/java/org/oppia/android/util/parser/html/LiTagHandlerTest.kt
+++ b/utility/src/test/java/org/oppia/android/util/parser/html/LiTagHandlerTest.kt
@@ -165,57 +165,6 @@ class LiTagHandlerTest {
.hasLength(4)
}
- @Test
- fun testGetContentDescription_handlesNestedOrderedList() {
- val displayLocale = createDisplayLocaleImpl(US_ENGLISH_CONTEXT)
- val htmlString = "You should know the following before going on:
" +
- "The counting numbers (1, 2, 3, 4, 5 ….)" +
- "How to tell whether one counting number is bigger or " +
- "smaller than another Item 1 Item 2" +
- ""
- val liTaghandler = LiTagHandler(context, displayLocale)
- val contentDescription =
- CustomHtmlContentHandler.getContentDescription(
- html = htmlString,
- imageRetriever = mockImageRetriever,
- customTagHandlers = mapOf(
- CUSTOM_LIST_LI_TAG to liTaghandler,
- CUSTOM_LIST_OL_TAG to liTaghandler
- )
- )
- assertThat(contentDescription).isEqualTo(
- "You should know the following before going on:\n" +
- "The counting numbers (1, 2, 3, 4, 5 ….)\n" +
- "How to tell whether one counting number is bigger or smaller than another \n" +
- "Item 1 \n" +
- "Item 2"
- )
- }
-
- @Test
- fun testGetContentDescription_handlesSimpleUnorderedList() {
- val displayLocale = createDisplayLocaleImpl(US_ENGLISH_CONTEXT)
- val htmlString = "You should know the following before going on:
" +
- "The counting numbers (1, 2, 3, 4, 5 ….)" +
- "How to tell whether one counting number is bigger or " +
- "smaller than another
"
- val liTaghandler = LiTagHandler(context, displayLocale)
- val contentDescription =
- CustomHtmlContentHandler.getContentDescription(
- html = htmlString,
- imageRetriever = mockImageRetriever,
- customTagHandlers = mapOf(
- CUSTOM_LIST_LI_TAG to liTaghandler,
- CUSTOM_LIST_OL_TAG to liTaghandler
- )
- )
- assertThat(contentDescription).isEqualTo(
- "You should know the following before going on:\n" +
- "The counting numbers (1, 2, 3, 4, 5 ….)\n" +
- "How to tell whether one counting number is bigger or smaller than another"
- )
- }
-
private fun createDisplayLocaleImpl(context: OppiaLocaleContext): DisplayLocaleImpl {
val formattingLocale = androidLocaleFactory.createOneOffAndroidLocale(context)
return DisplayLocaleImpl(context, formattingLocale, machineLocale, formatterFactory)
From d7eaad8ef8ddff8f7aa7c88a7c33212bca927c3d Mon Sep 17 00:00:00 2001
From: Manas <119405883+manas-yu@users.noreply.github.com>
Date: Tue, 18 Feb 2025 07:06:45 +0530
Subject: [PATCH 2/8] Fix #3708: Content Description Generation For
ImageRegionSelectionInteraction (#5691)
## Explanation
Fix #3708
This PR adds logic for dynamic content descriptions in
**ImageRegionSelectionInteraction**, i.e. generating distinct
descriptions for selected and not selected regions. For example:
- **Selected:** "Image showing Region 3."
- **Not Selected:** "Select Region 3 image."
## Before


## After


## Essential Checklist
- [x] The PR title and explanation each start with "Fix #bugnum: " (If
this PR fixes part of an issue, prefix the title with "Fix part of
#bugnum: ...".)
- [x] Any changes to
[scripts/assets](https://github.com/oppia/oppia-android/tree/develop/scripts/assets)
files have their rationale included in the PR explanation.
- [x] The PR follows the [style
guide](https://github.com/oppia/oppia-android/wiki/Coding-style-guide).
- [x] The PR does not contain any unnecessary code changes from Android
Studio
([reference](https://github.com/oppia/oppia-android/wiki/Guidance-on-submitting-a-PR#undo-unnecessary-changes)).
- [x] The PR is made from a branch that's **not** called "develop" and
is up-to-date with "develop".
- [x] The PR is **assigned** to the appropriate reviewers
([reference](https://github.com/oppia/oppia-android/wiki/Guidance-on-submitting-a-PR#clarification-regarding-assignees-and-reviewers-section)).
---
.../ImageRegionSelectionInteractionView.kt | 12 ++--
.../app/utility/ClickableAreasImage.kt | 64 ++++++++++++++++---
app/src/main/res/values/strings.xml | 2 +
...ImageRegionSelectionInteractionViewTest.kt | 27 ++++++--
4 files changed, 85 insertions(+), 20 deletions(-)
diff --git a/app/src/main/java/org/oppia/android/app/player/state/ImageRegionSelectionInteractionView.kt b/app/src/main/java/org/oppia/android/app/player/state/ImageRegionSelectionInteractionView.kt
index e7f2dbe3e2e..a113976602f 100644
--- a/app/src/main/java/org/oppia/android/app/player/state/ImageRegionSelectionInteractionView.kt
+++ b/app/src/main/java/org/oppia/android/app/player/state/ImageRegionSelectionInteractionView.kt
@@ -11,6 +11,7 @@ import androidx.fragment.app.FragmentManager
import org.oppia.android.app.model.ImageWithRegions
import org.oppia.android.app.model.UserAnswerState
import org.oppia.android.app.shim.ViewBindingShim
+import org.oppia.android.app.translation.AppLanguageResourceHandler
import org.oppia.android.app.utility.ClickableAreasImage
import org.oppia.android.app.utility.OnClickableAreaClickedListener
import org.oppia.android.app.view.ViewComponentFactory
@@ -46,6 +47,7 @@ class ImageRegionSelectionInteractionView @JvmOverloads constructor(
@Inject lateinit var machineLocale: OppiaLocale.MachineLocale
@Inject lateinit var accessibilityService: AccessibilityService
@Inject lateinit var imageLoader: ImageLoader
+ @Inject lateinit var resourceHandler: AppLanguageResourceHandler
private lateinit var entityId: String
private lateinit var overlayView: FrameLayout
@@ -64,8 +66,8 @@ class ImageRegionSelectionInteractionView @JvmOverloads constructor(
maybeInitializeClickableAreas()
}
- fun setUserAnswerState(userAnswerrState: UserAnswerState) {
- this.userAnswerState = userAnswerrState
+ fun setUserAnswerState(userAnswerState: UserAnswerState) {
+ this.userAnswerState = userAnswerState
}
fun setEntityId(entityId: String) {
@@ -118,7 +120,8 @@ class ImageRegionSelectionInteractionView @JvmOverloads constructor(
::entityId.isInitialized &&
::imageUrl.isInitialized &&
::onRegionClicked.isInitialized &&
- ::overlayView.isInitialized
+ ::overlayView.isInitialized &&
+ ::resourceHandler.isInitialized
) {
loadImage()
@@ -129,7 +132,8 @@ class ImageRegionSelectionInteractionView @JvmOverloads constructor(
bindingInterface,
isAccessibilityEnabled = accessibilityService.isScreenReaderEnabled(),
clickableAreas,
- userAnswerState
+ userAnswerState,
+ resourceHandler
)
areasImage.addRegionViews()
performAttachment(areasImage)
diff --git a/app/src/main/java/org/oppia/android/app/utility/ClickableAreasImage.kt b/app/src/main/java/org/oppia/android/app/utility/ClickableAreasImage.kt
index fa77fa271de..dd57c27f29a 100644
--- a/app/src/main/java/org/oppia/android/app/utility/ClickableAreasImage.kt
+++ b/app/src/main/java/org/oppia/android/app/utility/ClickableAreasImage.kt
@@ -13,6 +13,7 @@ import org.oppia.android.app.model.ImageWithRegions.LabeledRegion
import org.oppia.android.app.model.UserAnswerState
import org.oppia.android.app.player.state.ImageRegionSelectionInteractionView
import org.oppia.android.app.shim.ViewBindingShim
+import org.oppia.android.app.translation.AppLanguageResourceHandler
import kotlin.math.roundToInt
/** Helper class to handle clicks on an image along with highlighting the selected region. */
@@ -23,7 +24,8 @@ class ClickableAreasImage(
bindingInterface: ViewBindingShim,
private val isAccessibilityEnabled: Boolean,
private val clickableAreas: List,
- userAnswerState: UserAnswerState
+ userAnswerState: UserAnswerState,
+ private val resourceHandler: AppLanguageResourceHandler
) {
private var imageLabel: String? = null
private val defaultRegionView by lazy { bindingInterface.getDefaultRegion(parentView) }
@@ -60,6 +62,16 @@ class ClickableAreasImage(
// Remove any previously selected region excluding 0th index(image view)
if (index > 0) {
childView.setBackgroundResource(0)
+ if (childView.tag != null) {
+ val regionLabel = childView.tag as String
+ clickableAreas.find { it.label == regionLabel }?.let { clickableArea ->
+ updateRegionContentDescription(
+ childView,
+ clickableArea,
+ regionLabel == imageLabel
+ )
+ }
+ }
}
}
}
@@ -112,8 +124,13 @@ class ClickableAreasImage(
newView.isFocusable = true
newView.isFocusableInTouchMode = true
newView.tag = clickableArea.label
+
+ val isInitiallySelected = clickableArea.label.equals(imageLabel)
+ updateRegionContentDescription(newView, clickableArea, isInitiallySelected)
+
newView.initializeToggleRegionTouchListener(clickableArea)
- if (clickableArea.label.equals(imageLabel)) {
+
+ if (isInitiallySelected) {
showOrHideRegion(newView = newView, clickableArea = clickableArea)
}
if (isAccessibilityEnabled) {
@@ -123,7 +140,7 @@ class ClickableAreasImage(
showOrHideRegion(newView, clickableArea)
}
}
- newView.contentDescription = clickableArea.contentDescription
+
parentView.addView(newView)
}
@@ -143,14 +160,18 @@ class ClickableAreasImage(
}
private fun showOrHideRegion(newView: View, clickableArea: LabeledRegion) {
- resetRegionSelectionViews()
- listener.onClickableAreaTouched(
- NamedRegionClickedEvent(
- clickableArea.label,
- clickableArea.contentDescription
+ if (clickableArea.label != imageLabel) {
+ imageLabel = clickableArea.label
+ resetRegionSelectionViews()
+ newView.setBackgroundResource(R.drawable.selected_region_background)
+
+ listener.onClickableAreaTouched(
+ NamedRegionClickedEvent(
+ clickableArea.label,
+ generateContentDescription(clickableArea, true)
+ )
)
- )
- newView.setBackgroundResource(R.drawable.selected_region_background)
+ }
}
private fun View.initializeShowRegionTouchListener() {
@@ -172,4 +193,27 @@ class ClickableAreasImage(
return@setOnTouchListener true
}
}
+
+ private fun generateContentDescription(
+ clickableArea: LabeledRegion,
+ isSelected: Boolean
+ ): String = if (isSelected) {
+ resourceHandler.getStringInLocaleWithWrapping(
+ R.string.selected_image_region_selection_content_description,
+ clickableArea.label
+ )
+ } else {
+ resourceHandler.getStringInLocaleWithWrapping(
+ R.string.unselected_image_region_selection_content_description,
+ clickableArea.label
+ )
+ }
+
+ private fun updateRegionContentDescription(
+ view: View,
+ clickableArea: LabeledRegion,
+ isSelected: Boolean
+ ) {
+ view.contentDescription = generateContentDescription(clickableArea, isSelected)
+ }
}
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 4d6299de34d..4eb884b7ab1 100755
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -191,6 +191,8 @@
Correct!
Topic: %s
%1$s %2$s!
+ Image showing %1$s.
+ Select %1$s image.
- 1 Chapter
- %s Chapters
diff --git a/app/src/sharedTest/java/org/oppia/android/app/testing/ImageRegionSelectionInteractionViewTest.kt b/app/src/sharedTest/java/org/oppia/android/app/testing/ImageRegionSelectionInteractionViewTest.kt
index 6b558212760..9690ec629d4 100644
--- a/app/src/sharedTest/java/org/oppia/android/app/testing/ImageRegionSelectionInteractionViewTest.kt
+++ b/app/src/sharedTest/java/org/oppia/android/app/testing/ImageRegionSelectionInteractionViewTest.kt
@@ -11,6 +11,7 @@ import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.isEnabled
+import androidx.test.espresso.matcher.ViewMatchers.withContentDescription
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withTagValue
import androidx.test.espresso.matcher.ViewMatchers.withText
@@ -177,7 +178,7 @@ class ImageRegionSelectionInteractionViewTest {
assertThat(regionClickedEvent.value)
.isEqualTo(
NamedRegionClickedEvent(
- regionLabel = "Region 3", contentDescription = "You have selected Region 3"
+ regionLabel = "Region 3", contentDescription = "Image showing Region 3."
)
)
}
@@ -218,12 +219,26 @@ class ImageRegionSelectionInteractionViewTest {
assertThat(regionClickedEvent.value)
.isEqualTo(
NamedRegionClickedEvent(
- regionLabel = "Region 2", contentDescription = "You have selected Region 2"
+ regionLabel = "Region 2", contentDescription = "Image showing Region 2."
)
)
}
}
+ @Test
+ // TODO(#1611): Fix ImageRegionSelectionInteractionViewTest
+ @RunOn(TestPlatform.ESPRESSO)
+ fun testImageRegionSelectionInteractionView_initialContentDescriptionRegion3_isCorrect() {
+ launch(ImageRegionSelectionTestActivity::class.java).use {
+ onView(
+ allOf(
+ withTagValue(`is`("Region 3")),
+ withContentDescription("Select Region 3 image.")
+ )
+ ).check(matches(isDisplayed()))
+ }
+ }
+
@Test
// TODO(#1611): Fix ImageRegionSelectionInteractionViewTest
@RunOn(TestPlatform.ESPRESSO)
@@ -280,7 +295,7 @@ class ImageRegionSelectionInteractionViewTest {
assertThat(regionClickedEvent.value)
.isEqualTo(
NamedRegionClickedEvent(
- regionLabel = "Region 2", contentDescription = "You have selected Region 2"
+ regionLabel = "Region 2", contentDescription = "Image showing Region 2."
)
)
}
@@ -308,7 +323,7 @@ class ImageRegionSelectionInteractionViewTest {
assertThat(regionClickedEvent.value)
.isEqualTo(
NamedRegionClickedEvent(
- regionLabel = "Region 3", contentDescription = "You have selected Region 3"
+ regionLabel = "Region 3", contentDescription = "Image showing Region 3."
)
)
}
@@ -352,7 +367,7 @@ class ImageRegionSelectionInteractionViewTest {
assertThat(regionClickedEvent.value)
.isEqualTo(
NamedRegionClickedEvent(
- regionLabel = "Region 3", contentDescription = "You have selected Region 3"
+ regionLabel = "Region 3", contentDescription = "Image showing Region 3."
)
)
}
@@ -394,7 +409,7 @@ class ImageRegionSelectionInteractionViewTest {
assertThat(regionClickedEvent.value)
.isEqualTo(
NamedRegionClickedEvent(
- regionLabel = "Region 2", contentDescription = "You have selected Region 2"
+ regionLabel = "Region 2", contentDescription = "Image showing Region 2."
)
)
}
From 9ce673f5ffea73c28e4f1dbd75e13114a2cbf1bd Mon Sep 17 00:00:00 2001
From: Ben Henning
Date: Tue, 18 Feb 2025 21:50:21 -0800
Subject: [PATCH 3/8] Fix #5660: Ensure mobile-install works (#5664)
## Explanation
Fixes #5660
I've verified with local testing that ``bazel mobile-install`` works
with the APK generated for each AAB target (it ends in ``_binary``, e.g.
``oppia_dev_binary``). Thus, documentation has been updated to recommend
this as the main way to install the local development version of the app
as ``mobile-install`` provides substantial performance benefits (see
https://bazel.build/docs/mobile-install).
The ``install_*`` targets (e.g. ``install_oppia_dev``) have been removed
since ``mobile-install`` should be used, instead. The existing Starlark
rule & macro has been repurposed to generating a universal APK which, in
turn, will be useful for specific situations where ``mobile-install``
breaks down or is not used (such as for emulator tests).
## Essential Checklist
- [x] The PR title and explanation each start with "Fix #bugnum: " (If
this PR fixes part of an issue, prefix the title with "Fix part of
#bugnum: ...".)
- [x] Any changes to
[scripts/assets](https://github.com/oppia/oppia-android/tree/develop/scripts/assets)
files have their rationale included in the PR explanation.
- [x] The PR follows the [style
guide](https://github.com/oppia/oppia-android/wiki/Coding-style-guide).
- [x] The PR does not contain any unnecessary code changes from Android
Studio
([reference](https://github.com/oppia/oppia-android/wiki/Guidance-on-submitting-a-PR#undo-unnecessary-changes)).
- [x] The PR is made from a branch that's **not** called "develop" and
is up-to-date with "develop".
- [x] The PR is **assigned** to the appropriate reviewers
([reference](https://github.com/oppia/oppia-android/wiki/Guidance-on-submitting-a-PR#clarification-regarding-assignees-and-reviewers-section)).
## For UI-specific PRs only
N/A -- this changes nothing about the build of the app, only
installation pathways.
---
.bazelrc | 3 +
BUILD.bazel | 6 +-
build_flavors.bzl | 8 +-
oppia_android_application.bzl | 106 +++++++++++----------
wiki/Bazel-Setup-Instructions-for-Linux.md | 31 +++++-
wiki/Bazel-Setup-Instructions-for-Mac.md | 31 +++++-
6 files changed, 127 insertions(+), 58 deletions(-)
diff --git a/.bazelrc b/.bazelrc
index 5ca45844311..a12cb1f82e4 100644
--- a/.bazelrc
+++ b/.bazelrc
@@ -17,3 +17,6 @@ build:ignore_build_warnings --//tools/kotlin:warn_mode=warning
# Show all test output by default (for better debugging).
test --test_output=all
+
+# Always start the app by default when using mobile-install.
+mobile-install --start_app
diff --git a/BUILD.bazel b/BUILD.bazel
index 4c6d5a97c2a..0563e369bd5 100644
--- a/BUILD.bazel
+++ b/BUILD.bazel
@@ -131,9 +131,9 @@ package_group(
]
]
-# Define all binary flavors that can be built. Note that these are AABs, not APKs, and can be
-# be installed on a local device or emulator using a 'bazel run' command like so:
-# bazel run //:install_oppia_dev
+# Define all binary flavors that can be built. Note that these are AABs and thus cannot be installed
+# directly. However, their binaries can be installed using mobile-install like so:
+# bazel mobile-install //:oppia_dev_binary
[
define_oppia_aab_binary_flavor(flavor = flavor)
for flavor in AVAILABLE_FLAVORS
diff --git a/build_flavors.bzl b/build_flavors.bzl
index aa5493a391e..b0ed8e2169c 100644
--- a/build_flavors.bzl
+++ b/build_flavors.bzl
@@ -2,7 +2,7 @@
Macros & definitions corresponding to Oppia binary build flavors.
"""
-load("//:oppia_android_application.bzl", "declare_deployable_application", "oppia_android_application")
+load("//:oppia_android_application.bzl", "generate_universal_apk", "oppia_android_application")
load("//:version.bzl", "MAJOR_VERSION", "MINOR_VERSION", "OPPIA_ALPHA_KITKAT_VERSION_CODE", "OPPIA_ALPHA_VERSION_CODE", "OPPIA_BETA_VERSION_CODE", "OPPIA_DEV_KITKAT_VERSION_CODE", "OPPIA_DEV_VERSION_CODE", "OPPIA_GA_VERSION_CODE")
# Defines the list of flavors available to build the Oppia app in. Note to developers: this list
@@ -242,7 +242,7 @@ def define_oppia_aab_binary_flavor(flavor):
This will define two targets:
- //:oppia_ (the AAB)
- - //:install_oppia_ (the installable binary target--see declare_deployable_application
+ - //:oppia__universal_apk (the installable binary target--see generate_universal_apk
for details)
Args:
@@ -278,7 +278,7 @@ def define_oppia_aab_binary_flavor(flavor):
shrink_resources = True if len(_FLAVOR_METADATA[flavor]["proguard_specs"]) != 0 else False,
deps = _FLAVOR_METADATA[flavor]["deps"],
)
- declare_deployable_application(
- name = "install_oppia_%s" % flavor,
+ generate_universal_apk(
+ name = "oppia_%s_universal_apk" % flavor,
aab_target = ":oppia_%s" % flavor,
)
diff --git a/oppia_android_application.bzl b/oppia_android_application.bzl
index d9951cefa18..35e07feca65 100644
--- a/oppia_android_application.bzl
+++ b/oppia_android_application.bzl
@@ -158,61 +158,57 @@ def _package_metadata_into_deployable_aab_impl(ctx):
runfiles = ctx.runfiles(files = [output_aab_file]),
)
-def _generate_apks_and_install_impl(ctx):
- input_file = ctx.attr.input_file.files.to_list()[0]
+def _generate_universal_apk_impl(ctx):
+ input_aab_file = ctx.attr.input_aab_file.files.to_list()[0]
+ output_apk_file = ctx.outputs.output_apk_file
debug_keystore_file = ctx.attr.debug_keystore.files.to_list()[0]
apks_file = ctx.actions.declare_file("%s_processed.apks" % ctx.label.name)
- deploy_shell = ctx.actions.declare_file("%s_run.sh" % ctx.label.name)
- # Reference: https://developer.android.com/studio/command-line/bundletool#generate_apks.
+ # Reference: https://developer.android.com/tools/bundletool#generate_apks.
# See also the Bazel BUILD file for the keystore for details on its password and alias.
- generate_apks_arguments = [
+ generate_universal_apk_arguments = [
"build-apks",
- "--bundle=%s" % input_file.path,
+ "--bundle=%s" % input_aab_file.path,
"--output=%s" % apks_file.path,
"--ks=%s" % debug_keystore_file.path,
"--ks-pass=pass:android",
"--ks-key-alias=androiddebugkey",
"--key-pass=pass:android",
+ "--mode=universal",
]
+ # bundletool only generates an APKs file, so the universal APK still needs to be extracted.
+
# Reference: https://docs.bazel.build/versions/master/skylark/lib/actions.html#run.
ctx.actions.run(
outputs = [apks_file],
- inputs = ctx.files.input_file + ctx.files.debug_keystore,
+ inputs = ctx.files.input_aab_file + ctx.files.debug_keystore,
tools = [ctx.executable._bundletool_tool],
executable = ctx.executable._bundletool_tool.path,
- arguments = generate_apks_arguments,
- mnemonic = "BuildApksFromDeployAab",
- progress_message = "Preparing AAB deploy to device",
+ arguments = generate_universal_apk_arguments,
+ mnemonic = "GenerateUniversalAPK",
+ progress_message = "Generating universal APK from AAB",
)
- # References: https://github.com/bazelbuild/bazel/issues/7390,
- # https://developer.android.com/studio/command-line/bundletool#deploy_with_bundletool, and
- # https://docs.bazel.build/versions/main/skylark/rules.html#executable-rules-and-test-rules.
- # Note that the bundletool can be executed directly since Bazel creates a wrapper script that
- # utilizes its own internal Java toolchain.
- ctx.actions.write(
- output = deploy_shell,
- content = """
- #!/bin/sh
- {0} install-apks --apks={1}
- echo The APK should now be installed
- """.format(ctx.executable._bundletool_tool.short_path, apks_file.short_path),
- is_executable = True,
+ command = """
+ # Extract APK to working directory.
+ unzip -q "$(pwd)/{0}" universal.apk
+ mv universal.apk "$(pwd)/{1}"
+ """.format(apks_file.path, output_apk_file.path)
+
+ # Reference: https://docs.bazel.build/versions/main/skylark/lib/actions.html#run_shell.
+ ctx.actions.run_shell(
+ outputs = [output_apk_file],
+ inputs = [apks_file],
+ tools = [],
+ command = command,
+ mnemonic = "ExtractUniversalAPK",
+ progress_message = "Extracting universal APK from .apks file",
)
- # Reference for including necessary runfiles for Java:
- # https://github.com/bazelbuild/bazel/issues/487#issuecomment-178119424.
- runfiles = ctx.runfiles(
- files = [
- ctx.executable._bundletool_tool,
- apks_file,
- ],
- ).merge(ctx.attr._bundletool_tool.default_runfiles)
return DefaultInfo(
- executable = deploy_shell,
- runfiles = runfiles,
+ files = depset([output_apk_file]),
+ runfiles = ctx.runfiles(files = [output_apk_file]),
)
_convert_apk_to_module_aab = rule(
@@ -303,10 +299,13 @@ _package_metadata_into_deployable_aab = rule(
implementation = _package_metadata_into_deployable_aab_impl,
)
-_generate_apks_and_install = rule(
+_generate_universal_apk = rule(
attrs = {
- "input_file": attr.label(
- allow_single_file = True,
+ "input_aab_file": attr.label(
+ allow_single_file = [".aab"],
+ mandatory = True,
+ ),
+ "output_apk_file": attr.output(
mandatory = True,
),
"debug_keystore": attr.label(
@@ -319,14 +318,19 @@ _generate_apks_and_install = rule(
default = "//third_party:android_bundletool_binary",
),
},
- executable = True,
- implementation = _generate_apks_and_install_impl,
+ implementation = _generate_universal_apk_impl,
)
def oppia_android_application(name, config_file, proguard_generate_mapping, **kwargs):
"""
Creates an Android App Bundle (AAB) binary with the specified name and arguments.
+ This generates a mobile-installable target that ends in '_binary'. For example, if there's an
+ Oppia Android application defined with the name 'oppia_dev' then its APK binary can be
+ mobile-installed using:
+
+ bazel mobile-install //:oppia_dev_binary
+
Args:
name: str. The name of the Android App Bundle to build. This will corresponding to the name
of the generated .aab file.
@@ -390,30 +394,34 @@ def oppia_android_application(name, config_file, proguard_generate_mapping, **kw
tags = ["manual"],
)
-def declare_deployable_application(name, aab_target):
+def generate_universal_apk(name, aab_target):
"""
- Creates a new target that can be run with 'bazel run' to install an AAB file.
+ Creates a new 'bazel mobile-install'-able universal APK target for the provided AAB target.
- Example:
- declare_deployable_application(
- name = "install_oppia_prod",
+ Example usage in a top-level BUILD.bazel file and CLI:
+ generate_universal_apk(
+ name = "oppia_prod_universal_apk",
aab_target = "//:oppia_prod",
)
- $ bazel run //:install_oppia_prod
+ $ bazel mobile-install //:oppia_prod_universal_apk
+
+ Note that, sometimes, you may not want to use mobile-install such as for production builds that
+ may have functional disparity from incremental installations of the app. In those cases, it's
+ best to uninstall the app from the target device and install the APK directly using
+ 'adb install' as so (per the above example):
- This will build (if necessary) and install the correct APK derived from the Android app bundle
- on the locally attached emulator or device. Note that this command does not support targeting a
- specific device so it will not work if more than one device is available via 'adb devices'.
+ $ adb install bazel-bin/oppia_prod_universal_apk.apk
Args:
name: str. The name of the runnable target to install an AAB file on a local device.
aab_target: target. The target (declared via oppia_android_application) that should be made
installable.
"""
- _generate_apks_and_install(
+ _generate_universal_apk(
name = name,
- input_file = aab_target,
+ input_aab_file = aab_target,
+ output_apk_file = "%s.apk" % name,
debug_keystore = "@bazel_tools//tools/android:debug_keystore",
tags = ["manual"],
)
diff --git a/wiki/Bazel-Setup-Instructions-for-Linux.md b/wiki/Bazel-Setup-Instructions-for-Linux.md
index 6410ca034b6..f0dceb51c9f 100644
--- a/wiki/Bazel-Setup-Instructions-for-Linux.md
+++ b/wiki/Bazel-Setup-Instructions-for-Linux.md
@@ -42,5 +42,34 @@ INFO: Build completed successfully, ...
Note also that the ``oppia_dev.aab`` under the ``bazel-bin`` directory of your local copy of Oppia Android should be a fully functioning development version of the app that can be installed using bundle-tool. However, it's recommended to deploy Oppia to an emulator or connected device using the following Bazel command:
```sh
-bazel run //:install_oppia_dev
+bazel mobile-install //:oppia_dev_binary
+```
+
+``mobile-install`` is much faster for local development (especially for the developer flavor of the app) because it does more sophisticated dex regeneration detection for faster incremental installs. See https://bazel.build/docs/mobile-install for details.
+
+**Note**: If you run into a failure like the following when trying to use `mobile-install` to a device running SDK 34 or newer:
+
+```
+FATAL EXCEPTION: main
+ Process: org.oppia.android, PID: 9508
+ java.lang.RuntimeException: Unable to instantiate application com.google.devtools.build.android.incrementaldeployment.StubApplication package org.oppia.android: java.lang.SecurityException: Writable dex file '/data/local/tmp/incrementaldeployment/org.oppia.android/dex/incremental_classes4.dex' is not allowed.
+ at android.app.LoadedApk.makeApplicationInner(LoadedApk.java:1466)
+ at android.app.LoadedApk.makeApplicationInner(LoadedApk.java:1395)
+ at android.app.ActivityThread.handleBindApplication(ActivityThread.java:6959)
+ at android.app.ActivityThread.-$$Nest$mhandleBindApplication(Unknown Source:0)
+ at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2236)
+ at android.os.Handler.dispatchMessage(Handler.java:106)
+ at android.os.Looper.loopOnce(Looper.java:205)
+ at android.os.Looper.loop(Looper.java:294)
+ at android.app.ActivityThread.main(ActivityThread.java:8177)
+ at java.lang.reflect.Method.invoke(Native Method)
+ at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:552)
+ at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:971)
+ Caused by: java.lang.SecurityException: Writable dex file '/data/local/tmp/incrementaldeployment/org.oppia.android/dex/incremental_classes4.dex' is not allowed.
+```
+
+Then you will need to use `adb install` directly:
+
+```sh
+adb install bazel-bin/oppia_dev_binary.apk
```
diff --git a/wiki/Bazel-Setup-Instructions-for-Mac.md b/wiki/Bazel-Setup-Instructions-for-Mac.md
index f7b3b2e7dfd..eaf97c4f39e 100644
--- a/wiki/Bazel-Setup-Instructions-for-Mac.md
+++ b/wiki/Bazel-Setup-Instructions-for-Mac.md
@@ -70,5 +70,34 @@ INFO: Build completed successfully, ...
Note also that the ``oppia_dev.aab`` under the ``bazel-bin`` directory of your local copy of Oppia Android should be a fully functioning development version of the app that can be installed using bundle-tool. However, it's recommended to deploy Oppia to an emulator or connected device using the following Bazel command:
```sh
-bazel run //:install_oppia_dev
+bazel mobile-install //:oppia_dev_binary
+```
+
+``mobile-install`` is much faster for local development (especially for the developer flavor of the app) because it does more sophisticated dex regeneration detection for faster incremental installs. See https://bazel.build/docs/mobile-install for details.
+
+**Note**: If you run into a failure like the following when trying to use `mobile-install` to a device running SDK 34 or newer:
+
+```
+FATAL EXCEPTION: main
+ Process: org.oppia.android, PID: 9508
+ java.lang.RuntimeException: Unable to instantiate application com.google.devtools.build.android.incrementaldeployment.StubApplication package org.oppia.android: java.lang.SecurityException: Writable dex file '/data/local/tmp/incrementaldeployment/org.oppia.android/dex/incremental_classes4.dex' is not allowed.
+ at android.app.LoadedApk.makeApplicationInner(LoadedApk.java:1466)
+ at android.app.LoadedApk.makeApplicationInner(LoadedApk.java:1395)
+ at android.app.ActivityThread.handleBindApplication(ActivityThread.java:6959)
+ at android.app.ActivityThread.-$$Nest$mhandleBindApplication(Unknown Source:0)
+ at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2236)
+ at android.os.Handler.dispatchMessage(Handler.java:106)
+ at android.os.Looper.loopOnce(Looper.java:205)
+ at android.os.Looper.loop(Looper.java:294)
+ at android.app.ActivityThread.main(ActivityThread.java:8177)
+ at java.lang.reflect.Method.invoke(Native Method)
+ at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:552)
+ at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:971)
+ Caused by: java.lang.SecurityException: Writable dex file '/data/local/tmp/incrementaldeployment/org.oppia.android/dex/incremental_classes4.dex' is not allowed.
+```
+
+Then you will need to use `adb install` directly:
+
+```sh
+adb install bazel-bin/oppia_dev_binary.apk
```
From bcaec12a14ba2c6a769d673e7688fab12bca52fd Mon Sep 17 00:00:00 2001
From: Tanish Moral <134790673+TanishMoral11@users.noreply.github.com>
Date: Thu, 20 Feb 2025 05:02:46 +0530
Subject: [PATCH 4/8] Fix part of #3000 : TODO Integrate Buildifier Linter into
Pre-push Checks (#5674)
## Explanation
Fixes part of #3000
This PR integrates the Buildifier linter into the pre-push checks to
ensure proper formatting of Bazel build files. The addition of
`buildifier_lint_check.sh` ensures that any changes to build files are
automatically checked for proper formatting before being pushed. This
helps maintain consistency and quality in the project's build files.
- The `buildifier_lint_check.sh` script is added to the pre-push checks.
- The script is executed along with other existing checks, such as
`ktlint`, `checkstyle`, and `buf`, ensuring that all code formatting
checks are run in sequence before a commit is pushed.
- If any of the checks fail, the push is blocked until the issues are
resolved.
This change enhances the automated code quality checks and reduces the
chances of improperly formatted Bazel files being committed to the
repository.
## Essential Checklist
- [x] The PR title and explanation each start with "Fix #bugnum: " (If
this PR fixes part of an issue, prefix the title with "Fix part of
#bugnum: ...".)
- [x] Any changes to
[scripts/assets](https://github.com/oppia/oppia-android/tree/develop/scripts/assets)
files have their rationale included in the PR explanation.
- [x] The PR follows the [style
guide](https://github.com/oppia/oppia-android/wiki/Coding-style-guide).
- [x] The PR does not contain any unnecessary code changes from Android
Studio
([reference](https://github.com/oppia/oppia-android/wiki/Guidance-on-submitting-a-PR#undo-unnecessary-changes)).
- [x] The PR is made from a branch that's **not** called "develop" and
is up-to-date with "develop".
- [x] The PR is **assigned** to the appropriate reviewers
([reference](https://github.com/oppia/oppia-android/wiki/Guidance-on-submitting-a-PR#clarification-regarding-assignees-and-reviewers-section)).
---------
Co-authored-by: Adhiambo Peres <59600948+adhiamboperes@users.noreply.github.com>
---
scripts/pre-push.sh | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/scripts/pre-push.sh b/scripts/pre-push.sh
index ea2878926de..4a927e5cd93 100755
--- a/scripts/pre-push.sh
+++ b/scripts/pre-push.sh
@@ -6,14 +6,14 @@ source scripts/formatting.sh
# - ktlint
# - checkstyle
# - buf
+# - buildifier
# - (others in the future)
-if bash scripts/ktlint_lint_check.sh && bash scripts/checkstyle_lint_check.sh && bash scripts/buf_lint_check.sh ; then
+if bash scripts/ktlint_lint_check.sh && bash scripts/checkstyle_lint_check.sh && bash scripts/buf_lint_check.sh && bash scripts/buildifier_lint_check.sh; then
echo_success "All checks passed successfully"
exit 0
else
exit 1
fi
-# TODO(#3000): Add Bazel Linter to the project
# TODO(#970): Add XML Linter to the project
From 6e349bc9f21a685861214c6164adbc1af78f30fa Mon Sep 17 00:00:00 2001
From: Ben Henning
Date: Wed, 19 Feb 2025 16:35:35 -0800
Subject: [PATCH 5/8] Fix #5012: Remove KitKat support (#5630)
## Explanation
Fixes #5012
Removes references and code corresponding to KitKat (SDK version 19)
since the app has been set to a minimum version of Lollipop (SDK 21)
since #3910 (roughly--technically KitKat was still supported but we no
longer shipped a KitKat version). Because of this, the changes in this
PR are largely a no-op from a build perspective.
More broadly, this change is motivated by a desire to decrease CI time
(which was already reduce considerably in the recent merge of #5629) by
removing extraneous app builds being done in CI:
- ``//:oppia`` since we should really only use ``//:oppia_dev`` moving
forward.
- Dev and alpha KitKat builds (the latter of which takes a long time
since only the alpha job was building 2 proguarded builds of the app and
thus taking much more time than the beta and GA build jobs).
Separately, ``build_tests.yml`` and ``unit_tests.yml`` were updated to
no longer support caching since we disabled this behavior a while back
(#2884) and are unlikely to reintroduce it due to high storage costs.
Some other noteworthy changes:
- The optional providing of ``Retrofit`` and Retrofit services has been
reverted since it's no longer necessary (and corresponding tests have
been removed since there's no longer any condition to verify).
- Code that was gated to run below Lollipop has been removed since it
can't execute except on KitKat devices.
- Support for a main dex target list has been removed as we're unlikely
to need it with native multidex support (which wasn't an option for
KitKat builds), though we may choose to reintroduce it to speed up cold
app starts since it _can potentially_ help improve time-to-splash app
startup performance.
- Updated manifest min SDK values to avoid potential developer confusion
on the min SDK version supported (though, strictly speaking, this
doesn't technically matter with the way the app builds).
- This updates the default min version supported for OS deprecation. No
additional work is needed to actually activate the deprecation notice on
the KitKat version of the app since we no longer deploy it, and OS
deprecation wasn't added to the last version that was deployed.
- Three file content pattern failure messages were updated to no longer
reference KitKat (though the value of these checks mostly still seems
realized, so I think it's fine to keep them in).
- Some additional licenses were added due to tooling around the dev AAB
(even though these aren't dependencies that ship with the app). I think
it's fine including extra licenses in this case.
- The protobuf license was corrected to BSD 3-Clause as declared for the
dependency and in the GitHub repository for the dependencies.
- This bumps version codes due to removing two flavors (for KitKat dev
and KitKat alpha).
- ``NetworkModuleTest`` has had its tests redesigned and better filled
in in order to pass the code coverage minimum requirement (since old
tests were removed). Note that it's not quite perfect in what it's
testing, but it is at least verifying the complete configuration of the
``Retrofit`` instance, and the singleton properties of the two services
currently supported (verifying that the services are set up would
require a lot more testing that seems outside the direct scope of this
test, so it seems okay to ignore here).
- As part of the previous item's work, ``NetworkLoggingInterceptor`` had
an issue fixed where it could cause an OkHttp exception to be thrown
when trying to process logs (due to reading the response body twice).
Reverting this change will now cause breakage failures in
``NetworkModuleTest``.
- ``NetworkModule``'s initialization order was changed for slightly
better network request logging (see the new comment in that file for
more context).
Note that there are some workflow job removals and renames in this PR.
Any required CI checks will be correspondingly updated once this PR is
approved and ready to merge (to avoid blocking any other in-flight PRs).
## Essential Checklist
- [x] The PR title and explanation each start with "Fix #bugnum: " (If
this PR fixes part of an issue, prefix the title with "Fix part of
#bugnum: ...".)
- [x] Any changes to
[scripts/assets](https://github.com/oppia/oppia-android/tree/develop/scripts/assets)
files have their rationale included in the PR explanation.
- [x] The PR follows the [style
guide](https://github.com/oppia/oppia-android/wiki/Coding-style-guide).
- [x] The PR does not contain any unnecessary code changes from Android
Studio
([reference](https://github.com/oppia/oppia-android/wiki/Guidance-on-submitting-a-PR#undo-unnecessary-changes)).
- [x] The PR is made from a branch that's **not** called "develop" and
is up-to-date with "develop".
- [x] The PR is **assigned** to the appropriate reviewers
([reference](https://github.com/oppia/oppia-android/wiki/Guidance-on-submitting-a-PR#clarification-regarding-assignees-and-reviewers-section)).
## For UI-specific PRs only
This shouldn't have an side effects on non-KitKat UIs, and KitKat
support itself is being removed in this PR (so there's nothing to
demonstrate).
---
.github/CODEOWNERS | 3 -
.github/workflows/build_tests.yml | 349 +-----------------
.github/workflows/unit_tests.yml | 79 +---
BUILD.bazel | 63 +---
app/src/main/AppAndroidManifest.xml | 2 +-
app/src/main/DatabindingAdaptersManifest.xml | 2 +-
app/src/main/DatabindingResourcesManifest.xml | 2 +-
app/src/main/RecyclerviewAdaptersManifest.xml | 2 +-
app/src/main/ViewModelManifest.xml | 2 +-
app/src/main/ViewModelsManifest.xml | 2 +-
app/src/main/ViewsManifest.xml | 2 +-
.../application/AbstractOppiaApplication.kt | 9 -
.../databinding/TextViewBindingAdapters.java | 4 +-
.../OsDeprecationNoticeDialogFragmentTest.kt | 2 +-
.../PlatformParameterIntegrationTest.kt | 20 +-
build_flavors.bzl | 40 +-
config/kitkat_main_dex_class_list.txt | 50 ---
.../oppia/android/config/AndroidManifest.xml | 2 +-
.../backends/gae/NetworkLoggingInterceptor.kt | 15 +-
.../data/backends/gae/NetworkModule.kt | 56 ++-
data/src/test/AndroidManifest.xml | 2 +-
.../data/backends/gae/NetworkModuleTest.kt | 238 ++++++++++--
.../syncup/PlatformParameterSyncUpWorker.kt | 18 +-
.../PlatformParameterModuleTest.kt | 2 +-
...rameterSyncUpWorkManagerInitializerTest.kt | 20 +-
.../PlatformParameterSyncUpWorkerTest.kt | 20 +-
.../src/javatests/AndroidManifest.xml | 2 +-
.../file_content_validation_checks.textproto | 6 +-
scripts/assets/maven_dependencies.textproto | 25 +-
.../license/MavenDependenciesRetriever.kt | 2 +-
.../license/MavenDependenciesListCheckTest.kt | 2 +-
.../license/MavenDependenciesRetrieverTest.kt | 2 +-
.../GenerateMavenDependenciesListTest.kt | 2 +-
.../regex/RegexPatternValidationCheckTest.kt | 6 +-
.../testing/network/RetrofitTestModule.kt | 10 +-
.../PerformanceMetricsAssessorImpl.kt | 20 +-
.../PlatformParameterConstants.kt | 4 +-
.../android/util/statusbar/StatusBarColor.kt | 12 +-
utility/src/test/AndroidManifest.xml | 2 +-
version.bzl | 18 +-
wiki/Bazel-Setup-Instructions-for-Windows.md | 8 +-
...-Bazel-Migration-Best-Practices-and-FAQ.md | 2 +-
wiki/Oppia-Bazel-Setup-Instructions.md | 4 +-
wiki/Troubleshooting-Installation.md | 6 +-
44 files changed, 374 insertions(+), 765 deletions(-)
delete mode 100644 config/kitkat_main_dex_class_list.txt
diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
index fdf7a2db439..8531242a536 100644
--- a/.github/CODEOWNERS
+++ b/.github/CODEOWNERS
@@ -259,9 +259,6 @@ WORKSPACE @oppia/android-app-infrastructure-reviewers
# Proguard configurations for Bazel builds.
/config/proguard/ @oppia/android-dev-workflow-reviewers
-# Configuration for KitKat-specific curated builds.
-/config/kitkat_main_dex_class_list.txt @oppia/android-dev-workflow-reviewers
-
# Specific manifest files specifically required for Bazel builds.
/app/src/main/AppAndroidManifest.xml @oppia/android-dev-workflow-reviewers
/app/src/main/DatabindingAdaptersManifest.xml @oppia/android-dev-workflow-reviewers
diff --git a/.github/workflows/build_tests.yml b/.github/workflows/build_tests.yml
index 2ceff32af16..9ffd5cb6594 100644
--- a/.github/workflows/build_tests.yml
+++ b/.github/workflows/build_tests.yml
@@ -15,149 +15,6 @@ concurrency:
cancel-in-progress: true
jobs:
- bazel_build_app:
- name: Build Binary with Bazel
- runs-on: ${{ matrix.os }}
- strategy:
- matrix:
- os: [ubuntu-20.04]
- env:
- ENABLE_CACHING: false
- CACHE_DIRECTORY: ~/.bazel_cache
- steps:
- - uses: actions/checkout@v2
- with:
- fetch-depth: 0
-
- - name: Set up JDK 11
- uses: actions/setup-java@v1
- with:
- java-version: 11
-
- - name: Set up Bazel
- uses: abhinavsingh/setup-bazel@v3
- with:
- version: 6.5.0
-
- - name: Set up build environment
- uses: ./.github/actions/set-up-android-bazel-build-environment
-
- # For reference on this & the later cache actions, see:
- # https://github.com/actions/cache/issues/239#issuecomment-606950711 &
- # https://github.com/actions/cache/issues/109#issuecomment-558771281. Note that these work
- # with Bazel since Bazel can share the most recent cache from an unrelated build and still
- # benefit from incremental build performance (assuming that actions/cache aggressively removes
- # older caches due to the 5GB cache limit size & Bazel's large cache size).
- - uses: actions/cache@v4
- id: cache
- with:
- path: ${{ env.CACHE_DIRECTORY }}
- key: ${{ runner.os }}-${{ env.CACHE_DIRECTORY }}-bazel-binary-${{ github.sha }}
- restore-keys: |
- ${{ runner.os }}-${{ env.CACHE_DIRECTORY }}-bazel-binary-
- ${{ runner.os }}-${{ env.CACHE_DIRECTORY }}-bazel-tests-
- ${{ runner.os }}-${{ env.CACHE_DIRECTORY }}-bazel-
-
- # This check is needed to ensure that Bazel's unbounded cache growth doesn't result in a
- # situation where the cache never updates (e.g. due to exceeding GitHub's cache size limit)
- # thereby only ever using the last successful cache version. This solution will result in a
- # few slower CI actions around the time cache is detected to be too large, but it should
- # incrementally improve thereafter.
- - name: Ensure cache size
- env:
- BAZEL_CACHE_DIR: ${{ env.CACHE_DIRECTORY }}
- run: |
- # See https://stackoverflow.com/a/27485157 for reference.
- EXPANDED_BAZEL_CACHE_PATH="${BAZEL_CACHE_DIR/#\~/$HOME}"
- CACHE_SIZE_MB=$(du -smc $EXPANDED_BAZEL_CACHE_PATH | grep total | cut -f1)
- echo "Total size of Bazel cache (rounded up to MBs): $CACHE_SIZE_MB"
- # Use a 4.5GB threshold since actions/cache compresses the results, and Bazel caches seem
- # to only increase by a few hundred megabytes across changes for unrelated branches. This
- # is also a reasonable upper-bound (local tests as of 2021-03-31 suggest that a full build
- # of the codebase (e.g. //...) from scratch only requires a ~2.1GB uncompressed/~900MB
- # compressed cache).
- if [[ "$CACHE_SIZE_MB" -gt 4500 ]]; then
- echo "Cache exceeds cut-off; resetting it (will result in a slow build)"
- rm -rf $EXPANDED_BAZEL_CACHE_PATH
- fi
-
- - name: Configure Bazel to use a local cache
- env:
- BAZEL_CACHE_DIR: ${{ env.CACHE_DIRECTORY }}
- run: |
- EXPANDED_BAZEL_CACHE_PATH="${BAZEL_CACHE_DIR/#\~/$HOME}"
- echo "Using $EXPANDED_BAZEL_CACHE_PATH as Bazel's cache path"
- echo "build --disk_cache=$EXPANDED_BAZEL_CACHE_PATH" >> $HOME/.bazelrc
- shell: bash
-
- - name: Check Bazel environment
- run: bazel info
-
- # See https://git-secret.io/installation for details on installing git-secret. Note that the
- # apt-get method isn't used since it's much slower to update & upgrade apt before installation
- # versus just directly cloning & installing the project. Further, the specific version
- # shouldn't matter since git-secret relies on a future-proof storage mechanism for secrets.
- # This also uses a different directory to install git-secret to avoid requiring root access
- # when running the git secret command.
- - name: Install git-secret (non-fork only)
- if: ${{ env.ENABLE_CACHING == 'true' && github.event.pull_request.head.repo.full_name == 'oppia/oppia-android' }}
- shell: bash
- run: |
- cd $HOME
- mkdir -p $HOME/gitsecret
- git clone https://github.com/sobolevn/git-secret.git git-secret
- cd git-secret && make build
- PREFIX="$HOME/gitsecret" make install
- echo "$HOME/gitsecret" >> $GITHUB_PATH
- echo "$HOME/gitsecret/bin" >> $GITHUB_PATH
-
- - name: Decrypt secrets (non-fork only)
- if: ${{ env.ENABLE_CACHING == 'true' && github.event.pull_request.head.repo.full_name == 'oppia/oppia-android' }}
- env:
- GIT_SECRET_GPG_PRIVATE_KEY: ${{ secrets.GIT_SECRET_GPG_PRIVATE_KEY }}
- run: |
- cd $HOME
- # NOTE TO DEVELOPERS: Make sure to never print this key directly to stdout!
- echo $GIT_SECRET_GPG_PRIVATE_KEY | base64 --decode > ./git_secret_private_key.gpg
- gpg --import ./git_secret_private_key.gpg
- cd $GITHUB_WORKSPACE
- git secret reveal
-
- # Note that caching only works on non-forks.
- - name: Build Oppia binary (with caching, non-fork only)
- if: ${{ env.ENABLE_CACHING == 'true' && github.event.pull_request.head.repo.full_name == 'oppia/oppia-android' }}
- env:
- BAZEL_REMOTE_CACHE_URL: ${{ secrets.BAZEL_REMOTE_CACHE_URL }}
- run: |
- bazel build --remote_http_cache=$BAZEL_REMOTE_CACHE_URL --google_credentials=./config/oppia-dev-workflow-remote-cache-credentials.json -- //:oppia
-
- - name: Build Oppia binary (without caching, or on a fork)
- if: ${{ env.ENABLE_CACHING == 'false' || github.event.pull_request.head.repo.full_name != 'oppia/oppia-android' }}
- run: |
- bazel build -- //:oppia
-
- # Note that caching only works on non-forks.
- - name: Build Oppia KitKat binary (with caching, non-fork only)
- if: ${{ env.ENABLE_CACHING == 'true' && github.event.pull_request.head.repo.full_name == 'oppia/oppia-android' }}
- env:
- BAZEL_REMOTE_CACHE_URL: ${{ secrets.BAZEL_REMOTE_CACHE_URL }}
- run: |
- bazel build --remote_http_cache=$BAZEL_REMOTE_CACHE_URL --google_credentials=./config/oppia-dev-workflow-remote-cache-credentials.json -- //:oppia_kitkat
-
- - name: Build Oppia binary KitKat (without caching, or on a fork)
- if: ${{ env.ENABLE_CACHING == 'false' || github.event.pull_request.head.repo.full_name != 'oppia/oppia-android' }}
- run: |
- bazel build -- //:oppia_kitkat
-
- - name: Copy Oppia dev APKs for uploading
- run: |
- cp $GITHUB_WORKSPACE/bazel-bin/oppia.apk /home/runner/work/oppia-android/oppia-android/
-
- - uses: actions/upload-artifact@v4
- with:
- name: oppia-bazel.apk
- path: /home/runner/work/oppia-android/oppia-android/oppia.apk
-
build_oppia_dev_aab:
name: Build Oppia AAB (developer flavors)
runs-on: ${{ matrix.os }}
@@ -165,7 +22,6 @@ jobs:
matrix:
os: [ubuntu-20.04]
env:
- ENABLE_CACHING: false
CACHE_DIRECTORY: ~/.bazel_cache
steps:
- uses: actions/checkout@v2
@@ -236,61 +92,8 @@ jobs:
- name: Check Bazel environment
run: bazel info
- # See https://git-secret.io/installation for details on installing git-secret. Note that the
- # apt-get method isn't used since it's much slower to update & upgrade apt before installation
- # versus just directly cloning & installing the project. Further, the specific version
- # shouldn't matter since git-secret relies on a future-proof storage mechanism for secrets.
- # This also uses a different directory to install git-secret to avoid requiring root access
- # when running the git secret command.
- - name: Install git-secret (non-fork only)
- if: ${{ env.ENABLE_CACHING == 'true' && github.event.pull_request.head.repo.full_name == 'oppia/oppia-android' }}
- shell: bash
- run: |
- cd $HOME
- mkdir -p $HOME/gitsecret
- git clone https://github.com/sobolevn/git-secret.git git-secret
- cd git-secret && make build
- PREFIX="$HOME/gitsecret" make install
- echo "$HOME/gitsecret" >> $GITHUB_PATH
- echo "$HOME/gitsecret/bin" >> $GITHUB_PATH
-
- - name: Decrypt secrets (non-fork only)
- if: ${{ env.ENABLE_CACHING == 'true' && github.event.pull_request.head.repo.full_name == 'oppia/oppia-android' }}
- env:
- GIT_SECRET_GPG_PRIVATE_KEY: ${{ secrets.GIT_SECRET_GPG_PRIVATE_KEY }}
- run: |
- cd $HOME
- # NOTE TO DEVELOPERS: Make sure to never print this key directly to stdout!
- echo $GIT_SECRET_GPG_PRIVATE_KEY | base64 --decode > ./git_secret_private_key.gpg
- gpg --import ./git_secret_private_key.gpg
- cd $GITHUB_WORKSPACE
- git secret reveal
-
- # Note that caching only works on non-forks.
- - name: Build Oppia developer AAB (with caching, non-fork only)
- if: ${{ env.ENABLE_CACHING == 'true' && github.event.pull_request.head.repo.full_name == 'oppia/oppia-android' }}
- env:
- BAZEL_REMOTE_CACHE_URL: ${{ secrets.BAZEL_REMOTE_CACHE_URL }}
- run: |
- bazel build --remote_http_cache=$BAZEL_REMOTE_CACHE_URL --google_credentials=./config/oppia-dev-workflow-remote-cache-credentials.json -- //:oppia_dev
-
- - name: Build Oppia developer AAB (without caching, or on a fork)
- if: ${{ env.ENABLE_CACHING == 'false' || github.event.pull_request.head.repo.full_name != 'oppia/oppia-android' }}
- run: |
- bazel build -- //:oppia_dev
-
- # Note that caching only works on non-forks.
- - name: Build Oppia developer KitKat AAB (with caching, non-fork only)
- if: ${{ env.ENABLE_CACHING == 'true' && github.event.pull_request.head.repo.full_name == 'oppia/oppia-android' }}
- env:
- BAZEL_REMOTE_CACHE_URL: ${{ secrets.BAZEL_REMOTE_CACHE_URL }}
- run: |
- bazel build --remote_http_cache=$BAZEL_REMOTE_CACHE_URL --google_credentials=./config/oppia-dev-workflow-remote-cache-credentials.json -- //:oppia_dev_kitkat
-
- - name: Build Oppia developer KitKat AAB (without caching, or on a fork)
- if: ${{ env.ENABLE_CACHING == 'false' || github.event.pull_request.head.repo.full_name != 'oppia/oppia-android' }}
- run: |
- bazel build -- //:oppia_dev_kitkat
+ - name: Build Oppia developer AAB
+ run: bazel build -- //:oppia_dev
build_oppia_alpha_aab:
name: Build Oppia AAB (alpha flavors)
@@ -299,7 +102,6 @@ jobs:
matrix:
os: [ubuntu-20.04]
env:
- ENABLE_CACHING: false
CACHE_DIRECTORY: ~/.bazel_cache
steps:
- uses: actions/checkout@v2
@@ -370,61 +172,8 @@ jobs:
- name: Check Bazel environment
run: bazel info
- # See https://git-secret.io/installation for details on installing git-secret. Note that the
- # apt-get method isn't used since it's much slower to update & upgrade apt before installation
- # versus just directly cloning & installing the project. Further, the specific version
- # shouldn't matter since git-secret relies on a future-proof storage mechanism for secrets.
- # This also uses a different directory to install git-secret to avoid requiring root access
- # when running the git secret command.
- - name: Install git-secret (non-fork only)
- if: ${{ env.ENABLE_CACHING == 'true' && github.event.pull_request.head.repo.full_name == 'oppia/oppia-android' }}
- shell: bash
- run: |
- cd $HOME
- mkdir -p $HOME/gitsecret
- git clone https://github.com/sobolevn/git-secret.git git-secret
- cd git-secret && make build
- PREFIX="$HOME/gitsecret" make install
- echo "$HOME/gitsecret" >> $GITHUB_PATH
- echo "$HOME/gitsecret/bin" >> $GITHUB_PATH
-
- - name: Decrypt secrets (non-fork only)
- if: ${{ env.ENABLE_CACHING == 'true' && github.event.pull_request.head.repo.full_name == 'oppia/oppia-android' }}
- env:
- GIT_SECRET_GPG_PRIVATE_KEY: ${{ secrets.GIT_SECRET_GPG_PRIVATE_KEY }}
- run: |
- cd $HOME
- # NOTE TO DEVELOPERS: Make sure to never print this key directly to stdout!
- echo $GIT_SECRET_GPG_PRIVATE_KEY | base64 --decode > ./git_secret_private_key.gpg
- gpg --import ./git_secret_private_key.gpg
- cd $GITHUB_WORKSPACE
- git secret reveal
-
- # Note that caching only works on non-forks.
- - name: Build Oppia alpha AAB (with caching, non-fork only)
- if: ${{ env.ENABLE_CACHING == 'true' && github.event.pull_request.head.repo.full_name == 'oppia/oppia-android' }}
- env:
- BAZEL_REMOTE_CACHE_URL: ${{ secrets.BAZEL_REMOTE_CACHE_URL }}
- run: |
- bazel build --compilation_mode=opt --remote_http_cache=$BAZEL_REMOTE_CACHE_URL --google_credentials=./config/oppia-dev-workflow-remote-cache-credentials.json -- //:oppia_alpha
-
- - name: Build Oppia alpha AAB (without caching, or on a fork)
- if: ${{ env.ENABLE_CACHING == 'false' || github.event.pull_request.head.repo.full_name != 'oppia/oppia-android' }}
- run: |
- bazel build --compilation_mode=opt -- //:oppia_alpha
-
- # Note that caching only works on non-forks.
- - name: Build Oppia alpha KitKat AAB (with caching, non-fork only)
- if: ${{ env.ENABLE_CACHING == 'true' && github.event.pull_request.head.repo.full_name == 'oppia/oppia-android' }}
- env:
- BAZEL_REMOTE_CACHE_URL: ${{ secrets.BAZEL_REMOTE_CACHE_URL }}
- run: |
- bazel build --compilation_mode=opt --remote_http_cache=$BAZEL_REMOTE_CACHE_URL --google_credentials=./config/oppia-dev-workflow-remote-cache-credentials.json -- //:oppia_alpha_kitkat
-
- - name: Build Oppia alpha KitKat AAB (without caching, or on a fork)
- if: ${{ env.ENABLE_CACHING == 'false' || github.event.pull_request.head.repo.full_name != 'oppia/oppia-android' }}
- run: |
- bazel build --compilation_mode=opt -- //:oppia_alpha_kitkat
+ - name: Build Oppia alpha AAB
+ run: bazel build --compilation_mode=opt -- //:oppia_alpha
build_oppia_beta_aab:
name: Build Oppia AAB (beta flavor)
@@ -433,7 +182,6 @@ jobs:
matrix:
os: [ubuntu-20.04]
env:
- ENABLE_CACHING: false
CACHE_DIRECTORY: ~/.bazel_cache
steps:
- uses: actions/checkout@v2
@@ -504,48 +252,8 @@ jobs:
- name: Check Bazel environment
run: bazel info
- # See https://git-secret.io/installation for details on installing git-secret. Note that the
- # apt-get method isn't used since it's much slower to update & upgrade apt before installation
- # versus just directly cloning & installing the project. Further, the specific version
- # shouldn't matter since git-secret relies on a future-proof storage mechanism for secrets.
- # This also uses a different directory to install git-secret to avoid requiring root access
- # when running the git secret command.
- - name: Install git-secret (non-fork only)
- if: ${{ env.ENABLE_CACHING == 'true' && github.event.pull_request.head.repo.full_name == 'oppia/oppia-android' }}
- shell: bash
- run: |
- cd $HOME
- mkdir -p $HOME/gitsecret
- git clone https://github.com/sobolevn/git-secret.git git-secret
- cd git-secret && make build
- PREFIX="$HOME/gitsecret" make install
- echo "$HOME/gitsecret" >> $GITHUB_PATH
- echo "$HOME/gitsecret/bin" >> $GITHUB_PATH
-
- - name: Decrypt secrets (non-fork only)
- if: ${{ env.ENABLE_CACHING == 'true' && github.event.pull_request.head.repo.full_name == 'oppia/oppia-android' }}
- env:
- GIT_SECRET_GPG_PRIVATE_KEY: ${{ secrets.GIT_SECRET_GPG_PRIVATE_KEY }}
- run: |
- cd $HOME
- # NOTE TO DEVELOPERS: Make sure to never print this key directly to stdout!
- echo $GIT_SECRET_GPG_PRIVATE_KEY | base64 --decode > ./git_secret_private_key.gpg
- gpg --import ./git_secret_private_key.gpg
- cd $GITHUB_WORKSPACE
- git secret reveal
-
- # Note that caching only works on non-forks.
- - name: Build Oppia beta AAB (with caching, non-fork only)
- if: ${{ env.ENABLE_CACHING == 'true' && github.event.pull_request.head.repo.full_name == 'oppia/oppia-android' }}
- env:
- BAZEL_REMOTE_CACHE_URL: ${{ secrets.BAZEL_REMOTE_CACHE_URL }}
- run: |
- bazel build --compilation_mode=opt --remote_http_cache=$BAZEL_REMOTE_CACHE_URL --google_credentials=./config/oppia-dev-workflow-remote-cache-credentials.json -- //:oppia_beta
-
- - name: Build Oppia beta AAB (without caching, or on a fork)
- if: ${{ env.ENABLE_CACHING == 'false' || github.event.pull_request.head.repo.full_name != 'oppia/oppia-android' }}
- run: |
- bazel build --compilation_mode=opt -- //:oppia_beta
+ - name: Build Oppia beta AAB
+ run: bazel build --compilation_mode=opt -- //:oppia_beta
build_oppia_ga_aab:
name: Build Oppia AAB (GA flavor)
@@ -554,7 +262,6 @@ jobs:
matrix:
os: [ubuntu-20.04]
env:
- ENABLE_CACHING: false
CACHE_DIRECTORY: ~/.bazel_cache
steps:
- uses: actions/checkout@v2
@@ -625,45 +332,5 @@ jobs:
- name: Check Bazel environment
run: bazel info
- # See https://git-secret.io/installation for details on installing git-secret. Note that the
- # apt-get method isn't used since it's much slower to update & upgrade apt before installation
- # versus just directly cloning & installing the project. Further, the specific version
- # shouldn't matter since git-secret relies on a future-proof storage mechanism for secrets.
- # This also uses a different directory to install git-secret to avoid requiring root access
- # when running the git secret command.
- - name: Install git-secret (non-fork only)
- if: ${{ env.ENABLE_CACHING == 'true' && github.event.pull_request.head.repo.full_name == 'oppia/oppia-android' }}
- shell: bash
- run: |
- cd $HOME
- mkdir -p $HOME/gitsecret
- git clone https://github.com/sobolevn/git-secret.git git-secret
- cd git-secret && make build
- PREFIX="$HOME/gitsecret" make install
- echo "$HOME/gitsecret" >> $GITHUB_PATH
- echo "$HOME/gitsecret/bin" >> $GITHUB_PATH
-
- - name: Decrypt secrets (non-fork only)
- if: ${{ env.ENABLE_CACHING == 'true' && github.event.pull_request.head.repo.full_name == 'oppia/oppia-android' }}
- env:
- GIT_SECRET_GPG_PRIVATE_KEY: ${{ secrets.GIT_SECRET_GPG_PRIVATE_KEY }}
- run: |
- cd $HOME
- # NOTE TO DEVELOPERS: Make sure to never print this key directly to stdout!
- echo $GIT_SECRET_GPG_PRIVATE_KEY | base64 --decode > ./git_secret_private_key.gpg
- gpg --import ./git_secret_private_key.gpg
- cd $GITHUB_WORKSPACE
- git secret reveal
-
- # Note that caching only works on non-forks.
- - name: Build Oppia GA AAB (with caching, non-fork only)
- if: ${{ env.ENABLE_CACHING == 'true' && github.event.pull_request.head.repo.full_name == 'oppia/oppia-android' }}
- env:
- BAZEL_REMOTE_CACHE_URL: ${{ secrets.BAZEL_REMOTE_CACHE_URL }}
- run: |
- bazel build --compilation_mode=opt --remote_http_cache=$BAZEL_REMOTE_CACHE_URL --google_credentials=./config/oppia-dev-workflow-remote-cache-credentials.json -- //:oppia_ga
-
- - name: Build Oppia GA AAB (without caching, or on a fork)
- if: ${{ env.ENABLE_CACHING == 'false' || github.event.pull_request.head.repo.full_name != 'oppia/oppia-android' }}
- run: |
- bazel build --compilation_mode=opt -- //:oppia_ga
+ - name: Build Oppia GA AAB
+ run: bazel build --compilation_mode=opt -- //:oppia_ga
diff --git a/.github/workflows/unit_tests.yml b/.github/workflows/unit_tests.yml
index 0ae969fd9f8..acd9d7795e8 100644
--- a/.github/workflows/unit_tests.yml
+++ b/.github/workflows/unit_tests.yml
@@ -106,7 +106,6 @@ jobs:
max-parallel: 10
matrix: ${{ fromJson(needs.bazel_compute_affected_targets.outputs.matrix) }}
env:
- ENABLE_CACHING: false
CACHE_DIRECTORY: ~/.bazel_cache
steps:
- uses: actions/checkout@v2
@@ -206,57 +205,7 @@ jobs:
echo "build --disk_cache=$EXPANDED_BAZEL_CACHE_PATH" >> $HOME/.bazelrc
shell: bash
- # See explanation in bazel_build_app for how this is installed.
- - name: Install git-secret (non-fork only)
- if: ${{ env.ENABLE_CACHING == 'true' && ((github.ref == 'refs/heads/develop' && github.event_name == 'push') || (github.event.pull_request.head.repo.full_name == 'oppia/oppia-android')) }}
- shell: bash
- run: |
- cd $HOME
- mkdir -p $HOME/gitsecret
- git clone https://github.com/sobolevn/git-secret.git git-secret
- cd git-secret && make build
- PREFIX="$HOME/gitsecret" make install
- echo "$HOME/gitsecret" >> $GITHUB_PATH
- echo "$HOME/gitsecret/bin" >> $GITHUB_PATH
-
- - name: Decrypt secrets (non-fork only)
- if: ${{ env.ENABLE_CACHING == 'true' && ((github.ref == 'refs/heads/develop' && github.event_name == 'push') || (github.event.pull_request.head.repo.full_name == 'oppia/oppia-android')) }}
- env:
- GIT_SECRET_GPG_PRIVATE_KEY: ${{ secrets.GIT_SECRET_GPG_PRIVATE_KEY }}
- run: |
- cd $HOME
- # NOTE TO DEVELOPERS: Make sure to never print this key directly to stdout!
- echo $GIT_SECRET_GPG_PRIVATE_KEY | base64 --decode > ./git_secret_private_key.gpg
- gpg --import ./git_secret_private_key.gpg
- cd $GITHUB_WORKSPACE
- git secret reveal
-
- # See https://www.cyberciti.biz/faq/unix-for-loop-1-to-10/ for for-loop reference.
- - name: Build Oppia Tests (with caching, non-fork only)
- if: ${{ env.ENABLE_CACHING == 'true' && ((github.ref == 'refs/heads/develop' && github.event_name == 'push') || (github.event.pull_request.head.repo.full_name == 'oppia/oppia-android')) }}
- env:
- BAZEL_REMOTE_CACHE_URL: ${{ secrets.BAZEL_REMOTE_CACHE_URL }}
- BAZEL_TEST_TARGETS: ${{ env.BAZEL_TEST_TARGETS }}
- run: |
- # Attempt to build 5 times in case there are flaky builds.
- # TODO(#3759): Remove this once there are no longer app test build failures.
- i=0
- # Disable exit-on-first-failure.
- set +e
- while [ $i -ne 5 ]; do
- i=$(( $i+1 ))
- echo "Attempt $i/5 to build test targets"
- bazel build --keep_going --remote_http_cache=$BAZEL_REMOTE_CACHE_URL --google_credentials=./config/oppia-dev-workflow-remote-cache-credentials.json -- $BAZEL_TEST_TARGETS
- done
- # Capture the error code of the final command run (which should be a success if there isn't a real build failure).
- last_error_code=$?
- # Reenable exit-on-first-failure.
- set -e
- # Exit only if the most recent exit was a failure (by using a subshell).
- (exit $last_error_code)
-
- - name: Build Oppia Tests (without caching, or on a fork)
- if: ${{ env.ENABLE_CACHING == 'false' || ((github.ref != 'refs/heads/develop' || github.event_name != 'push') && (github.event.pull_request.head.repo.full_name != 'oppia/oppia-android')) }}
+ - name: Build Oppia Tests
env:
BAZEL_TEST_TARGETS: ${{ env.BAZEL_TEST_TARGETS }}
run: |
@@ -276,30 +225,8 @@ jobs:
set -e
# Exit only if the most recent exit was a failure (by using a subshell).
(exit $last_error_code)
- - name: Run Oppia Tests (with caching, non-fork only)
- if: ${{ env.ENABLE_CACHING == 'true' && ((github.ref == 'refs/heads/develop' && github.event_name == 'push') || (github.event.pull_request.head.repo.full_name == 'oppia/oppia-android')) }}
- env:
- BAZEL_REMOTE_CACHE_URL: ${{ secrets.BAZEL_REMOTE_CACHE_URL }}
- BAZEL_TEST_TARGETS: ${{ env.BAZEL_TEST_TARGETS }}
- run: |
- # Attempt to build 5 times in case there are flaky builds.
- # TODO(#3970): Remove this once there are no longer app test build failures.
- i=0
- # Disable exit-on-first-failure.
- set +e
- while [ $i -ne 5 ]; do
- i=$(( $i+1 ))
- echo "Attempt $i/5 to run test targets"
- bazel test --keep_going --remote_http_cache=$BAZEL_REMOTE_CACHE_URL --google_credentials=./config/oppia-dev-workflow-remote-cache-credentials.json -- $BAZEL_TEST_TARGETS
- done
- # Capture the error code of the final command run (which should be a success if there isn't a real build failure).
- last_error_code=$?
- # Reenable exit-on-first-failure.
- set -e
- # Exit only if the most recent exit was a failure (by using a subshell).
- (exit $last_error_code)
- - name: Run Oppia Tests (without caching, or on a fork)
- if: ${{ env.ENABLE_CACHING == 'false' || ((github.ref != 'refs/heads/develop' || github.event_name != 'push') && (github.event.pull_request.head.repo.full_name != 'oppia/oppia-android')) }}
+
+ - name: Run Oppia Tests
env:
BAZEL_TEST_TARGETS: ${{ env.BAZEL_TEST_TARGETS }}
run: |
diff --git a/BUILD.bazel b/BUILD.bazel
index 0563e369bd5..0e1f0cecff6 100644
--- a/BUILD.bazel
+++ b/BUILD.bazel
@@ -1,11 +1,7 @@
# TODO(#1532): Rename file to 'BUILD' post-Gradle.
load("@dagger//:workspace_defs.bzl", "dagger_rules")
-load("//:build_flavors.bzl", "AVAILABLE_FLAVORS", "define_oppia_aab_binary_flavor", "transform_android_manifest")
-load("//:version.bzl", "MAJOR_VERSION", "MINOR_VERSION", "OPPIA_DEV_KITKAT_VERSION_CODE", "OPPIA_DEV_VERSION_CODE")
-
-# This is exported here since config/ isn't a Bazel package.
-exports_files(["config/kitkat_main_dex_class_list.txt"])
+load("//:build_flavors.bzl", "AVAILABLE_FLAVORS", "define_oppia_aab_binary_flavor")
# Corresponds to being accessible to all Oppia targets. This should be used for production APIs &
# modules that may be used both in production targets and in tests.
@@ -73,63 +69,6 @@ package_group(
)
# TODO(#1640): Move binary manifest to top-level package post-Gradle.
-[
- transform_android_manifest(
- name = "oppia_apk_%s_transformed_manifest" % apk_flavor_metadata["flavor"],
- application_relative_qualified_class = ".app.application.dev.DeveloperOppiaApplication",
- build_flavor = apk_flavor_metadata["flavor"],
- input_file = "//app:src/main/AndroidManifest.xml",
- major_version = MAJOR_VERSION,
- minor_version = MINOR_VERSION,
- output_file = "AndroidManifest_transformed_%s.xml" % apk_flavor_metadata["flavor"],
- version_code = apk_flavor_metadata["version_code"],
- )
- for apk_flavor_metadata in [
- {
- "flavor": "oppia",
- "version_code": OPPIA_DEV_VERSION_CODE,
- },
- {
- "flavor": "oppia_kitkat",
- "version_code": OPPIA_DEV_KITKAT_VERSION_CODE,
- },
- ]
-]
-
-[
- android_binary(
- name = apk_flavor_metadata["flavor"],
- custom_package = "org.oppia.android",
- enable_data_binding = True,
- main_dex_list = apk_flavor_metadata.get("main_dex_list"),
- manifest = "oppia_apk_%s_transformed_manifest" % apk_flavor_metadata["flavor"],
- manifest_values = {
- "applicationId": "org.oppia.android",
- "minSdkVersion": "%d" % apk_flavor_metadata["min_sdk_version"],
- "targetSdkVersion": "%d" % apk_flavor_metadata["target_sdk_version"],
- },
- multidex = apk_flavor_metadata["multidex"],
- deps = [
- "//app/src/main/java/org/oppia/android/app/application/dev:developer_application",
- "//config/src/java/org/oppia/android/config:all_languages_config",
- ],
- )
- for apk_flavor_metadata in [
- {
- "flavor": "oppia",
- "min_sdk_version": 21,
- "multidex": "native",
- "target_sdk_version": 34,
- },
- {
- "flavor": "oppia_kitkat",
- "main_dex_list": "//:config/kitkat_main_dex_class_list.txt",
- "min_sdk_version": 21,
- "multidex": "manual_main_dex",
- "target_sdk_version": 34,
- },
- ]
-]
# Define all binary flavors that can be built. Note that these are AABs and thus cannot be installed
# directly. However, their binaries can be installed using mobile-install like so:
diff --git a/app/src/main/AppAndroidManifest.xml b/app/src/main/AppAndroidManifest.xml
index 0b5a672e4b7..b46ab704ac4 100644
--- a/app/src/main/AppAndroidManifest.xml
+++ b/app/src/main/AppAndroidManifest.xml
@@ -1,5 +1,5 @@
-
diff --git a/app/src/main/DatabindingAdaptersManifest.xml b/app/src/main/DatabindingAdaptersManifest.xml
index 783be2f9363..9ca28db49a7 100644
--- a/app/src/main/DatabindingAdaptersManifest.xml
+++ b/app/src/main/DatabindingAdaptersManifest.xml
@@ -1,5 +1,5 @@
-
diff --git a/app/src/main/DatabindingResourcesManifest.xml b/app/src/main/DatabindingResourcesManifest.xml
index b48bc109de3..b7d5ffe9e72 100644
--- a/app/src/main/DatabindingResourcesManifest.xml
+++ b/app/src/main/DatabindingResourcesManifest.xml
@@ -1,5 +1,5 @@
-
diff --git a/app/src/main/RecyclerviewAdaptersManifest.xml b/app/src/main/RecyclerviewAdaptersManifest.xml
index 8f917756650..f05de7c5ee8 100644
--- a/app/src/main/RecyclerviewAdaptersManifest.xml
+++ b/app/src/main/RecyclerviewAdaptersManifest.xml
@@ -1,5 +1,5 @@
-
diff --git a/app/src/main/ViewModelManifest.xml b/app/src/main/ViewModelManifest.xml
index 67e79d1f41d..8d8132db1e2 100644
--- a/app/src/main/ViewModelManifest.xml
+++ b/app/src/main/ViewModelManifest.xml
@@ -2,6 +2,6 @@
-
diff --git a/app/src/main/ViewModelsManifest.xml b/app/src/main/ViewModelsManifest.xml
index 83d6b023161..41ddc2727ca 100644
--- a/app/src/main/ViewModelsManifest.xml
+++ b/app/src/main/ViewModelsManifest.xml
@@ -2,6 +2,6 @@
-
diff --git a/app/src/main/ViewsManifest.xml b/app/src/main/ViewsManifest.xml
index eac7e6941c4..059e24dbebe 100644
--- a/app/src/main/ViewsManifest.xml
+++ b/app/src/main/ViewsManifest.xml
@@ -2,6 +2,6 @@
-
diff --git a/app/src/main/java/org/oppia/android/app/application/AbstractOppiaApplication.kt b/app/src/main/java/org/oppia/android/app/application/AbstractOppiaApplication.kt
index 11a3025ff14..610178c32cd 100644
--- a/app/src/main/java/org/oppia/android/app/application/AbstractOppiaApplication.kt
+++ b/app/src/main/java/org/oppia/android/app/application/AbstractOppiaApplication.kt
@@ -4,7 +4,6 @@ import android.annotation.SuppressLint
import android.app.Application
import android.os.Build
import androidx.appcompat.app.AppCompatActivity
-import androidx.appcompat.app.AppCompatDelegate
import androidx.multidex.MultiDexApplication
import androidx.work.Configuration
import androidx.work.WorkManager
@@ -39,14 +38,6 @@ abstract class AbstractOppiaApplication(
@SuppressLint("ObsoleteSdkInt") // Incorrect warning.
override fun onCreate() {
super.onCreate()
- if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
- // Ensure vector drawables can be properly loaded on KitKat devices. Note that this can
- // introduce memory issues, but it's an easier-to-maintain solution that replacing all image
- // binding with custom hook-ins (especially when it comes to databinding which isn't
- // configurable in how it loads drawables), or building a custom vector drawable->PNG pipeline
- // in Bazel.
- AppCompatDelegate.setCompatVectorFromResourcesEnabled(true)
- }
// The current WorkManager version doesn't work in SDK 31+, so disable it.
// TODO(#4751): Re-enable WorkManager for S+.
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
diff --git a/app/src/main/java/org/oppia/android/app/databinding/TextViewBindingAdapters.java b/app/src/main/java/org/oppia/android/app/databinding/TextViewBindingAdapters.java
index 284e1332b2d..6b75951614e 100644
--- a/app/src/main/java/org/oppia/android/app/databinding/TextViewBindingAdapters.java
+++ b/app/src/main/java/org/oppia/android/app/databinding/TextViewBindingAdapters.java
@@ -45,7 +45,7 @@ public static void setProfileLastVisitedText(@NonNull TextView textView, long ti
}
// TODO(#4345): Add test for this method.
- /** Binds an AndroidX KitKat-compatible drawable top to the specified text view. */
+ /** Binds an AndroidX drawable top to the specified text view. */
@BindingAdapter("drawableTopCompat")
public static void setDrawableTopCompat(
@NonNull TextView imageView,
@@ -56,7 +56,7 @@ public static void setDrawableTopCompat(
);
}
- /** Binds an AndroidX KitKat-compatible drawable end to the specified text view. */
+ /** Binds an AndroidX drawable end to the specified text view. */
@BindingAdapter("drawableEndCompat")
public static void setDrawableEndCompat(
@NonNull TextView imageView,
diff --git a/app/src/sharedTest/java/org/oppia/android/app/notice/OsDeprecationNoticeDialogFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/notice/OsDeprecationNoticeDialogFragmentTest.kt
index 8c44a36dc31..c2ec1431c63 100644
--- a/app/src/sharedTest/java/org/oppia/android/app/notice/OsDeprecationNoticeDialogFragmentTest.kt
+++ b/app/src/sharedTest/java/org/oppia/android/app/notice/OsDeprecationNoticeDialogFragmentTest.kt
@@ -168,7 +168,7 @@ class OsDeprecationNoticeDialogFragmentTest {
.onActionButtonClicked(
DeprecationNoticeActionResponse.Dismiss(
deprecationNoticeType = DeprecationNoticeType.OS_DEPRECATION,
- deprecatedVersion = 19,
+ deprecatedVersion = 21,
)
)
}
diff --git a/app/src/test/java/org/oppia/android/app/testing/PlatformParameterIntegrationTest.kt b/app/src/test/java/org/oppia/android/app/testing/PlatformParameterIntegrationTest.kt
index 55923caadbc..67b2d0b7270 100644
--- a/app/src/test/java/org/oppia/android/app/testing/PlatformParameterIntegrationTest.kt
+++ b/app/src/test/java/org/oppia/android/app/testing/PlatformParameterIntegrationTest.kt
@@ -15,7 +15,6 @@ import androidx.work.WorkInfo
import androidx.work.WorkManager
import androidx.work.impl.utils.SynchronousExecutor
import androidx.work.testing.WorkManagerTestInitHelper
-import com.google.common.base.Optional
import com.google.common.truth.Truth.assertThat
import dagger.Component
import dagger.Module
@@ -310,19 +309,17 @@ class PlatformParameterIntegrationTest {
jsonPrefixNetworkInterceptor: JsonPrefixNetworkInterceptor,
remoteAuthNetworkInterceptor: RemoteAuthNetworkInterceptor,
@BaseUrl baseUrl: String
- ): Optional {
+ ): Retrofit {
val client = OkHttpClient.Builder()
.addInterceptor(jsonPrefixNetworkInterceptor)
.addInterceptor(remoteAuthNetworkInterceptor)
.build()
- return Optional.of(
- Retrofit.Builder()
- .baseUrl(baseUrl)
- .addConverterFactory(MoshiConverterFactory.create())
- .client(client)
- .build()
- )
+ return Retrofit.Builder()
+ .baseUrl(baseUrl)
+ .addConverterFactory(MoshiConverterFactory.create())
+ .client(client)
+ .build()
}
@Provides
@@ -332,9 +329,8 @@ class PlatformParameterIntegrationTest {
@Provides
fun provideMockPlatformParameterService(
mockRetrofit: MockRetrofit
- ): Optional {
- val delegate = mockRetrofit.create(PlatformParameterService::class.java)
- return Optional.of(MockPlatformParameterService(delegate))
+ ): PlatformParameterService {
+ return MockPlatformParameterService(mockRetrofit.create(PlatformParameterService::class.java))
}
}
diff --git a/build_flavors.bzl b/build_flavors.bzl
index b0ed8e2169c..19b9939c3fa 100644
--- a/build_flavors.bzl
+++ b/build_flavors.bzl
@@ -3,25 +3,18 @@ Macros & definitions corresponding to Oppia binary build flavors.
"""
load("//:oppia_android_application.bzl", "generate_universal_apk", "oppia_android_application")
-load("//:version.bzl", "MAJOR_VERSION", "MINOR_VERSION", "OPPIA_ALPHA_KITKAT_VERSION_CODE", "OPPIA_ALPHA_VERSION_CODE", "OPPIA_BETA_VERSION_CODE", "OPPIA_DEV_KITKAT_VERSION_CODE", "OPPIA_DEV_VERSION_CODE", "OPPIA_GA_VERSION_CODE")
+load("//:version.bzl", "MAJOR_VERSION", "MINOR_VERSION", "OPPIA_ALPHA_VERSION_CODE", "OPPIA_BETA_VERSION_CODE", "OPPIA_DEV_VERSION_CODE", "OPPIA_GA_VERSION_CODE")
# Defines the list of flavors available to build the Oppia app in. Note to developers: this list
# should be ordered by the development pipeline (i.e. features go through dev first, then other
# flavors as they mature).
AVAILABLE_FLAVORS = [
"dev",
- "dev_kitkat",
"alpha",
- "alpha_kitkat",
"beta",
"ga",
]
-# This file contains the list of classes that must be in the main dex list for the legacy multidex
-# build used on KitKat devices. Generally, this is the main application class is needed so that it
-# can load multidex support, plus any dependencies needed by that pipeline.
-_MAIN_DEX_LIST_TARGET_KITKAT = "//:config/kitkat_main_dex_class_list.txt"
-
# keep sorted
_PRODUCTION_PROGUARD_SPECS = [
"config/proguard/android-proguard-rules.pro",
@@ -55,21 +48,6 @@ _FLAVOR_METADATA = {
"version_code": OPPIA_DEV_VERSION_CODE,
"application_class": ".app.application.dev.DeveloperOppiaApplication",
},
- "dev_kitkat": {
- "manifest": "//app:src/main/AndroidManifest.xml",
- "min_sdk_version": 21,
- "target_sdk_version": 34,
- "multidex": "manual_main_dex",
- "main_dex_list": _MAIN_DEX_LIST_TARGET_KITKAT,
- "proguard_specs": [], # Developer builds are not optimized.
- "production_release": False,
- "deps": [
- "//app/src/main/java/org/oppia/android/app/application/dev:developer_application",
- "//config/src/java/org/oppia/android/config:all_languages_config",
- ],
- "version_code": OPPIA_DEV_KITKAT_VERSION_CODE,
- "application_class": ".app.application.dev.DeveloperOppiaApplication",
- },
"alpha": {
"manifest": "//app:src/main/AndroidManifest.xml",
"min_sdk_version": 21,
@@ -84,21 +62,6 @@ _FLAVOR_METADATA = {
"version_code": OPPIA_ALPHA_VERSION_CODE,
"application_class": ".app.application.alpha.AlphaOppiaApplication",
},
- "alpha_kitkat": {
- "manifest": "//app:src/main/AndroidManifest.xml",
- "min_sdk_version": 21,
- "target_sdk_version": 34,
- "multidex": "manual_main_dex",
- "main_dex_list": _MAIN_DEX_LIST_TARGET_KITKAT,
- "proguard_specs": [],
- "production_release": True,
- "deps": [
- "//app/src/main/java/org/oppia/android/app/application/alpha:alpha_application",
- "//config/src/java/org/oppia/android/config:all_languages_config",
- ],
- "version_code": OPPIA_ALPHA_KITKAT_VERSION_CODE,
- "application_class": ".app.application.alpha.AlphaOppiaApplication",
- },
"beta": {
"manifest": "//app:src/main/AndroidManifest.xml",
"min_sdk_version": 21,
@@ -272,7 +235,6 @@ def define_oppia_aab_binary_flavor(flavor):
"targetSdkVersion": "%d" % _FLAVOR_METADATA[flavor]["target_sdk_version"],
},
multidex = _FLAVOR_METADATA[flavor]["multidex"],
- main_dex_list = _FLAVOR_METADATA[flavor].get("main_dex_list"),
proguard_generate_mapping = True if len(_FLAVOR_METADATA[flavor]["proguard_specs"]) != 0 else False,
proguard_specs = _FLAVOR_METADATA[flavor]["proguard_specs"],
shrink_resources = True if len(_FLAVOR_METADATA[flavor]["proguard_specs"]) != 0 else False,
diff --git a/config/kitkat_main_dex_class_list.txt b/config/kitkat_main_dex_class_list.txt
deleted file mode 100644
index ea2de317d21..00000000000
--- a/config/kitkat_main_dex_class_list.txt
+++ /dev/null
@@ -1,50 +0,0 @@
-androidx/multidex/BuildConfig.class
-androidx/multidex/MultiDex$V19.class
-androidx/multidex/MultiDex.class
-androidx/multidex/MultiDexApplication.class
-androidx/multidex/MultiDexExtractor$1.class
-androidx/multidex/MultiDexExtractor$ExtractedDex.class
-androidx/multidex/MultiDexExtractor.class
-androidx/multidex/ZipUtil$CentralDirectory.class
-androidx/multidex/ZipUtil.class
-androidx/work/Configuration$Provider.class
-androidx/work/Configuration.class
-androidx/work/WorkManager.class
-javax/inject/Provider.class
-kotlin/Function.class
-kotlin/jvm/functions/Function0.class
-kotlin/jvm/internal/FunctionBase.class
-kotlin/jvm/internal/Intrinsics.class
-kotlin/jvm/internal/Lambda.class
-kotlin/Lazy.class
-kotlin/LazyKt.class
-kotlin/LazyKt__LazyJVMKt.class
-kotlin/LazyKt__LazyKt.class
-kotlin/SynchronizedLazyImpl.class
-kotlin/UNINITIALIZED_VALUE.class
-org/oppia/android/app/activity/ActivityComponent.class
-org/oppia/android/app/activity/ActivityComponentFactory.class
-org/oppia/android/app/activity/ActivityComponentImpl$Builder.class
-org/oppia/android/app/activity/ActivityComponentImpl.class
-org/oppia/android/app/application/ApplicationComponent$Builder.class
-org/oppia/android/app/application/ApplicationComponent.class
-org/oppia/android/app/application/ApplicationInjector.class
-org/oppia/android/app/application/ApplicationInjectorProvider$DefaultImpls.class
-org/oppia/android/app/application/ApplicationInjectorProvider.class
-org/oppia/android/app/application/ApplicationModule.class
-org/oppia/android/app/application/dev/DaggerDeveloperApplicationComponent$ActivityComponentImplBuilder.class
-org/oppia/android/app/application/dev/DaggerDeveloperApplicationComponent$Builder.class
-org/oppia/android/app/application/dev/DaggerDeveloperApplicationComponent.class
-org/oppia/android/app/application/dev/DeveloperOppiaApplication.class
-org/oppia/android/app/translation/AppLanguageActivityInjector.class
-org/oppia/android/app/translation/AppLanguageActivityInjectorProvider.class
-org/oppia/android/app/translation/AppLanguageApplicationInjector.class
-org/oppia/android/app/translation/AppLanguageApplicationInjectorProvider.class
-org/oppia/android/app/utility/datetime/DateTimeUtil$Injector.class
-org/oppia/android/domain/locale/LocaleApplicationInjector.class
-org/oppia/android/domain/locale/LocaleApplicationInjectorProvider.class
-org/oppia/android/domain/oppialogger/ApplicationStartupListener.class
-org/oppia/android/util/data/DataProvidersInjector.class
-org/oppia/android/util/data/DataProvidersInjectorProvider.class
-org/oppia/android/util/system/OppiaClockInjector.class
-org/oppia/android/util/system/OppiaClockInjectorProvider.class
diff --git a/config/src/java/org/oppia/android/config/AndroidManifest.xml b/config/src/java/org/oppia/android/config/AndroidManifest.xml
index 58158ec68c4..0b7883ee463 100644
--- a/config/src/java/org/oppia/android/config/AndroidManifest.xml
+++ b/config/src/java/org/oppia/android/config/AndroidManifest.xml
@@ -1,5 +1,5 @@
-
+
diff --git a/data/src/main/java/org/oppia/android/data/backends/gae/NetworkLoggingInterceptor.kt b/data/src/main/java/org/oppia/android/data/backends/gae/NetworkLoggingInterceptor.kt
index 97aab7d61a9..43e7749dd5e 100755
--- a/data/src/main/java/org/oppia/android/data/backends/gae/NetworkLoggingInterceptor.kt
+++ b/data/src/main/java/org/oppia/android/data/backends/gae/NetworkLoggingInterceptor.kt
@@ -14,6 +14,7 @@ import java.io.IOException
import java.lang.Exception
import javax.inject.Inject
import javax.inject.Singleton
+import kotlin.text.Charsets
/**
* Interceptor on top of Retrofit to log network requests and responses.
@@ -42,7 +43,15 @@ class NetworkLoggingInterceptor @Inject constructor(
return try {
val response = chain.proceed(request)
- val responseBody = response.body?.string()
+ // The response body needs to be cloned for reading otherwise it will throw since the body can
+ // only be read fully at most one time and other interceptors in the chain may read it. See
+ // https://stackoverflow.com/a/33862068 or OkHttp's HttpLoggingInterceptor for a reference.
+ val responseBody = response.body
+ val requestLength = responseBody?.contentLength()?.takeIf { it != -1L }
+ val responseBodyText =
+ responseBody?.source()?.also {
+ it.request(requestLength ?: Long.MAX_VALUE)
+ }?.buffer?.clone()?.readString(Charsets.UTF_8)
CoroutineScope(backgroundDispatcher).launch {
_logNetworkCallFlow.emit(
@@ -50,7 +59,7 @@ class NetworkLoggingInterceptor @Inject constructor(
.setRequestUrl(request.url.toString())
.setHeaders(request.headers.toString())
.setResponseStatusCode(response.code)
- .setBody(responseBody ?: "")
+ .setBody(responseBodyText ?: "")
.build()
)
}
@@ -62,7 +71,7 @@ class NetworkLoggingInterceptor @Inject constructor(
.setRequestUrl(request.url.toString())
.setHeaders(request.headers.toString())
.setResponseStatusCode(response.code)
- .setErrorMessage(responseBody ?: "")
+ .setErrorMessage(responseBodyText ?: "")
.build()
)
}
diff --git a/data/src/main/java/org/oppia/android/data/backends/gae/NetworkModule.kt b/data/src/main/java/org/oppia/android/data/backends/gae/NetworkModule.kt
index 0ac58d8fe72..0570dac652c 100644
--- a/data/src/main/java/org/oppia/android/data/backends/gae/NetworkModule.kt
+++ b/data/src/main/java/org/oppia/android/data/backends/gae/NetworkModule.kt
@@ -1,8 +1,5 @@
package org.oppia.android.data.backends.gae
-import android.annotation.SuppressLint
-import android.os.Build
-import com.google.common.base.Optional
import dagger.Module
import dagger.Provides
import okhttp3.OkHttpClient
@@ -19,58 +16,51 @@ import javax.inject.Singleton
*/
@Module
class NetworkModule {
- @SuppressLint("ObsoleteSdkInt") // AS warning is incorrect in this context.
@OppiaRetrofit
@Provides
@Singleton
fun provideRetrofitInstance(
- jsonPrefixNetworkInterceptor: JsonPrefixNetworkInterceptor,
remoteAuthNetworkInterceptor: RemoteAuthNetworkInterceptor,
networkLoggingInterceptor: NetworkLoggingInterceptor,
+ jsonPrefixNetworkInterceptor: JsonPrefixNetworkInterceptor,
@BaseUrl baseUrl: String
- ): Optional {
- // TODO(#1720): Make this a compile-time dep once Hilt provides it as an option.
- return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
- val client = OkHttpClient.Builder()
- .addInterceptor(jsonPrefixNetworkInterceptor)
- .addInterceptor(remoteAuthNetworkInterceptor)
- .addInterceptor(networkLoggingInterceptor)
- .build()
-
- Optional.of(
- Retrofit.Builder()
- .baseUrl(baseUrl)
- .addConverterFactory(MoshiConverterFactory.create())
- .client(client)
- .build()
+ ): Retrofit {
+ return Retrofit.Builder().apply {
+ baseUrl(baseUrl)
+ addConverterFactory(MoshiConverterFactory.create())
+ client(
+ OkHttpClient.Builder().apply {
+ // This is in a specific order. The auth modifies a request, so it happens first. The
+ // prefix remover executes other interceptors before changing the response, so it's
+ // registered last so that the network logging interceptor receives a response with the
+ // XSSI prefix correctly removed.
+ addInterceptor(remoteAuthNetworkInterceptor)
+ addInterceptor(networkLoggingInterceptor)
+ addInterceptor(jsonPrefixNetworkInterceptor)
+ }.build()
)
- } else Optional.absent()
+ }.build()
}
@Provides
@Singleton
fun provideFeedbackReportingService(
- @OppiaRetrofit retrofit: Optional
- ): Optional {
- return retrofit.map { it.create(FeedbackReportingService::class.java) }
+ @OppiaRetrofit retrofit: Retrofit
+ ): FeedbackReportingService {
+ return retrofit.create(FeedbackReportingService::class.java)
}
@Provides
@Singleton
fun providePlatformParameterService(
- @OppiaRetrofit retrofit: Optional
- ): Optional {
- return retrofit.map { it.create(PlatformParameterService::class.java) }
+ @OppiaRetrofit retrofit: Retrofit
+ ): PlatformParameterService {
+ return retrofit.create(PlatformParameterService::class.java)
}
// Provides the API key to use in authenticating remote messages sent or received. This will be
- // replaced with a secret key in production.
+ // replaced with a secret key in production builds.
@Provides
@NetworkApiKey
fun provideNetworkApiKey(): String = ""
-
- private companion object {
- private fun Optional.map(mapFunc: (T) -> V): Optional =
- transform { mapFunc(checkNotNull(it)) } // Payload should never actually be null.
- }
}
diff --git a/data/src/test/AndroidManifest.xml b/data/src/test/AndroidManifest.xml
index 2078d324983..cca8e4c298f 100644
--- a/data/src/test/AndroidManifest.xml
+++ b/data/src/test/AndroidManifest.xml
@@ -1,5 +1,5 @@
-
diff --git a/data/src/test/java/org/oppia/android/data/backends/gae/NetworkModuleTest.kt b/data/src/test/java/org/oppia/android/data/backends/gae/NetworkModuleTest.kt
index e30664000e8..bffa94db228 100644
--- a/data/src/test/java/org/oppia/android/data/backends/gae/NetworkModuleTest.kt
+++ b/data/src/test/java/org/oppia/android/data/backends/gae/NetworkModuleTest.kt
@@ -2,26 +2,46 @@ package org.oppia.android.data.backends.gae
import android.app.Application
import android.content.Context
-import android.os.Build
import androidx.test.core.app.ApplicationProvider
+import androidx.test.core.content.pm.ApplicationInfoBuilder
+import androidx.test.core.content.pm.PackageInfoBuilder
import androidx.test.ext.junit.runners.AndroidJUnit4
-import com.google.common.base.Optional
import com.google.common.truth.Truth.assertThat
+import com.squareup.moshi.Json
+import com.squareup.moshi.JsonClass
+import com.squareup.moshi.JsonDataException
import dagger.BindsInstance
import dagger.Component
import dagger.Module
import dagger.Provides
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.async
+import kotlinx.coroutines.flow.take
+import kotlinx.coroutines.flow.toList
+import okhttp3.mockwebserver.MockResponse
+import okhttp3.mockwebserver.MockWebServer
+import org.junit.After
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.oppia.android.data.backends.gae.api.FeedbackReportingService
import org.oppia.android.data.backends.gae.api.PlatformParameterService
+import org.oppia.android.testing.assertThrows
import org.oppia.android.testing.robolectric.RobolectricModule
+import org.oppia.android.testing.threading.BackgroundTestDispatcher
+import org.oppia.android.testing.threading.TestCoroutineDispatcher
+import org.oppia.android.testing.threading.TestCoroutineDispatchers
import org.oppia.android.testing.threading.TestDispatcherModule
+import org.robolectric.Shadows
import org.robolectric.annotation.Config
import org.robolectric.annotation.LooperMode
+import retrofit2.Call
import retrofit2.Retrofit
+import retrofit2.http.GET
+import java.net.HttpURLConnection
import javax.inject.Inject
+import javax.inject.Provider
import javax.inject.Singleton
/** Tests for [NetworkModule]. */
@@ -29,58 +49,212 @@ import javax.inject.Singleton
@LooperMode(LooperMode.Mode.PAUSED)
@Config(application = NetworkModuleTest.TestApplication::class)
class NetworkModuleTest {
- @field:[Inject NetworkApiKey]
- lateinit var networkApiKey: String
+ @field:[Inject NetworkApiKey] lateinit var networkApiKey: String
+ @field:[Inject OppiaRetrofit] lateinit var retrofit: Retrofit
+ @field:[Inject OppiaRetrofit] lateinit var retrofitProvider: Provider
+ @Inject lateinit var context: Context
+ @Inject lateinit var mockWebServer: MockWebServer
+ @Inject lateinit var platformParameterService: PlatformParameterService
+ @Inject lateinit var feedbackReportingServiceProvider: Provider
+ @Inject lateinit var platformParameterServiceProvider: Provider
+ @Inject lateinit var testCoroutineDispatchers: TestCoroutineDispatchers
+ @Inject lateinit var networkLoggingInterceptor: NetworkLoggingInterceptor
+
+ @field:[Inject BackgroundTestDispatcher]
+ lateinit var backgroundTestDispatcher: TestCoroutineDispatcher
@Before
fun setUp() {
setUpTestApplicationComponent()
+ setUpApplicationForContext()
+ }
+
+ @After
+ fun tearDown() {
+ mockWebServer.shutdown()
+ }
+
+ @Test
+ fun testInjectedRetrofit_hasMockBaseUrl() {
+ val baseUrl = retrofit.baseUrl().toUrl().toString()
+
+ assertThat(baseUrl).isEqualTo(mockWebServer.url("/").toUrl().toString())
+ }
+
+ @Test
+ fun testInjectedRetrofit_doesNotHaveOppiaBaseUrl() {
+ val baseUrl = retrofit.baseUrl().toUrl().toString()
+
+ // The URL should point to a local development server since a MockWebServer is being used.
+ assertThat(baseUrl).doesNotContain("oppia.org")
+ }
+
+ @Test
+ fun testInjectedRetrofit_secondInjection_returnsSingletonInstance() {
+ val firstInjection = retrofitProvider.get()
+ val secondInjection = retrofitProvider.get()
+
+ // Multiple injections should yield the same instance due to it being a singleton.
+ assertThat(firstInjection).isEqualTo(secondInjection)
+ }
+
+ @Test
+ fun testRetrofit_withTestService_responseWithoutXssiPrefix_nonJsonResponse_callSucceeds() {
+ val service = retrofit.create(TestService::class.java)
+ setUpTestServiceResponse(json = "{}")
+
+ val parametersCall = service.fetchNothing().execute()
+
+ assertThat(parametersCall.isSuccessful).isTrue()
}
@Test
- @Config(sdk = [Build.VERSION_CODES.LOLLIPOP])
- fun testRetrofitInstance_lollipop_isProvided() {
- assertThat(getTestApplication().getRetrofit()).isPresent()
+ fun testRetrofit_withTestService_malformedJsonResponse_throwsJsonDataException() {
+ val service = retrofit.create(TestService::class.java)
+ setUpTestServiceResponse(json = "{}")
+
+ val exception = assertThrows() { service.fetchTestObject().execute() }
+
+ // Verify that Moshi deserialization fails correctly on malformed JSON responses.
+ assertThat(exception).hasMessageThat().contains("Required value 'field1' missing")
}
@Test
- @Config(sdk = [Build.VERSION_CODES.LOLLIPOP])
- fun testFeedbackReportingService_lollipop_isProvided() {
- assertThat(getTestApplication().getFeedbackReportingService()).isPresent()
+ fun testRetrofit_withTestService_responseWithoutXssiPrefix_jsonResponse_returnsCorrectObject() {
+ val service = retrofit.create(TestService::class.java)
+ setUpTestServiceResponse(json = "{\"field1\":\"asdf\",\"field2\":1}")
+
+ val testObject = service.fetchTestObject().execute().body()
+
+ // Verify that Moshi deserialization works correctly.
+ assertThat(testObject?.field1).isEqualTo("asdf")
+ assertThat(testObject?.field2).isEqualTo(1)
}
@Test
- @Config(sdk = [Build.VERSION_CODES.LOLLIPOP])
- fun testPlatformParameterService_lollipop_isProvided() {
- assertThat(getTestApplication().getPlatformParameterService()).isPresent()
+ fun testRetrofit_withTestService_responseWithXssiPrefix_jsonResponse_returnsCorrectObject() {
+ val service = retrofit.create(TestService::class.java)
+ setUpTestObjectServiceResponse(field1 = "field val", field2 = 3)
+
+ val testObject = service.fetchTestObject().execute().body()
+
+ // Verify that the XSSI prefix is correctly removed.
+ assertThat(testObject?.field1).isEqualTo("field val")
+ assertThat(testObject?.field2).isEqualTo(3)
}
@Test
- fun testNetworkApiKey_isEmpty() {
+ fun testRetrofit_withTestService_sendsCorrectAuthHeaderContext() {
+ val service = retrofit.create(TestService::class.java)
+ setUpTestObjectServiceResponse(field1 = "field val", field2 = 3)
+
+ service.fetchTestObject().execute()
+
+ val request = mockWebServer.takeRequest()
+ assertThat(request.getHeader("api_key")).isEmpty() // Verifies presence, but value is empty.
+ assertThat(request.getHeader("app_package_name")).isEqualTo("org.oppia.android.data")
+ assertThat(request.getHeader("app_version_name")).isEqualTo(TEST_APP_VERSION_NAME)
+ assertThat(request.getHeader("app_version_code")).isEqualTo("$TEST_APP_VERSION_CODE")
+ }
+
+ @Test
+ @ExperimentalCoroutinesApi
+ fun testRetrofit_withTestService_logsSuccessfulResponse() {
+ val service = retrofit.create(TestService::class.java)
+ setUpTestObjectServiceResponse(field1 = "field val", field2 = 3)
+ // Collect requests.
+ val firstRequestsDeferred = CoroutineScope(backgroundTestDispatcher).async {
+ networkLoggingInterceptor.logNetworkCallFlow.take(1).toList()
+ }
+ testCoroutineDispatchers.advanceUntilIdle() // Ensure the flow is subscribed before emit().
+
+ service.fetchTestObject().execute()
+ testCoroutineDispatchers.advanceUntilIdle()
+
+ val firstRequest = firstRequestsDeferred.getCompleted().single()
+ assertThat(firstRequest.responseStatusCode).isEqualTo(HttpURLConnection.HTTP_OK)
+ assertThat(firstRequest.body).isEqualTo("{\"field1\":\"field val\",\"field2\":3}")
+ }
+
+ @Test
+ fun testInjectedFeedbackReportingService_secondInjection_returnsSingletonInstance() {
+ val firstInjection = feedbackReportingServiceProvider.get()
+ val secondInjection = feedbackReportingServiceProvider.get()
+
+ // Multiple injections should yield the same instance due to it being a singleton.
+ assertThat(firstInjection).isEqualTo(secondInjection)
+ }
+
+ @Test
+ fun testInjectedPlatformParameterService_secondInjection_returnsSingletonInstance() {
+ val firstInjection = platformParameterServiceProvider.get()
+ val secondInjection = platformParameterServiceProvider.get()
+
+ // Multiple injections should yield the same instance due to it being a singleton.
+ assertThat(firstInjection).isEqualTo(secondInjection)
+ }
+
+ @Test
+ fun testInjectedNetworkApiKey_isEmptyByDefault() {
// The network API key is empty by default on developer builds.
assertThat(networkApiKey).isEmpty()
}
+ private fun setUpTestObjectServiceResponse(field1: String, field2: Int) {
+ setUpTestServiceResponse(json = "$XSSI_PREFIX\n{\"field1\":\"$field1\",\"field2\":$field2}")
+ }
+
+ private fun setUpTestServiceResponse(json: String) {
+ mockWebServer.enqueue(MockResponse().setBody(json))
+ }
+
private fun getTestApplication() = ApplicationProvider.getApplicationContext()
private fun setUpTestApplicationComponent() {
getTestApplication().inject(this)
}
+ private fun setUpApplicationForContext() {
+ val packageManager = Shadows.shadowOf(context.packageManager)
+ val applicationInfo =
+ ApplicationInfoBuilder.newBuilder()
+ .setPackageName(context.packageName)
+ .build()
+ val packageInfo =
+ PackageInfoBuilder.newBuilder()
+ .setPackageName(context.packageName)
+ .setApplicationInfo(applicationInfo)
+ .build()
+ packageInfo.versionName = TEST_APP_VERSION_NAME
+ @Suppress("DEPRECATION") // versionCode is needed to test production code.
+ packageInfo.versionCode = TEST_APP_VERSION_CODE
+ packageManager.installPackage(packageInfo)
+ }
+
@Module
class TestModule {
@Provides
@Singleton
- fun provideContext(application: Application): Context {
- return application
- }
+ fun provideContext(application: Application): Context = application
+
+ @Provides
+ @Singleton
+ fun provideMockWebServer() = MockWebServer().also { it.start() }
+
+ @Provides
+ @BaseUrl
+ fun provideNetworkBaseUrl(mockWebServer: MockWebServer): String =
+ mockWebServer.url("/").toUrl().toString()
+
+ @Provides
+ @XssiPrefix
+ fun provideXssiPrefix() = XSSI_PREFIX
}
@Singleton
@Component(
modules = [
- TestModule::class, NetworkModule::class, NetworkConfigProdModule::class,
- TestDispatcherModule::class, RobolectricModule::class
+ TestModule::class, NetworkModule::class, TestDispatcherModule::class, RobolectricModule::class
]
)
@@ -93,9 +267,6 @@ class NetworkModuleTest {
}
fun inject(networkModuleTest: NetworkModuleTest)
- @OppiaRetrofit fun getRetrofit(): Optional
- fun getFeedbackReportingService(): Optional
- fun getPlatformParameterService(): Optional
}
class TestApplication : Application() {
@@ -108,9 +279,26 @@ class NetworkModuleTest {
fun inject(networkModuleTest: NetworkModuleTest) {
component.inject(networkModuleTest)
}
+ }
+
+ interface TestService {
+ @GET("test_path/test_object_handler")
+ // TODO(#76): Update return payload for handling storage failures once retry policy is defined.
+ fun fetchTestObject(): Call
+
+ @GET("test_path/test_nothing_handler")
+ fun fetchNothing(): Call
+ }
+
+ @JsonClass(generateAdapter = true)
+ data class TestMoshiObject(
+ @Json(name = "field1") val field1: String,
+ @Json(name = "field2") val field2: Int
+ )
- fun getRetrofit() = component.getRetrofit()
- fun getFeedbackReportingService() = component.getFeedbackReportingService()
- fun getPlatformParameterService() = component.getPlatformParameterService()
+ private companion object {
+ private const val XSSI_PREFIX = ")]}'"
+ private const val TEST_APP_VERSION_NAME = "oppia-android-test-0123456789"
+ private const val TEST_APP_VERSION_CODE = 1
}
}
diff --git a/domain/src/main/java/org/oppia/android/domain/platformparameter/syncup/PlatformParameterSyncUpWorker.kt b/domain/src/main/java/org/oppia/android/domain/platformparameter/syncup/PlatformParameterSyncUpWorker.kt
index 50422db85a2..bfbd6bc3905 100644
--- a/domain/src/main/java/org/oppia/android/domain/platformparameter/syncup/PlatformParameterSyncUpWorker.kt
+++ b/domain/src/main/java/org/oppia/android/domain/platformparameter/syncup/PlatformParameterSyncUpWorker.kt
@@ -3,7 +3,6 @@ package org.oppia.android.domain.platformparameter.syncup
import android.content.Context
import androidx.work.ListenableWorker
import androidx.work.WorkerParameters
-import com.google.common.base.Optional
import com.google.common.util.concurrent.ListenableFuture
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
@@ -29,7 +28,7 @@ class PlatformParameterSyncUpWorker private constructor(
context: Context,
params: WorkerParameters,
private val platformParameterController: PlatformParameterController,
- private val platformParameterService: Optional,
+ private val platformParameterService: PlatformParameterService,
private val oppiaLogger: OppiaLogger,
private val exceptionsController: ExceptionsController,
@BackgroundDispatcher private val backgroundDispatcher: CoroutineDispatcher
@@ -85,19 +84,16 @@ class PlatformParameterSyncUpWorker private constructor(
/**
* Synchronously executes the network request to get platform parameters from the Oppia backend.
*/
- private fun makeNetworkCallForPlatformParameters(): Optional>?> {
- return platformParameterService.transform { service ->
- service?.getPlatformParametersByVersion(
- applicationContext.getVersionName()
- )?.execute()
- }
+ private fun makeNetworkCallForPlatformParameters(): Response