Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,14 @@ android {
versionCode 6
versionName "1.0.5"

resourceConfigurations += ["en", "uk"]
// Only include supported languages to reduce APK size
// Must match languages available in LanguageFragment.kt
resourceConfigurations += [
"en", // English (default)
"bo", // Tibetan
"vi", // Vietnamese
"zh-rHK" // Chinese - Hong Kong
]

testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
Expand Down
27 changes: 27 additions & 0 deletions app/src/main/java/org/openedx/app/AppActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import android.os.Bundle
import android.view.View
import android.view.WindowManager
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.app.AppCompatDelegate
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat
Expand All @@ -27,6 +28,7 @@ import org.openedx.auth.presentation.logistration.LogistrationFragment
import org.openedx.auth.presentation.signin.SignInFragment
import org.openedx.core.ApiConstants
import org.openedx.core.data.storage.CorePreferences
import org.openedx.core.data.storage.ThemeMode
import org.openedx.core.presentation.global.InsetHolder
import org.openedx.core.presentation.global.WindowSizeHolder
import org.openedx.core.utils.Logger
Expand Down Expand Up @@ -102,6 +104,7 @@ class AppActivity : AppCompatActivity(), InsetHolder, WindowSizeHolder {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
applyThemeMode()
installSplashScreen()
binding = ActivityAppBinding.inflate(layoutInflater)
lifecycle.addObserver(viewModel)
Expand Down Expand Up @@ -276,6 +279,30 @@ class AppActivity : AppCompatActivity(), InsetHolder, WindowSizeHolder {
viewModel.makeExternalRoute(supportFragmentManager, deepLink)
}

private fun applyThemeMode() {
val themeMode = corePreferencesManager.themeMode

val nightMode = when (themeMode) {
ThemeMode.LIGHT -> AppCompatDelegate.MODE_NIGHT_NO
ThemeMode.DARK -> AppCompatDelegate.MODE_NIGHT_YES
ThemeMode.SYSTEM -> AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
}

AppCompatDelegate.setDefaultNightMode(nightMode)

// Also apply language
applyLanguage()
}

private fun applyLanguage() {
val languageCode = corePreferencesManager.appLanguage

if (languageCode.isNotEmpty()) {
val localeList = androidx.core.os.LocaleListCompat.forLanguageTags(languageCode)
AppCompatDelegate.setApplicationLocales(localeList)
}
}

companion object {
const val TOP_INSET = "topInset"
const val BOTTOM_INSET = "bottomInset"
Expand Down
10 changes: 10 additions & 0 deletions app/src/main/java/org/openedx/app/AppRouter.kt
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,9 @@ import org.openedx.profile.presentation.delete.DeleteProfileFragment
import org.openedx.profile.presentation.edit.EditProfileFragment
import org.openedx.profile.presentation.manageaccount.ManageAccountFragment
import org.openedx.profile.presentation.profile.ProfileFragment
import org.openedx.profile.presentation.settings.LanguageFragment
import org.openedx.profile.presentation.settings.SettingsFragment
import org.openedx.profile.presentation.settings.ThemeFragment
import org.openedx.profile.presentation.video.VideoSettingsFragment
import org.openedx.whatsnew.WhatsNewRouter
import org.openedx.whatsnew.presentation.whatsnew.WhatsNewFragment
Expand Down Expand Up @@ -441,6 +443,14 @@ class AppRouter :
override fun navigateToCoursesToSync(fm: FragmentManager) {
replaceFragmentWithBackStack(fm, CoursesToSyncFragment())
}

override fun navigateToThemeSettings(fm: FragmentManager) {
replaceFragmentWithBackStack(fm, ThemeFragment.newInstance())
}

override fun navigateToLanguageSettings(fm: FragmentManager) {
replaceFragmentWithBackStack(fm, LanguageFragment.newInstance())
}
// endregion

fun getVisibleFragment(fm: FragmentManager): Fragment? {
Expand Down
44 changes: 44 additions & 0 deletions app/src/main/java/org/openedx/app/MainFragment.kt
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
super.onViewCreated(view, savedInstanceState)

initViewPager()
fixBottomNavigationTextRendering()

binding.bottomNavView.setOnItemSelectedListener {
when (it.itemId) {
Expand Down Expand Up @@ -134,7 +135,50 @@
}
}

/**
* Fix Tibetan text rendering in bottom navigation.
* Moves Tibetan labels down to prevent overlapping with icons.
*/
private fun fixBottomNavigationTextRendering() {
binding.bottomNavView.post {
val bottomNavMenuView = binding.bottomNavView.getChildAt(0) as? android.view.ViewGroup
bottomNavMenuView?.let { menuView ->
for (i in 0 until menuView.childCount) {
val item = menuView.getChildAt(i) as? android.view.ViewGroup
item?.let { itemView ->
fixTextViewsRecursively(itemView)
}
}
}
}
}

/**
* Recursively find TextViews and fix Tibetan text rendering.
*/
private fun fixTextViewsRecursively(view: View) {
if (view is android.widget.TextView) {
// Check if text contains Tibetan characters (Unicode range U+0F00-U+0FFF)
val text = view.text?.toString() ?: ""
val hasTibetan = text.any { it.code in 0x0F00..0x0FFF }

Check warning

Code scanning / detekt

Report magic numbers. Magic number is a numeric literal that is not defined as a constant and hence it's unclear what the purpose of this number is. It's better to declare such numbers as constants and give them a proper name. By default, -1, 0, 1, and 2 are not considered to be magic numbers. Warning

This expression contains a magic number. Consider defining it to a well named constant.

Check warning

Code scanning / detekt

Report magic numbers. Magic number is a numeric literal that is not defined as a constant and hence it's unclear what the purpose of this number is. It's better to declare such numbers as constants and give them a proper name. By default, -1, 0, 1, and 2 are not considered to be magic numbers. Warning

This expression contains a magic number. Consider defining it to a well named constant.

if (hasTibetan) {
// Only apply fixes for Tibetan text
view.includeFontPadding = false

// Move text down 8dp to prevent overlap with icon
val density = resources.displayMetrics.density
view.translationY = 8 * density

Check warning

Code scanning / detekt

Report magic numbers. Magic number is a numeric literal that is not defined as a constant and hence it's unclear what the purpose of this number is. It's better to declare such numbers as constants and give them a proper name. By default, -1, 0, 1, and 2 are not considered to be magic numbers. Warning

This expression contains a magic number. Consider defining it to a well named constant.
}
} else if (view is android.view.ViewGroup) {
for (i in 0 until view.childCount) {
fixTextViewsRecursively(view.getChildAt(i))
}
}
}


companion object {

Check warning

Code scanning / detekt

Reports consecutive blank lines Warning

Needless blank line(s)
private const val ARG_COURSE_ID = "courseId"
private const val ARG_INFO_TYPE = "info_type"
private const val ARG_OPEN_TAB = "open_tab"
Expand Down
40 changes: 40 additions & 0 deletions app/src/main/java/org/openedx/app/OpenEdXApp.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package org.openedx.app

import android.app.Application
import androidx.appcompat.app.AppCompatDelegate
import com.braze.Braze
import com.braze.configuration.BrazeConfig
import com.braze.ui.BrazeDeeplinkHandler
Expand All @@ -14,6 +15,7 @@ import org.openedx.app.di.appModule
import org.openedx.app.di.networkingModule
import org.openedx.app.di.screenModule
import org.openedx.core.config.Config
import org.openedx.core.data.storage.ThemeMode
import org.openedx.firebase.OEXFirebaseAnalytics

class OpenEdXApp : Application() {
Expand All @@ -23,6 +25,8 @@ class OpenEdXApp : Application() {

override fun onCreate() {
super.onCreate()
applyThemeMode()

startKoin {
androidContext(this@OpenEdXApp)
modules(
Expand All @@ -31,6 +35,7 @@ class OpenEdXApp : Application() {
screenModule
)
}

if (config.getFirebaseConfig().enabled) {
FirebaseApp.initializeApp(this)
}
Expand Down Expand Up @@ -69,4 +74,39 @@ class OpenEdXApp : Application() {
pluginManager.addPlugin(OEXFirebaseAnalytics(context = this))
}
}

private fun applyThemeMode() {
val prefs = getSharedPreferences(BuildConfig.APPLICATION_ID, MODE_PRIVATE)
val themeModeString = prefs.getString("theme_mode", ThemeMode.SYSTEM.name)
?: ThemeMode.SYSTEM.name

val themeMode = try {
ThemeMode.valueOf(themeModeString)
} catch (_: Exception) {
ThemeMode.SYSTEM
}

val nightMode = when (themeMode) {
ThemeMode.LIGHT -> AppCompatDelegate.MODE_NIGHT_NO
ThemeMode.DARK -> AppCompatDelegate.MODE_NIGHT_YES
ThemeMode.SYSTEM -> AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
}

AppCompatDelegate.setDefaultNightMode(nightMode)
applyLanguage()
}

private fun applyLanguage() {
val prefs = getSharedPreferences(BuildConfig.APPLICATION_ID, MODE_PRIVATE)
val languageCode = prefs.getString("app_language", "") ?: ""

if (languageCode.isNotEmpty()) {
// Use AppCompatDelegate for proper locale handling
val localeList = androidx.core.os.LocaleListCompat.forLanguageTags(languageCode)
AppCompatDelegate.setApplicationLocales(localeList)
} else {
// Empty string means use system language
AppCompatDelegate.setApplicationLocales(androidx.core.os.LocaleListCompat.getEmptyLocaleList())
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,23 @@
override fun isCalendarSyncEventsDialogShown(courseName: String): Boolean =
getBoolean(courseName.replaceSpace("_"))

override var themeMode: org.openedx.core.data.storage.ThemeMode
set(value) {
saveString(THEME_MODE, value.name)
}
get() {
val v = getString(THEME_MODE, defValue = org.openedx.core.data.storage.ThemeMode.SYSTEM.name)
return try {
org.openedx.core.data.storage.ThemeMode.valueOf(v)
} catch (e: Exception) {

Check warning

Code scanning / detekt

The caught exception is swallowed. The original exception could be lost. Warning

The caught exception is swallowed. The original exception could be lost.
org.openedx.core.data.storage.ThemeMode.SYSTEM
}
}

override var appLanguage: String
get() = getString(APP_LANGUAGE, "")
set(value) = saveString(APP_LANGUAGE, value)

companion object {
private const val ACCESS_TOKEN = "access_token"
private const val REFRESH_TOKEN = "refresh_token"
Expand All @@ -239,5 +256,7 @@
private const val IS_RELATIVE_DATES_ENABLED = "IS_RELATIVE_DATES_ENABLED"
private const val HIDE_INACTIVE_COURSES = "HIDE_INACTIVE_COURSES"
private const val CALENDAR_USER = "CALENDAR_USER"
private const val THEME_MODE = "theme_mode"
private const val APP_LANGUAGE = "app_language"
}
}
1 change: 1 addition & 0 deletions app/src/main/java/org/openedx/app/di/ScreenModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,7 @@ val screenModule = module {
get(),
get(),
get(),
get(),
)
}
viewModel { ManageAccountViewModel(get(), get(), get(), get(), get()) }
Expand Down
10 changes: 7 additions & 3 deletions app/src/main/res/xml/locales_config.xml
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<locale-config xmlns:android="http://schemas.android.com/apk/res/android">
<locale android:name = "en"/>
<locale android:name = "uk"/>
</locale-config>
<!-- Default language -->
<locale android:name="en"/>
<!-- Supported languages with complete translations -->
<locale android:name="bo"/>
<locale android:name="vi"/>
<locale android:name="zh-HK"/>
</locale-config>
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ import org.openedx.core.data.model.User
import org.openedx.core.domain.model.AppConfig
import org.openedx.core.domain.model.VideoSettings

enum class ThemeMode {
LIGHT,
DARK,
SYSTEM
}

interface CorePreferences {
var accessToken: String
var refreshToken: String
Expand All @@ -14,6 +20,7 @@ interface CorePreferences {
var appConfig: AppConfig
var canResetAppDirectory: Boolean
var isRelativeDatesEnabled: Boolean

var themeMode: ThemeMode
var appLanguage: String
fun clearCorePreferences()
}
4 changes: 2 additions & 2 deletions core/src/main/java/org/openedx/core/ui/WebContentScreen.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import android.net.Uri
import android.webkit.WebResourceRequest
import android.webkit.WebView
import android.webkit.WebViewClient
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
Expand Down Expand Up @@ -36,6 +35,7 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import androidx.compose.ui.zIndex
import org.openedx.core.ui.theme.appColors
import org.openedx.core.ui.theme.getDarkThemeFromPreferences
import org.openedx.core.utils.EmailUtil
import org.openedx.foundation.extension.applyDarkModeIfEnabled
import org.openedx.foundation.extension.isEmailValid
Expand Down Expand Up @@ -132,7 +132,7 @@ private fun WebViewContent(
onWebPageLoaded: () -> Unit
) {
val context = LocalContext.current
val isDarkTheme = isSystemInDarkTheme()
val isDarkTheme = getDarkThemeFromPreferences()
AndroidView(
factory = {
WebView(context).apply {
Expand Down
23 changes: 22 additions & 1 deletion core/src/main/java/org/openedx/core/ui/theme/Theme.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.ui.platform.LocalContext
import org.koin.core.context.GlobalContext

Check warning

Code scanning / detekt

Detects unused imports Warning

Unused import
import android.content.Context
import org.openedx.core.data.storage.ThemeMode

private val DarkColorPalette = AppColors(
material = darkColors(
Expand Down Expand Up @@ -195,7 +199,7 @@

@OptIn(ExperimentalFoundationApi::class)
@Composable
fun OpenEdXTheme(darkTheme: Boolean = isSystemInDarkTheme(), content: @Composable () -> Unit) {
fun OpenEdXTheme(darkTheme: Boolean = getDarkThemeFromPreferences(), content: @Composable () -> Unit) {
val colors = if (darkTheme) {
DarkColorPalette
} else {
Expand All @@ -213,3 +217,20 @@
)
}
}

@Composable
fun getDarkThemeFromPreferences(): Boolean {
val context = LocalContext.current
val prefs = context.getSharedPreferences(context.packageName, Context.MODE_PRIVATE)
val value = prefs.getString("theme_mode", ThemeMode.SYSTEM.name) ?: ThemeMode.SYSTEM.name
val mode = try {
ThemeMode.valueOf(value)
} catch (e: Exception) {

Check warning

Code scanning / detekt

The caught exception is swallowed. The original exception could be lost. Warning

The caught exception is swallowed. The original exception could be lost.
ThemeMode.SYSTEM
}
return when (mode) {
ThemeMode.LIGHT -> false
ThemeMode.DARK -> true
ThemeMode.SYSTEM -> isSystemInDarkTheme()
}
}
9 changes: 9 additions & 0 deletions core/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -191,4 +191,13 @@
<string name="core_not_synced">Not Synced</string>
<string name="core_syncing_to_calendar">Syncing to calendar…</string>
<string name="core_next">Next</string>

<!-- Theme Options -->
<string name="core_theme">Theme</string>
<string name="core_theme_system">Use system theme</string>
<string name="core_theme_light">Light</string>
<string name="core_theme_dark">Dark</string>

<!-- Language Options -->
<string name="core_language">Language</string>
</resources>
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,8 @@ interface ProfileRouter {
fun navigateToManageAccount(fm: FragmentManager)

fun navigateToCoursesToSync(fm: FragmentManager)

fun navigateToThemeSettings(fm: FragmentManager)

fun navigateToLanguageSettings(fm: FragmentManager)
}
Loading
Loading