diff --git a/apk-viewer-plugin/src/main/res/layout/fragment_sample.xml b/apk-viewer-plugin/src/main/res/layout/fragment_sample.xml index def4089cab..07435a0732 100644 --- a/apk-viewer-plugin/src/main/res/layout/fragment_sample.xml +++ b/apk-viewer-plugin/src/main/res/layout/fragment_sample.xml @@ -2,6 +2,7 @@ + style="?android:attr/buttonBarButtonStyle" + android:text="Start Analyzing" + android:textColor="?android:attr/textColorSecondary" /> + + #B1C5FF + #172E60 + #304578 + #DAE2FF + + #E4E2E6 + #44464F + + #81C784 + #1B3A1B + + #F2B8B5 + #8C1D18 + diff --git a/apk-viewer-plugin/src/main/res/values/colors.xml b/apk-viewer-plugin/src/main/res/values/colors.xml new file mode 100644 index 0000000000..eb2b2edfae --- /dev/null +++ b/apk-viewer-plugin/src/main/res/values/colors.xml @@ -0,0 +1,16 @@ + + + #485D92 + #FFFFFF + #DAE2FF + #001847 + + #1B1B1F + #E1E2EC + + #2E7D32 + #E8F5E9 + + #B3261E + #F9DEDC + diff --git a/apk-viewer-plugin/src/main/res/values/styles.xml b/apk-viewer-plugin/src/main/res/values/styles.xml new file mode 100644 index 0000000000..fdc86fa776 --- /dev/null +++ b/apk-viewer-plugin/src/main/res/values/styles.xml @@ -0,0 +1,11 @@ + + + + diff --git a/app/src/main/java/com/itsaky/androidide/ui/themes/ThemeManager.kt b/app/src/main/java/com/itsaky/androidide/ui/themes/ThemeManager.kt index 947b9152aa..c8be8b69c8 100644 --- a/app/src/main/java/com/itsaky/androidide/ui/themes/ThemeManager.kt +++ b/app/src/main/java/com/itsaky/androidide/ui/themes/ThemeManager.kt @@ -18,6 +18,7 @@ package com.itsaky.androidide.ui.themes import android.app.Activity +import androidx.appcompat.app.AppCompatDelegate import com.google.auto.service.AutoService import com.itsaky.androidide.preferences.internal.GeneralPreferences import com.itsaky.androidide.utils.isSystemInDarkMode diff --git a/keystore-generator-plugin/src/main/AndroidManifest.xml b/keystore-generator-plugin/src/main/AndroidManifest.xml index 888d9b810d..3bcb72d61f 100644 --- a/keystore-generator-plugin/src/main/AndroidManifest.xml +++ b/keystore-generator-plugin/src/main/AndroidManifest.xml @@ -16,7 +16,7 @@ + android:value="1.0.2" /> (android.R.id.content) // Use root view statusContainer.setOnLongClickListener { view -> tooltipService?.showTooltip( anchorView = view, @@ -342,8 +340,9 @@ class KeystoreGeneratorFragment : Fragment(), BuildStatusListener { progressBar.visibility = View.VISIBLE statusContainer.visibility = View.VISIBLE statusText.text = message - statusText.setTextColor(ContextCompat.getColor(requireContext(), android.R.color.primary_text_light)) - statusContainer.setBackgroundColor(ContextCompat.getColor(requireContext(), android.R.color.background_light)) + val ctx = statusContainer.context + statusText.setTextColor(ContextCompat.getColor(ctx, R.color.status_text)) + statusContainer.setBackgroundColor(ContextCompat.getColor(ctx, R.color.status_background)) } private fun hideProgress() { @@ -353,15 +352,17 @@ class KeystoreGeneratorFragment : Fragment(), BuildStatusListener { private fun showSuccess(message: String) { statusContainer.visibility = View.VISIBLE statusText.text = message - statusText.setTextColor(Color.parseColor("#4CAF50")) - statusContainer.setBackgroundColor(Color.parseColor("#E8F5E8")) + val ctx = statusContainer.context + statusText.setTextColor(ContextCompat.getColor(ctx, R.color.status_success_text)) + statusContainer.setBackgroundColor(ContextCompat.getColor(ctx, R.color.status_success_background)) } private fun showError(message: String) { statusContainer.visibility = View.VISIBLE statusText.text = message - statusText.setTextColor(Color.parseColor("#F44336")) - statusContainer.setBackgroundColor(Color.parseColor("#FFF3F3")) + val ctx = statusContainer.context + statusText.setTextColor(ContextCompat.getColor(ctx, R.color.status_error_text)) + statusContainer.setBackgroundColor(ContextCompat.getColor(ctx, R.color.status_error_background)) } private fun hideStatus() { diff --git a/keystore-generator-plugin/src/main/res/layout/fragment_keystore_generator.xml b/keystore-generator-plugin/src/main/res/layout/fragment_keystore_generator.xml index c1e074dedd..94c68c2eba 100644 --- a/keystore-generator-plugin/src/main/res/layout/fragment_keystore_generator.xml +++ b/keystore-generator-plugin/src/main/res/layout/fragment_keystore_generator.xml @@ -299,6 +299,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Generate Keystore" + android:textColor="?android:attr/textColorSecondary" android:textStyle="bold" /> diff --git a/keystore-generator-plugin/src/main/res/values-night/colors.xml b/keystore-generator-plugin/src/main/res/values-night/colors.xml new file mode 100644 index 0000000000..d78de65d82 --- /dev/null +++ b/keystore-generator-plugin/src/main/res/values-night/colors.xml @@ -0,0 +1,16 @@ + + + #B1C5FF + #172E60 + #304578 + #DAE2FF + + #E4E2E6 + #44464F + + #81C784 + #1B3A1B + + #F2B8B5 + #8C1D18 + diff --git a/keystore-generator-plugin/src/main/res/values/colors.xml b/keystore-generator-plugin/src/main/res/values/colors.xml new file mode 100644 index 0000000000..eb2b2edfae --- /dev/null +++ b/keystore-generator-plugin/src/main/res/values/colors.xml @@ -0,0 +1,16 @@ + + + #485D92 + #FFFFFF + #DAE2FF + #001847 + + #1B1B1F + #E1E2EC + + #2E7D32 + #E8F5E9 + + #B3261E + #F9DEDC + diff --git a/keystore-generator-plugin/src/main/res/values/styles.xml b/keystore-generator-plugin/src/main/res/values/styles.xml new file mode 100644 index 0000000000..fdc86fa776 --- /dev/null +++ b/keystore-generator-plugin/src/main/res/values/styles.xml @@ -0,0 +1,11 @@ + + + + diff --git a/plugin-api/src/main/kotlin/com/itsaky/androidide/plugins/base/PluginFragment.kt b/plugin-api/src/main/kotlin/com/itsaky/androidide/plugins/base/PluginFragment.kt index f206a9c6d1..c2a793162a 100644 --- a/plugin-api/src/main/kotlin/com/itsaky/androidide/plugins/base/PluginFragment.kt +++ b/plugin-api/src/main/kotlin/com/itsaky/androidide/plugins/base/PluginFragment.kt @@ -1,7 +1,6 @@ package com.itsaky.androidide.plugins.base import android.content.Context -import android.os.Bundle import android.view.LayoutInflater import com.itsaky.androidide.plugins.ServiceRegistry @@ -82,12 +81,11 @@ object PluginFragmentHelper { */ @JvmStatic fun getPluginInflater(pluginId: String, defaultInflater: LayoutInflater): LayoutInflater { - val pluginContext = getPluginContext(pluginId) - return if (pluginContext != null) { - val inflater = pluginContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as? LayoutInflater - inflater ?: defaultInflater.cloneInContext(pluginContext) - } else { - defaultInflater - } + val pluginContext = getPluginContext(pluginId) ?: return defaultInflater + + pluginContext.theme + + val inflater = pluginContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as? LayoutInflater + return inflater ?: defaultInflater.cloneInContext(pluginContext) } } \ No newline at end of file diff --git a/plugin-api/src/main/kotlin/com/itsaky/androidide/plugins/services/IdeThemeService.kt b/plugin-api/src/main/kotlin/com/itsaky/androidide/plugins/services/IdeThemeService.kt new file mode 100644 index 0000000000..0ab8cb2571 --- /dev/null +++ b/plugin-api/src/main/kotlin/com/itsaky/androidide/plugins/services/IdeThemeService.kt @@ -0,0 +1,15 @@ +package com.itsaky.androidide.plugins.services + +interface IdeThemeService { + + fun isDarkMode(): Boolean + + fun addThemeChangeListener(listener: ThemeChangeListener) + + fun removeThemeChangeListener(listener: ThemeChangeListener) +} + +interface ThemeChangeListener { + + fun onThemeChanged(isDarkMode: Boolean) +} diff --git a/plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/core/PluginManager.kt b/plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/core/PluginManager.kt index c5f5198b82..23fc24ddbc 100644 --- a/plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/core/PluginManager.kt +++ b/plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/core/PluginManager.kt @@ -33,6 +33,8 @@ import com.itsaky.androidide.plugins.services.IdeFileService import com.itsaky.androidide.plugins.services.IdeSidebarService import com.itsaky.androidide.plugins.manager.services.IdeFileServiceImpl import com.itsaky.androidide.plugins.manager.services.IdeSidebarServiceImpl +import com.itsaky.androidide.plugins.manager.services.IdeThemeServiceImpl +import com.itsaky.androidide.plugins.services.IdeThemeService import com.itsaky.androidide.actions.SidebarSlotManager import com.itsaky.androidide.actions.SidebarSlotExceededException import kotlinx.coroutines.CoroutineScope @@ -439,6 +441,11 @@ class PluginManager private constructor( loadedPlugin.plugin.deactivate() loadedPlugin.plugin.dispose() + val themeService = loadedPlugin.context.services.get(IdeThemeService::class.java) + if (themeService is IdeThemeServiceImpl) { + themeService.dispose() + } + // Unregister the plugin's resource context PluginFragmentHelper.unregisterPluginContext(pluginId) @@ -816,6 +823,15 @@ class PluginManager private constructor( IdeSidebarServiceImpl(pluginId) } + registerServiceWithErrorHandling( + pluginServiceRegistry, + IdeThemeService::class.java, + pluginId, + "theme" + ) { + IdeThemeServiceImpl(context) + } + // Create PluginContext with resource context return PluginContextImpl( androidContext = resourceContext, // Use the resource context instead of app context @@ -930,8 +946,14 @@ class PluginManager private constructor( IdeSidebarServiceImpl(pluginId) } - // Copy other services from global registry - // TODO: Add mechanism to copy global services to plugin-specific registry + registerServiceWithErrorHandling( + pluginServiceRegistry, + IdeThemeService::class.java, + pluginId, + "theme" + ) { + IdeThemeServiceImpl(context) + } return PluginContextImpl( androidContext = context, diff --git a/plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/loaders/PluginResourceContext.kt b/plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/loaders/PluginResourceContext.kt index f646cde9dc..e9c76e5d90 100644 --- a/plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/loaders/PluginResourceContext.kt +++ b/plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/loaders/PluginResourceContext.kt @@ -1,40 +1,28 @@ package com.itsaky.androidide.plugins.manager.loaders import android.content.Context -import android.content.ContextWrapper import android.content.pm.ApplicationInfo import android.content.pm.PackageInfo -import android.content.pm.PackageManager import android.content.res.AssetManager +import android.content.res.Configuration import android.content.res.Resources import android.content.res.Resources.Theme import android.util.AttributeSet import android.view.ContextThemeWrapper import android.view.LayoutInflater import android.view.View +import androidx.appcompat.app.AppCompatDelegate -/** - * Context wrapper that provides plugin-specific resources - */ class PluginResourceContext( baseContext: Context, - private val pluginResources: Resources, + pluginResources: Resources, private val pluginPackageInfo: PackageInfo? = null -) : ContextThemeWrapper(baseContext, android.R.style.Theme_Material_Light) { +) : ContextThemeWrapper(baseContext, 0) { + private var pluginResources: Resources = pluginResources private var inflater: LayoutInflater? = null - - init { - // Apply plugin's theme if it exists - val themeResId = pluginResources.getIdentifier( - "PluginTheme", - "style", - pluginPackageInfo?.packageName - ) - if (themeResId != 0) { - theme.applyStyle(themeResId, true) - } - } + private var lastNightMode: Int = -1 + private var pluginTheme: Theme? = null override fun getResources(): Resources { return pluginResources @@ -44,9 +32,54 @@ class PluginResourceContext( return pluginResources.assets } + private fun recreatePluginResources(newConfig: Configuration) { + val sourceDir = pluginPackageInfo?.applicationInfo?.sourceDir ?: return + @Suppress("DEPRECATION") + val assetManager = AssetManager::class.java.getDeclaredConstructor().newInstance() + val addAssetPath = AssetManager::class.java.getMethod("addAssetPath", String::class.java) + addAssetPath.invoke(assetManager, sourceDir) + @Suppress("DEPRECATION") + pluginResources = Resources(assetManager, baseContext.resources.displayMetrics, newConfig) + } + + private fun resolveCurrentNightMode(): Int { + return when (AppCompatDelegate.getDefaultNightMode()) { + AppCompatDelegate.MODE_NIGHT_YES -> Configuration.UI_MODE_NIGHT_YES + AppCompatDelegate.MODE_NIGHT_NO -> Configuration.UI_MODE_NIGHT_NO + else -> baseContext.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK + } + } + override fun getTheme(): Theme { - // Return the theme from ContextThemeWrapper which is properly initialized - return super.getTheme() + val currentNightMode = resolveCurrentNightMode() + + if (currentNightMode != lastNightMode || pluginTheme == null) { + lastNightMode = currentNightMode + val correctedConfig = Configuration(baseContext.resources.configuration).apply { + uiMode = (uiMode and Configuration.UI_MODE_NIGHT_MASK.inv()) or currentNightMode + } + recreatePluginResources(correctedConfig) + inflater = null + + val pluginThemeResId = pluginResources.getIdentifier( + "PluginTheme", + "style", + pluginPackageInfo?.packageName + ) + + val themeResId = if (pluginThemeResId != 0) { + pluginThemeResId + } else if (currentNightMode == Configuration.UI_MODE_NIGHT_YES) { + android.R.style.Theme_Material + } else { + android.R.style.Theme_Material_Light + } + + pluginTheme = pluginResources.newTheme().apply { + applyStyle(themeResId, true) + } + } + return pluginTheme!! } override fun getClassLoader(): ClassLoader { diff --git a/plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/services/IdeThemeServiceImpl.kt b/plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/services/IdeThemeServiceImpl.kt new file mode 100644 index 0000000000..29d8b91189 --- /dev/null +++ b/plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/services/IdeThemeServiceImpl.kt @@ -0,0 +1,55 @@ +package com.itsaky.androidide.plugins.manager.services + +import android.content.ComponentCallbacks +import android.content.Context +import android.content.res.Configuration +import androidx.appcompat.app.AppCompatDelegate +import com.itsaky.androidide.plugins.services.IdeThemeService +import com.itsaky.androidide.plugins.services.ThemeChangeListener +import com.itsaky.androidide.utils.isSystemInDarkMode + +class IdeThemeServiceImpl( + private val context: Context +) : IdeThemeService { + + private val listeners = mutableListOf() + private var lastKnownDarkMode: Boolean = false + + private val configCallback = object : ComponentCallbacks { + override fun onConfigurationChanged(newConfig: Configuration) { + val isDark = isDarkMode() + if (isDark != lastKnownDarkMode) { + lastKnownDarkMode = isDark + listeners.forEach { it.onThemeChanged(isDark) } + } + } + + override fun onLowMemory() {} + } + + init { + lastKnownDarkMode = isDarkMode() + context.registerComponentCallbacks(configCallback) + } + + override fun isDarkMode(): Boolean { + return when (AppCompatDelegate.getDefaultNightMode()) { + AppCompatDelegate.MODE_NIGHT_YES -> true + AppCompatDelegate.MODE_NIGHT_NO -> false + else -> context.isSystemInDarkMode() + } + } + + override fun addThemeChangeListener(listener: ThemeChangeListener) { + listeners.add(listener) + } + + override fun removeThemeChangeListener(listener: ThemeChangeListener) { + listeners.remove(listener) + } + + fun dispose() { + context.unregisterComponentCallbacks(configCallback) + listeners.clear() + } +}