diff --git a/app/src/main/java/org/oppia/android/app/administratorcontrols/AdministratorControlsActivityPresenter.kt b/app/src/main/java/org/oppia/android/app/administratorcontrols/AdministratorControlsActivityPresenter.kt index a7095bc04f3..79a8439ca61 100644 --- a/app/src/main/java/org/oppia/android/app/administratorcontrols/AdministratorControlsActivityPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/administratorcontrols/AdministratorControlsActivityPresenter.kt @@ -1,8 +1,13 @@ package org.oppia.android.app.administratorcontrols +import android.os.Build import android.os.Bundle import android.view.View import androidx.appcompat.app.AppCompatActivity +import androidx.core.content.ContextCompat +import androidx.core.view.ViewCompat +import androidx.core.view.WindowInsetsCompat +import androidx.core.view.updatePadding import androidx.databinding.DataBindingUtil import org.oppia.android.app.activity.ActivityScope import org.oppia.android.app.administratorcontrols.appversion.AppVersionFragment @@ -17,6 +22,8 @@ import org.oppia.android.app.settings.profile.ProfileListFragment import org.oppia.android.app.translation.AppLanguageResourceHandler import org.oppia.android.app.ui.R import org.oppia.android.util.extensions.putProto +import org.oppia.android.util.platformparameter.EnableEdgeToEdge +import org.oppia.android.util.platformparameter.PlatformParameterValue import org.oppia.android.util.profile.CurrentUserProfileIdIntentDecorator.decorateWithUserProfileId import javax.inject.Inject @@ -24,7 +31,8 @@ import javax.inject.Inject @ActivityScope class AdministratorControlsActivityPresenter @Inject constructor( private val activity: AppCompatActivity, - private val resourceHandler: AppLanguageResourceHandler + private val resourceHandler: AppLanguageResourceHandler, + @EnableEdgeToEdge private val enableEdgeToEdge: PlatformParameterValue ) { private lateinit var navigationDrawerFragment: NavigationDrawerFragment private var isMultipane = false @@ -47,6 +55,9 @@ class AdministratorControlsActivityPresenter @Inject constructor( R.layout.administrator_controls_activity ) setUpNavigationDrawer() + if (enableEdgeToEdge.value) { + applyEdgeToEdgeInsets() + } this.lastLoadedFragment = lastLoadedFragment this.selectedProfileId = selectedProfileId this.isProfileDeletionDialogVisible = isProfileDeletionDialogVisible @@ -199,6 +210,42 @@ class AdministratorControlsActivityPresenter @Inject constructor( binding.extraControlsTitle?.text = title } + private fun applyEdgeToEdgeInsets() { + val toolbar = binding.administratorControlsActivityToolbar + val contentLayout = toolbar.parent as android.widget.LinearLayout + val statusBarBackground = View(activity).apply { + setBackgroundColor( + ContextCompat.getColor( + activity, + R.color.component_color_shared_activity_status_bar_color + ) + ) + } + contentLayout.addView(statusBarBackground, 0) + ViewCompat.setOnApplyWindowInsetsListener(statusBarBackground) { view, insets -> + val systemBars = insets.getInsets( + WindowInsetsCompat.Type.systemBars() or + WindowInsetsCompat.Type.displayCutout() + ) + view.layoutParams.height = systemBars.top + view.requestLayout() + insets + } + ViewCompat.setOnApplyWindowInsetsListener( + binding.administratorControlsActivityDrawerLayout + ) { view, insets -> + val systemBars = insets.getInsets( + WindowInsetsCompat.Type.systemBars() or + WindowInsetsCompat.Type.displayCutout() + ) + view.updatePadding(bottom = systemBars.bottom) + insets + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + activity.window.isNavigationBarContrastEnforced = false + } + } + /** Saves the state of the views on configuration changes. */ fun handleOnSaveInstanceState(outState: Bundle) { val titleTextView = binding.extraControlsTitle diff --git a/app/src/main/java/org/oppia/android/app/classroom/ClassroomListActivityPresenter.kt b/app/src/main/java/org/oppia/android/app/classroom/ClassroomListActivityPresenter.kt index e887df1b8dc..7e05b4e459d 100644 --- a/app/src/main/java/org/oppia/android/app/classroom/ClassroomListActivityPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/classroom/ClassroomListActivityPresenter.kt @@ -1,18 +1,28 @@ package org.oppia.android.app.classroom +import android.os.Build import android.view.View import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.widget.Toolbar +import androidx.core.content.ContextCompat +import androidx.core.view.ViewCompat +import androidx.core.view.WindowInsetsCompat +import androidx.core.view.updatePadding import androidx.drawerlayout.widget.DrawerLayout import org.oppia.android.app.drawer.NavigationDrawerFragment import org.oppia.android.app.ui.R +import org.oppia.android.util.platformparameter.EnableEdgeToEdge +import org.oppia.android.util.platformparameter.PlatformParameterValue import javax.inject.Inject /** Tag for identifying the [ClassroomListFragment] in transactions. */ private const val TAG_CLASSROOM_LIST_FRAGMENT = "CLASSROOM_LIST_FRAGMENT" /** The presenter for [ClassroomListActivity]. */ -class ClassroomListActivityPresenter @Inject constructor(private val activity: AppCompatActivity) { +class ClassroomListActivityPresenter @Inject constructor( + private val activity: AppCompatActivity, + @EnableEdgeToEdge private val enableEdgeToEdge: PlatformParameterValue +) { private var navigationDrawerFragment: NavigationDrawerFragment? = null /** @@ -22,6 +32,9 @@ class ClassroomListActivityPresenter @Inject constructor(private val activity: A fun handleOnCreate() { activity.setContentView(R.layout.classroom_list_activity) setUpNavigationDrawer() + if (enableEdgeToEdge.value) { + applyEdgeToEdgeInsets() + } if (getClassroomListFragment() == null) { activity.supportFragmentManager.beginTransaction().add( R.id.classroom_list_fragment_placeholder, @@ -51,4 +64,43 @@ class ClassroomListActivityPresenter @Inject constructor(private val activity: A R.id.classroom_list_fragment_placeholder ) as ClassroomListFragment? } + + private fun applyEdgeToEdgeInsets() { + val toolbar = activity.findViewById( + R.id.classroom_list_activity_toolbar + ) + val contentLayout = toolbar.parent as android.widget.LinearLayout + val statusBarBackground = View(activity).apply { + setBackgroundColor( + ContextCompat.getColor( + activity, + R.color.component_color_shared_activity_status_bar_color + ) + ) + } + contentLayout.addView(statusBarBackground, 0) + ViewCompat.setOnApplyWindowInsetsListener(statusBarBackground) { view, insets -> + val systemBars = insets.getInsets( + WindowInsetsCompat.Type.systemBars() or + WindowInsetsCompat.Type.displayCutout() + ) + view.layoutParams.height = systemBars.top + view.requestLayout() + insets + } + val drawerLayout = activity.findViewById( + R.id.classroom_list_activity_drawer_layout + ) + ViewCompat.setOnApplyWindowInsetsListener(drawerLayout) { view, insets -> + val systemBars = insets.getInsets( + WindowInsetsCompat.Type.systemBars() or + WindowInsetsCompat.Type.displayCutout() + ) + view.updatePadding(bottom = systemBars.bottom) + insets + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + activity.window.isNavigationBarContrastEnforced = false + } + } } diff --git a/app/src/main/java/org/oppia/android/app/devoptions/DeveloperOptionsActivityPresenter.kt b/app/src/main/java/org/oppia/android/app/devoptions/DeveloperOptionsActivityPresenter.kt index d0fa5b177f4..aaee99a300d 100644 --- a/app/src/main/java/org/oppia/android/app/devoptions/DeveloperOptionsActivityPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/devoptions/DeveloperOptionsActivityPresenter.kt @@ -1,13 +1,21 @@ package org.oppia.android.app.devoptions import android.content.Intent +import android.os.Build +import android.view.View import androidx.appcompat.app.AppCompatActivity +import androidx.core.content.ContextCompat +import androidx.core.view.ViewCompat +import androidx.core.view.WindowInsetsCompat +import androidx.core.view.updatePadding import androidx.databinding.DataBindingUtil import org.oppia.android.app.activity.ActivityScope import org.oppia.android.app.databinding.databinding.DeveloperOptionsActivityBinding import org.oppia.android.app.drawer.NavigationDrawerFragment import org.oppia.android.app.splash.SplashActivity import org.oppia.android.app.ui.R +import org.oppia.android.util.platformparameter.EnableEdgeToEdge +import org.oppia.android.util.platformparameter.PlatformParameterValue import javax.inject.Inject import kotlin.system.exitProcess @@ -17,7 +25,8 @@ const val TAG_FORCE_DOWNLOAD_DIALOG = "FORCE_DOWNLOAD_DIALOG_TAG" /** The presenter for [DeveloperOptionsActivity]. */ @ActivityScope class DeveloperOptionsActivityPresenter @Inject constructor( - private val activity: AppCompatActivity + private val activity: AppCompatActivity, + @EnableEdgeToEdge private val enableEdgeToEdge: PlatformParameterValue ) { private lateinit var navigationDrawerFragment: NavigationDrawerFragment private lateinit var binding: DeveloperOptionsActivityBinding @@ -28,6 +37,9 @@ class DeveloperOptionsActivityPresenter @Inject constructor( R.layout.developer_options_activity ) setUpNavigationDrawer() + if (enableEdgeToEdge.value) { + applyEdgeToEdgeInsets() + } val previousFragment = getDeveloperOptionsFragment() if (previousFragment == null) { activity.supportFragmentManager.beginTransaction().add( @@ -71,6 +83,42 @@ class DeveloperOptionsActivityPresenter @Inject constructor( dialog.showNow(activity.supportFragmentManager, TAG_FORCE_DOWNLOAD_DIALOG) } + private fun applyEdgeToEdgeInsets() { + val toolbar = binding.developerOptionsActivityToolbar + val contentLayout = toolbar.parent as android.widget.LinearLayout + val statusBarBackground = View(activity).apply { + setBackgroundColor( + ContextCompat.getColor( + activity, + R.color.component_color_shared_activity_status_bar_color + ) + ) + } + contentLayout.addView(statusBarBackground, 0) + ViewCompat.setOnApplyWindowInsetsListener(statusBarBackground) { view, insets -> + val systemBars = insets.getInsets( + WindowInsetsCompat.Type.systemBars() or + WindowInsetsCompat.Type.displayCutout() + ) + view.layoutParams.height = systemBars.top + view.requestLayout() + insets + } + ViewCompat.setOnApplyWindowInsetsListener( + binding.developerOptionsActivityDrawerLayout + ) { view, insets -> + val systemBars = insets.getInsets( + WindowInsetsCompat.Type.systemBars() or + WindowInsetsCompat.Type.displayCutout() + ) + view.updatePadding(bottom = systemBars.bottom) + insets + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + activity.window.isNavigationBarContrastEnforced = false + } + } + /** Called when restart is triggered by [ForceDownloadRemoteParametersDialogFragment]. */ fun restartApp() { val intent = Intent(activity, SplashActivity::class.java).also { diff --git a/app/src/main/java/org/oppia/android/app/drawer/NavigationDrawerFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/drawer/NavigationDrawerFragmentPresenter.kt index 44a88f9cb39..714c6400eaf 100644 --- a/app/src/main/java/org/oppia/android/app/drawer/NavigationDrawerFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/drawer/NavigationDrawerFragmentPresenter.kt @@ -7,7 +7,10 @@ import android.view.ViewGroup import androidx.appcompat.app.ActionBarDrawerToggle import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.widget.Toolbar +import androidx.core.view.ViewCompat +import androidx.core.view.WindowInsetsCompat import androidx.core.view.forEach +import androidx.core.view.updatePadding import androidx.drawerlayout.widget.DrawerLayout import androidx.fragment.app.Fragment import androidx.lifecycle.LiveData @@ -38,6 +41,7 @@ import org.oppia.android.domain.profile.ProfileManagementController import org.oppia.android.domain.topic.TopicController import org.oppia.android.util.data.AsyncResult import org.oppia.android.util.data.DataProviders.Companion.toLiveData +import org.oppia.android.util.platformparameter.EnableEdgeToEdge import org.oppia.android.util.platformparameter.EnableMultipleClassrooms import org.oppia.android.util.platformparameter.PlatformParameterValue import org.oppia.android.util.profile.CurrentUserProfileIdIntentDecorator.extractCurrentUserProfileId @@ -58,6 +62,7 @@ class NavigationDrawerFragmentPresenter @Inject constructor( private val footerViewModel: NavigationDrawerFooterViewModel, private val developerOptionsStarter: Optional, @EnableMultipleClassrooms private val enableMultipleClassrooms: PlatformParameterValue, + @EnableEdgeToEdge private val enableEdgeToEdge: PlatformParameterValue, ) : NavigationView.OnNavigationItemSelectedListener { private lateinit var drawerToggle: ActionBarDrawerToggle private lateinit var drawerLayout: DrawerLayout @@ -93,9 +98,57 @@ class NavigationDrawerFragmentPresenter @Inject constructor( // TODO(#3382): Remove debug only code from prod build (also check imports, constructor and drawer_fragment.xml) setIfDeveloperOptionsMenuItemListener() + if (enableEdgeToEdge.value) { + applyEdgeToEdgeInsetsToDrawer() + } + return binding.root } + private fun applyEdgeToEdgeInsetsToDrawer() { + // Disable fitsSystemWindows on the inner NavigationView since we handle insets manually. + binding.fragmentDrawerNavView.fitsSystemWindows = false + + // Add a colored View behind the transparent status bar to match the drawer header color. + val drawerLinearLayout = + binding.drawerNestedScrollView.getChildAt(0) as android.widget.LinearLayout + val statusBarBackground = android.view.View(activity).apply { + setBackgroundColor( + androidx.core.content.ContextCompat.getColor( + activity, + R.color.component_color_shared_slide_drawer_open_status_bar_color + ) + ) + } + drawerLinearLayout.addView(statusBarBackground, 0) + ViewCompat.setOnApplyWindowInsetsListener(statusBarBackground) { view, insets -> + val systemBars = insets.getInsets( + WindowInsetsCompat.Type.systemBars() or WindowInsetsCompat.Type.displayCutout() + ) + view.layoutParams.height = systemBars.top + view.requestLayout() + insets + } + + // Override the inner NavigationView's insets listener to prevent ScrimInsetsFrameLayout's + // constructor-set listener from storing insets and drawing the default #4000 scrim in + // system bar inset areas. Setting fitsSystemWindows=false alone is not sufficient because + // the OnApplyWindowInsetsListener set in ScrimInsetsFrameLayout's constructor is + // independent of the fitsSystemWindows flag. + ViewCompat.setOnApplyWindowInsetsListener(binding.fragmentDrawerNavView) { _, insets -> + insets + } + + // Handle bottom padding for the navigation bar. + ViewCompat.setOnApplyWindowInsetsListener(binding.root) { view, insets -> + val systemBarInsets = insets.getInsets( + WindowInsetsCompat.Type.systemBars() or WindowInsetsCompat.Type.displayCutout() + ) + view.updatePadding(bottom = systemBarInsets.bottom) + insets + } + } + // TODO(#3382): Remove debug only code from prod build (also check imports, constructor and drawer_fragment.xml) private fun setIfDeveloperOptionsMenuItemListener() { developerOptionsStarter.asSet().forEach { starter -> @@ -409,11 +462,13 @@ class NavigationDrawerFragmentPresenter @Inject constructor( override fun onDrawerOpened(drawerView: View) { super.onDrawerOpened(drawerView) fragment.requireActivity().invalidateOptionsMenu() - StatusBarColor.statusBarColorUpdate( - R.color.component_color_shared_slide_drawer_open_status_bar_color, - activity, - false - ) + if (!enableEdgeToEdge.value) { + StatusBarColor.statusBarColorUpdate( + R.color.component_color_shared_slide_drawer_open_status_bar_color, + activity, + false + ) + } } override fun onDrawerClosed(drawerView: View) { @@ -421,11 +476,13 @@ class NavigationDrawerFragmentPresenter @Inject constructor( // It's possible in some rare cases for the activity to be gone while the drawer is // closing (possibly an out-of-lifecycle call from the AndroidX component). fragment.activity?.invalidateOptionsMenu() - StatusBarColor.statusBarColorUpdate( - R.color.component_color_shared_activity_status_bar_color, - activity, - false - ) + if (!enableEdgeToEdge.value) { + StatusBarColor.statusBarColorUpdate( + R.color.component_color_shared_activity_status_bar_color, + activity, + false + ) + } } } drawerLayout.addDrawerListener(drawerToggle) @@ -448,21 +505,25 @@ class NavigationDrawerFragmentPresenter @Inject constructor( override fun onDrawerOpened(drawerView: View) { super.onDrawerOpened(drawerView) fragment.requireActivity().invalidateOptionsMenu() - StatusBarColor.statusBarColorUpdate( - R.color.component_color_shared_slide_drawer_open_status_bar_color, - activity, - false - ) + if (!enableEdgeToEdge.value) { + StatusBarColor.statusBarColorUpdate( + R.color.component_color_shared_slide_drawer_open_status_bar_color, + activity, + false + ) + } } override fun onDrawerClosed(drawerView: View) { super.onDrawerClosed(drawerView) fragment.requireActivity().invalidateOptionsMenu() - StatusBarColor.statusBarColorUpdate( - R.color.component_color_shared_activity_status_bar_color, - activity, - false - ) + if (!enableEdgeToEdge.value) { + StatusBarColor.statusBarColorUpdate( + R.color.component_color_shared_activity_status_bar_color, + activity, + false + ) + } } } drawerLayout.addDrawerListener(drawerToggle) diff --git a/app/src/main/java/org/oppia/android/app/help/HelpActivityPresenter.kt b/app/src/main/java/org/oppia/android/app/help/HelpActivityPresenter.kt index 6428ada3b35..65b2096a9b0 100644 --- a/app/src/main/java/org/oppia/android/app/help/HelpActivityPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/help/HelpActivityPresenter.kt @@ -1,5 +1,6 @@ package org.oppia.android.app.help +import android.os.Build import android.os.Bundle import android.view.View import android.widget.FrameLayout @@ -7,6 +8,10 @@ import android.widget.ImageButton import android.widget.TextView import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.widget.Toolbar +import androidx.core.content.ContextCompat +import androidx.core.view.ViewCompat +import androidx.core.view.WindowInsetsCompat +import androidx.core.view.updatePadding import androidx.drawerlayout.widget.DrawerLayout import androidx.fragment.app.Fragment import org.oppia.android.app.activity.ActivityScope @@ -24,13 +29,16 @@ import org.oppia.android.app.policies.PoliciesFragment import org.oppia.android.app.translation.AppLanguageResourceHandler import org.oppia.android.app.ui.R import org.oppia.android.util.extensions.putProto +import org.oppia.android.util.platformparameter.EnableEdgeToEdge +import org.oppia.android.util.platformparameter.PlatformParameterValue import javax.inject.Inject /** The presenter for [HelpActivity]. */ @ActivityScope class HelpActivityPresenter @Inject constructor( private val activity: AppCompatActivity, - private val resourceHandler: AppLanguageResourceHandler + private val resourceHandler: AppLanguageResourceHandler, + @EnableEdgeToEdge private val enableEdgeToEdge: PlatformParameterValue ) { private lateinit var navigationDrawerFragment: NavigationDrawerFragment private lateinit var toolbar: Toolbar @@ -61,6 +69,9 @@ class HelpActivityPresenter @Inject constructor( activity.setContentView(R.layout.help_activity) setUpToolbar() setUpNavigationDrawer() + if (enableEdgeToEdge.value) { + applyEdgeToEdgeInsets(R.id.help_activity_drawer_layout) + } } else { activity.setContentView(R.layout.help_without_drawer_activity) setUpToolbar() @@ -68,6 +79,9 @@ class HelpActivityPresenter @Inject constructor( toolbar.setNavigationOnClickListener { activity.finish() } + if (enableEdgeToEdge.value) { + applyEdgeToEdgeInsets(drawerLayoutId = null) + } } val titleTextView = activity.findViewById(R.id.options_activity_selected_options_title) @@ -354,4 +368,40 @@ class HelpActivityPresenter @Inject constructor( selectedFragmentTag = POLICIES_FRAGMENT_TAG selectedHelpOptionTitle = getMultipaneContainerTitle() } + + private fun applyEdgeToEdgeInsets(drawerLayoutId: Int?) { + val statusBarBackground = View(activity).apply { + setBackgroundColor( + ContextCompat.getColor( + activity, + R.color.component_color_shared_activity_status_bar_color + ) + ) + } + val contentLayout = toolbar.parent as android.widget.LinearLayout + contentLayout.addView(statusBarBackground, 0) + ViewCompat.setOnApplyWindowInsetsListener(statusBarBackground) { view, insets -> + val systemBars = insets.getInsets( + WindowInsetsCompat.Type.systemBars() or + WindowInsetsCompat.Type.displayCutout() + ) + view.layoutParams.height = systemBars.top + view.requestLayout() + insets + } + if (drawerLayoutId != null) { + val drawerLayout = activity.findViewById(drawerLayoutId) + ViewCompat.setOnApplyWindowInsetsListener(drawerLayout) { view, insets -> + val systemBars = insets.getInsets( + WindowInsetsCompat.Type.systemBars() or + WindowInsetsCompat.Type.displayCutout() + ) + view.updatePadding(bottom = systemBars.bottom) + insets + } + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + activity.window.isNavigationBarContrastEnforced = false + } + } } diff --git a/app/src/main/java/org/oppia/android/app/home/HomeActivityPresenter.kt b/app/src/main/java/org/oppia/android/app/home/HomeActivityPresenter.kt index 98455074306..482a4347c8d 100644 --- a/app/src/main/java/org/oppia/android/app/home/HomeActivityPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/home/HomeActivityPresenter.kt @@ -1,26 +1,38 @@ package org.oppia.android.app.home +import android.os.Build import android.view.View import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.widget.Toolbar +import androidx.core.view.ViewCompat +import androidx.core.view.WindowInsetsCompat +import androidx.core.view.updatePadding import androidx.drawerlayout.widget.DrawerLayout import org.oppia.android.app.activity.ActivityScope import org.oppia.android.app.drawer.NavigationDrawerFragment import org.oppia.android.app.spotlight.SpotlightFragment import org.oppia.android.app.spotlight.SpotlightManager import org.oppia.android.app.ui.R +import org.oppia.android.util.platformparameter.EnableEdgeToEdge +import org.oppia.android.util.platformparameter.PlatformParameterValue import javax.inject.Inject const val TAG_HOME_FRAGMENT = "HOME_FRAGMENT" /** The presenter for [HomeActivity]. */ @ActivityScope -class HomeActivityPresenter @Inject constructor(private val activity: AppCompatActivity) { +class HomeActivityPresenter @Inject constructor( + private val activity: AppCompatActivity, + @EnableEdgeToEdge private val enableEdgeToEdge: PlatformParameterValue +) { private var navigationDrawerFragment: NavigationDrawerFragment? = null fun handleOnCreate(internalProfileId: Int) { activity.setContentView(R.layout.home_activity) setUpNavigationDrawer() + if (enableEdgeToEdge.value) { + applyEdgeToEdgeInsets() + } if (getHomeFragment() == null) { activity.supportFragmentManager.beginTransaction().add( R.id.home_fragment_placeholder, @@ -62,4 +74,43 @@ class HomeActivityPresenter @Inject constructor(private val activity: AppCompatA R.id.home_spotlight_fragment_placeholder ) as? SpotlightFragment } + + private fun applyEdgeToEdgeInsets() { + val toolbar = activity.findViewById(R.id.home_activity_toolbar) + val contentLayout = toolbar.parent as android.widget.LinearLayout + + // Add a colored View behind the transparent status bar to restore the darker status bar color. + val statusBarBackground = View(activity).apply { + setBackgroundColor( + androidx.core.content.ContextCompat.getColor( + activity, + R.color.component_color_shared_activity_status_bar_color + ) + ) + } + contentLayout.addView(statusBarBackground, 0) + ViewCompat.setOnApplyWindowInsetsListener(statusBarBackground) { view, insets -> + val systemBars = insets.getInsets( + WindowInsetsCompat.Type.systemBars() or WindowInsetsCompat.Type.displayCutout() + ) + view.layoutParams.height = systemBars.top + view.requestLayout() + insets + } + + val drawerLayout = + activity.findViewById(R.id.home_activity_drawer_layout) + ViewCompat.setOnApplyWindowInsetsListener(drawerLayout) { view, insets -> + val systemBarInsets = insets.getInsets( + WindowInsetsCompat.Type.systemBars() or WindowInsetsCompat.Type.displayCutout() + ) + view.updatePadding(bottom = systemBarInsets.bottom) + insets + } + + // Make the navigation bar fully transparent instead of translucent. + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + activity.window.isNavigationBarContrastEnforced = false + } + } } diff --git a/app/src/main/java/org/oppia/android/app/options/OptionsActivityPresenter.kt b/app/src/main/java/org/oppia/android/app/options/OptionsActivityPresenter.kt index cbd6515d5a9..cf2d9c3b90f 100644 --- a/app/src/main/java/org/oppia/android/app/options/OptionsActivityPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/options/OptionsActivityPresenter.kt @@ -1,10 +1,15 @@ package org.oppia.android.app.options +import android.os.Build import android.view.View import android.widget.FrameLayout import android.widget.TextView import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.widget.Toolbar +import androidx.core.content.ContextCompat +import androidx.core.view.ViewCompat +import androidx.core.view.WindowInsetsCompat +import androidx.core.view.updatePadding import androidx.drawerlayout.widget.DrawerLayout import org.oppia.android.app.activity.ActivityScope import org.oppia.android.app.drawer.NavigationDrawerFragment @@ -14,12 +19,15 @@ import org.oppia.android.app.model.LegacyProfileId import org.oppia.android.app.model.OppiaLanguage import org.oppia.android.app.model.ReadingTextSize import org.oppia.android.app.ui.R +import org.oppia.android.util.platformparameter.EnableEdgeToEdge +import org.oppia.android.util.platformparameter.PlatformParameterValue import javax.inject.Inject /** The presenter for [OptionsActivity]. */ @ActivityScope class OptionsActivityPresenter @Inject constructor( - private val activity: AppCompatActivity + private val activity: AppCompatActivity, + @EnableEdgeToEdge private val enableEdgeToEdge: PlatformParameterValue ) { private var navigationDrawerFragment: NavigationDrawerFragment? = null private lateinit var toolbar: Toolbar @@ -37,6 +45,9 @@ class OptionsActivityPresenter @Inject constructor( activity.setContentView(R.layout.option_activity) setUpToolbar() setUpNavigationDrawer() + if (enableEdgeToEdge.value) { + applyEdgeToEdgeInsets(R.id.options_activity_drawer_layout) + } } else { activity.setContentView(R.layout.options_without_drawer_activity) setUpToolbar() @@ -44,6 +55,9 @@ class OptionsActivityPresenter @Inject constructor( toolbar.setNavigationOnClickListener { activity.finish() } + if (enableEdgeToEdge.value) { + applyEdgeToEdgeInsets(drawerLayoutId = null) + } } val titleTextView = activity.findViewById(R.id.options_activity_selected_options_title) @@ -154,4 +168,40 @@ class OptionsActivityPresenter @Inject constructor( fun setExtraOptionTitle(title: String) { activity.findViewById(R.id.options_activity_selected_options_title).text = title } + + private fun applyEdgeToEdgeInsets(drawerLayoutId: Int?) { + val statusBarBackground = View(activity).apply { + setBackgroundColor( + ContextCompat.getColor( + activity, + R.color.component_color_shared_activity_status_bar_color + ) + ) + } + val contentLayout = toolbar.parent as android.widget.LinearLayout + contentLayout.addView(statusBarBackground, 0) + ViewCompat.setOnApplyWindowInsetsListener(statusBarBackground) { view, insets -> + val systemBars = insets.getInsets( + WindowInsetsCompat.Type.systemBars() or + WindowInsetsCompat.Type.displayCutout() + ) + view.layoutParams.height = systemBars.top + view.requestLayout() + insets + } + if (drawerLayoutId != null) { + val drawerLayout = activity.findViewById(drawerLayoutId) + ViewCompat.setOnApplyWindowInsetsListener(drawerLayout) { view, insets -> + val systemBars = insets.getInsets( + WindowInsetsCompat.Type.systemBars() or + WindowInsetsCompat.Type.displayCutout() + ) + view.updatePadding(bottom = systemBars.bottom) + insets + } + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + activity.window.isNavigationBarContrastEnforced = false + } + } } diff --git a/config/src/java/org/oppia/android/config/platform/feature_flags.textproto b/config/src/java/org/oppia/android/config/platform/feature_flags.textproto index 764763671e2..c566358d4d6 100644 --- a/config/src/java/org/oppia/android/config/platform/feature_flags.textproto +++ b/config/src/java/org/oppia/android/config/platform/feature_flags.textproto @@ -73,3 +73,8 @@ feature_flag_definition { remote_server_name: "android_enable_topic_practice_tab" default_is_enabled: false } +feature_flag_definition { + id: EDGE_TO_EDGE + remote_server_name: "android_enable_edge_to_edge" + default_is_enabled: true +} diff --git a/domain/src/main/java/org/oppia/android/domain/platformparameter/FeatureFlagBindingModule.kt b/domain/src/main/java/org/oppia/android/domain/platformparameter/FeatureFlagBindingModule.kt index 56137e6f915..af9b35161b9 100644 --- a/domain/src/main/java/org/oppia/android/domain/platformparameter/FeatureFlagBindingModule.kt +++ b/domain/src/main/java/org/oppia/android/domain/platformparameter/FeatureFlagBindingModule.kt @@ -5,6 +5,7 @@ import dagger.Provides import org.oppia.android.app.model.FeatureFlagId import org.oppia.android.util.platformparameter.EnableAppAndOsDeprecation import org.oppia.android.util.platformparameter.EnableDownloadsSupport +import org.oppia.android.util.platformparameter.EnableEdgeToEdge import org.oppia.android.util.platformparameter.EnableEditAccountsOptionsUi import org.oppia.android.util.platformparameter.EnableFastLanguageSwitchingInLesson import org.oppia.android.util.platformparameter.EnableFlashbackSupport @@ -101,6 +102,11 @@ class FeatureFlagBindingModule { fun provideEnableTopicPracticeTab(processState: PlatformParameterProcessState) = processState.retrieveFeatureFlag(FeatureFlagId.TOPIC_PRACTICE_TAB) + @Provides + @EnableEdgeToEdge + fun provideEnableEdgeToEdge(processState: PlatformParameterProcessState) = + processState.retrieveFeatureFlag(FeatureFlagId.EDGE_TO_EDGE) + private companion object { private fun PlatformParameterProcessState.retrieveFeatureFlag( featureFlagId: FeatureFlagId diff --git a/domain/src/main/java/org/oppia/android/domain/platformparameter/FeatureFlagsMapBindingModule.kt b/domain/src/main/java/org/oppia/android/domain/platformparameter/FeatureFlagsMapBindingModule.kt index 31583782cf0..2a3f9f1dad4 100644 --- a/domain/src/main/java/org/oppia/android/domain/platformparameter/FeatureFlagsMapBindingModule.kt +++ b/domain/src/main/java/org/oppia/android/domain/platformparameter/FeatureFlagsMapBindingModule.kt @@ -6,6 +6,7 @@ import dagger.multibindings.IntoMap import org.oppia.android.app.model.FeatureFlagId import org.oppia.android.util.platformparameter.EnableAppAndOsDeprecation import org.oppia.android.util.platformparameter.EnableDownloadsSupport +import org.oppia.android.util.platformparameter.EnableEdgeToEdge import org.oppia.android.util.platformparameter.EnableEditAccountsOptionsUi import org.oppia.android.util.platformparameter.EnableFastLanguageSwitchingInLesson import org.oppia.android.util.platformparameter.EnableFlashbackSupport @@ -144,4 +145,12 @@ interface FeatureFlagsMapBindingModule { fun bindTopicPracticeTab( @EnableTopicPracticeTab param: PlatformParameterValue ): PlatformParameterValue + + @Binds + @IntoMap + @FeatureFlags + @FeatureFlagIdKey(FeatureFlagId.EDGE_TO_EDGE) + fun bindEdgeToEdge( + @EnableEdgeToEdge param: PlatformParameterValue + ): PlatformParameterValue } diff --git a/domain/src/test/java/org/oppia/android/domain/oppialogger/analytics/FeatureFlagsLoggerTest.kt b/domain/src/test/java/org/oppia/android/domain/oppialogger/analytics/FeatureFlagsLoggerTest.kt index 3bac46f37f0..613852b4b42 100644 --- a/domain/src/test/java/org/oppia/android/domain/oppialogger/analytics/FeatureFlagsLoggerTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/oppialogger/analytics/FeatureFlagsLoggerTest.kt @@ -141,7 +141,7 @@ class FeatureFlagsLoggerTest { @Test fun testLogFeatureFlags_correctNumberOfFeatureFlagsIsLogged() { - val expectedFeatureFlagCount = 15 + val expectedFeatureFlagCount = 16 featureFlagsLogger.logAllFeatureFlags(TEST_SESSION_ID) testCoroutineDispatchers.runCurrent() @@ -168,6 +168,7 @@ class FeatureFlagsLoggerTest { @Iteration("flashback_support", "index=12", "flagId=FLASHBACK_SUPPORT") @Iteration("topic_info_tab", "index=13", "flagId=TOPIC_INFO_TAB") @Iteration("topic_practice_tab", "index=14", "flagId=TOPIC_PRACTICE_TAB") + @Iteration("edge_to_edge", "index=15", "flagId=EDGE_TO_EDGE") fun testLogFeatureFlags_allFeatureFlagIdsAreLogged() { featureFlagsLogger.logAllFeatureFlags(TEST_SESSION_ID) diff --git a/model/src/main/proto/platform_parameter.proto b/model/src/main/proto/platform_parameter.proto index 6390feb98bc..6f447a93936 100644 --- a/model/src/main/proto/platform_parameter.proto +++ b/model/src/main/proto/platform_parameter.proto @@ -149,6 +149,9 @@ enum FeatureFlagId { // Corresponds to a topic activity feature that enables the practice tab. TOPIC_PRACTICE_TAB = 18; + + // Corresponds to a feature enabling edge-to-edge display support for Android 15+ compatibility. + EDGE_TO_EDGE = 19; } // Corresponds to a compile-time definition of a platform parameter which is a mechanism by which to diff --git a/testing/src/main/java/org/oppia/android/testing/platformparameter/TestPlatformParameterModule.kt b/testing/src/main/java/org/oppia/android/testing/platformparameter/TestPlatformParameterModule.kt index 8fe3503e6c3..9f54961bcb6 100644 --- a/testing/src/main/java/org/oppia/android/testing/platformparameter/TestPlatformParameterModule.kt +++ b/testing/src/main/java/org/oppia/android/testing/platformparameter/TestPlatformParameterModule.kt @@ -8,6 +8,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async import org.oppia.android.app.model.FeatureFlagId.APP_AND_OS_DEPRECATION import org.oppia.android.app.model.FeatureFlagId.DOWNLOADS_SUPPORT +import org.oppia.android.app.model.FeatureFlagId.EDGE_TO_EDGE import org.oppia.android.app.model.FeatureFlagId.EDIT_ACCOUNTS_OPTIONS_UI import org.oppia.android.app.model.FeatureFlagId.FAST_LANGUAGE_SWITCHING_IN_LESSON import org.oppia.android.app.model.FeatureFlagId.FLASHBACK_SUPPORT @@ -150,6 +151,10 @@ class TestPlatformParameterModule { TestPlatformParameterConfigRetriever.setFlagOverride(TOPIC_PRACTICE_TAB, value) } + fun forceEnableEdgeToEdge(value: Boolean) { + TestPlatformParameterConfigRetriever.setFlagOverride(EDGE_TO_EDGE, value) + } + fun reset() { TestPlatformParameterConfigRetriever.reset() } diff --git a/utility/src/main/java/org/oppia/android/util/platformparameter/FeatureFlagConstants.kt b/utility/src/main/java/org/oppia/android/util/platformparameter/FeatureFlagConstants.kt index 61fc10281f9..d7507544b05 100644 --- a/utility/src/main/java/org/oppia/android/util/platformparameter/FeatureFlagConstants.kt +++ b/utility/src/main/java/org/oppia/android/util/platformparameter/FeatureFlagConstants.kt @@ -201,3 +201,16 @@ const val ENABLE_TOPIC_PRACTICE_TAB = "enable_topic_practice_tab" /** Default value for the feature flag corresponding to [EnableTopicPracticeTab]. */ const val ENABLE_TOPIC_PRACTICE_TAB_DEFAULT_VALUE = false + +/** + * Qualifier for the feature flag that controls whether edge-to-edge display support + * is enabled for Android 15+ (API 35+) compatibility. + */ +@Qualifier +annotation class EnableEdgeToEdge + +/** Name of the feature flag that controls whether to enable edge-to-edge display support. */ +const val EDGE_TO_EDGE = "android_enable_edge_to_edge" + +/** Default value for the feature flag corresponding to [EnableEdgeToEdge]. */ +const val ENABLE_EDGE_TO_EDGE_DEFAULT_VALUE = false