From 2c2f517e52eed4b5c40b69813439acb602ad8104 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 6 Jun 2019 18:34:14 +0200 Subject: [PATCH] Hot change of theme - WIP --- .../vector/riotredesign/VectorApplication.kt | 13 ++ .../vector/riotredesign/core/di/AppModule.kt | 5 + .../core/platform/ConfigurationViewModel.kt | 55 +++++++ .../core/platform/VectorBaseActivity.kt | 25 +++ .../configuration/VectorConfiguration.kt | 146 ++++++++++++++++++ .../VectorSettingsPreferencesFragment.kt | 15 +- .../features/themes/ThemeUtils.kt | 18 +-- 7 files changed, 260 insertions(+), 17 deletions(-) create mode 100644 vector/src/main/java/im/vector/riotredesign/core/platform/ConfigurationViewModel.kt create mode 100644 vector/src/main/java/im/vector/riotredesign/features/configuration/VectorConfiguration.kt diff --git a/vector/src/main/java/im/vector/riotredesign/VectorApplication.kt b/vector/src/main/java/im/vector/riotredesign/VectorApplication.kt index d2ae5d4a93..88cee67496 100644 --- a/vector/src/main/java/im/vector/riotredesign/VectorApplication.kt +++ b/vector/src/main/java/im/vector/riotredesign/VectorApplication.kt @@ -18,6 +18,7 @@ package im.vector.riotredesign import android.app.Application import android.content.Context +import android.content.res.Configuration import androidx.multidex.MultiDex import com.airbnb.epoxy.EpoxyAsyncUtil import com.airbnb.epoxy.EpoxyController @@ -27,10 +28,12 @@ import com.github.piasy.biv.loader.glide.GlideImageLoader import com.jakewharton.threetenabp.AndroidThreeTen import im.vector.matrix.android.api.Matrix import im.vector.riotredesign.core.di.AppModule +import im.vector.riotredesign.features.configuration.VectorConfiguration import im.vector.riotredesign.features.home.HomeModule import im.vector.riotredesign.features.rageshake.VectorFileLogger import im.vector.riotredesign.features.rageshake.VectorUncaughtExceptionHandler import im.vector.riotredesign.features.roomdirectory.RoomDirectoryModule +import org.koin.android.ext.android.inject import org.koin.log.EmptyLogger import org.koin.standalone.StandAloneContext.startKoin import timber.log.Timber @@ -38,6 +41,8 @@ import timber.log.Timber class VectorApplication : Application() { + val vectorConfiguration: VectorConfiguration by inject() + override fun onCreate() { super.onCreate() @@ -61,6 +66,8 @@ class VectorApplication : Application() { startKoin(listOf(appModule, homeModule, roomDirectoryModule), logger = EmptyLogger()) Matrix.getInstance().setApplicationFlavor(BuildConfig.FLAVOR_DESCRIPTION) + + vectorConfiguration.initConfiguration() } override fun attachBaseContext(base: Context) { @@ -68,4 +75,10 @@ class VectorApplication : Application() { MultiDex.install(this) } + override fun onConfigurationChanged(newConfig: Configuration?) { + super.onConfigurationChanged(newConfig) + + vectorConfiguration.onConfigurationChanged(newConfig) + } + } \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/core/di/AppModule.kt b/vector/src/main/java/im/vector/riotredesign/core/di/AppModule.kt index dc7cf99c58..dc3068cfa3 100644 --- a/vector/src/main/java/im/vector/riotredesign/core/di/AppModule.kt +++ b/vector/src/main/java/im/vector/riotredesign/core/di/AppModule.kt @@ -24,6 +24,7 @@ import im.vector.riotredesign.core.error.ErrorFormatter import im.vector.riotredesign.core.resources.LocaleProvider import im.vector.riotredesign.core.resources.StringArrayProvider import im.vector.riotredesign.core.resources.StringProvider +import im.vector.riotredesign.features.configuration.VectorConfiguration import im.vector.riotredesign.features.home.HomeRoomListObservableStore import im.vector.riotredesign.features.home.group.SelectedGroupStore import im.vector.riotredesign.features.home.room.list.AlphabeticalRoomComparator @@ -37,6 +38,10 @@ class AppModule(private val context: Context) { val definition = module { + single { + VectorConfiguration(context) + } + single { LocaleProvider(context.resources) } diff --git a/vector/src/main/java/im/vector/riotredesign/core/platform/ConfigurationViewModel.kt b/vector/src/main/java/im/vector/riotredesign/core/platform/ConfigurationViewModel.kt new file mode 100644 index 0000000000..af7d0e5d60 --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/core/platform/ConfigurationViewModel.kt @@ -0,0 +1,55 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotredesign.core.platform + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import im.vector.riotredesign.core.utils.LiveEvent +import im.vector.riotredesign.features.configuration.VectorConfiguration +import org.koin.standalone.KoinComponent +import org.koin.standalone.inject +import timber.log.Timber + +class ConfigurationViewModel : ViewModel(), KoinComponent { + + private val vectorConfiguration: VectorConfiguration by inject() + + private var currentConfigurationValue: String? = null + + private val _activityRestarter = MutableLiveData>() + val activityRestarter: LiveData> + get() = _activityRestarter + + + fun onActivityResumed() { + if (currentConfigurationValue == null) { + currentConfigurationValue = vectorConfiguration.getHash() + Timber.v("Configuration: init to $currentConfigurationValue") + } else { + val newHash = vectorConfiguration.getHash() + Timber.v("Configuration: newHash $newHash") + + if (newHash != currentConfigurationValue) { + Timber.v("Configuration: recreate the Activity") + currentConfigurationValue = newHash + + _activityRestarter.postValue(LiveEvent(Unit)) + } + } + } +} diff --git a/vector/src/main/java/im/vector/riotredesign/core/platform/VectorBaseActivity.kt b/vector/src/main/java/im/vector/riotredesign/core/platform/VectorBaseActivity.kt index de1f0df0d8..2514df5e38 100644 --- a/vector/src/main/java/im/vector/riotredesign/core/platform/VectorBaseActivity.kt +++ b/vector/src/main/java/im/vector/riotredesign/core/platform/VectorBaseActivity.kt @@ -16,6 +16,7 @@ package im.vector.riotredesign.core.platform +import android.content.Context import android.content.res.Configuration import android.os.Bundle import android.view.Menu @@ -24,6 +25,8 @@ import android.view.View import androidx.annotation.* import androidx.appcompat.widget.Toolbar import androidx.coordinatorlayout.widget.CoordinatorLayout +import androidx.lifecycle.Observer +import androidx.lifecycle.ViewModelProviders import butterknife.BindView import butterknife.ButterKnife import butterknife.Unbinder @@ -33,6 +36,7 @@ import com.google.android.material.snackbar.Snackbar import im.vector.riotredesign.BuildConfig import im.vector.riotredesign.R import im.vector.riotredesign.core.utils.toast +import im.vector.riotredesign.features.configuration.VectorConfiguration import im.vector.riotredesign.features.rageshake.BugReportActivity import im.vector.riotredesign.features.rageshake.BugReporter import im.vector.riotredesign.features.rageshake.RageShake @@ -41,6 +45,7 @@ import im.vector.riotredesign.receivers.DebugReceiver import im.vector.ui.themes.ActivityOtherThemes import io.reactivex.disposables.CompositeDisposable import io.reactivex.disposables.Disposable +import org.koin.android.ext.android.inject import timber.log.Timber @@ -58,6 +63,10 @@ abstract class VectorBaseActivity : BaseMvRxActivity() { * DATA * ========================================================================================== */ + private val vectorConfiguration: VectorConfiguration by inject() + + private lateinit var configurationViewModel: ConfigurationViewModel + private var unBinder: Unbinder? = null private var savedInstanceState: Bundle? = null @@ -70,6 +79,10 @@ abstract class VectorBaseActivity : BaseMvRxActivity() { private var rageShake: RageShake? = null + override fun attachBaseContext(base: Context) { + super.attachBaseContext(vectorConfiguration.getLocalisedContext(base)) + } + override fun onSaveInstanceState(outState: Bundle) { super.onSaveInstanceState(outState) restorables.forEach { it.onSaveInstanceState(outState) } @@ -95,6 +108,16 @@ abstract class VectorBaseActivity : BaseMvRxActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + configurationViewModel = ViewModelProviders.of(this).get(ConfigurationViewModel::class.java) + + configurationViewModel.activityRestarter.observe(this, Observer { + if (!it.hasBeenHandled) { + // Recreate the Activity because configuration has changed + startActivity(intent) + finish() + } + }) + // Shake detector rageShake = RageShake(this) @@ -136,6 +159,8 @@ abstract class VectorBaseActivity : BaseMvRxActivity() { Timber.d("onResume Activity ${this.javaClass.simpleName}") + configurationViewModel.onActivityResumed() + if (this !is BugReportActivity) { rageShake?.start() } diff --git a/vector/src/main/java/im/vector/riotredesign/features/configuration/VectorConfiguration.kt b/vector/src/main/java/im/vector/riotredesign/features/configuration/VectorConfiguration.kt new file mode 100644 index 0000000000..8fff619dc3 --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/features/configuration/VectorConfiguration.kt @@ -0,0 +1,146 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotredesign.features.configuration + +import android.annotation.SuppressLint +import android.content.Context +import android.content.res.Configuration +import android.os.Build +import im.vector.riotredesign.features.settings.FontScale +import im.vector.riotredesign.features.settings.VectorLocale +import im.vector.riotredesign.features.themes.ThemeUtils +import timber.log.Timber +import java.util.* + +/** + * Handle locale configuration change, such as theme, font size and locale chosen by the user + */ +class VectorConfiguration(private val context: Context) { + + // TODO Import mLanguageReceiver From Riot? + fun onConfigurationChanged(newConfig: Configuration?) { + if (Locale.getDefault().toString() != VectorLocale.applicationLocale.toString()) { + Timber.v("## onConfigurationChanged() : the locale has been updated to " + Locale.getDefault().toString() + + ", restore the expected value " + VectorLocale.applicationLocale.toString()) + updateApplicationSettings(VectorLocale.applicationLocale, + FontScale.getFontScalePrefValue(context), + ThemeUtils.getApplicationTheme(context)) + } + } + + + private fun updateApplicationSettings(locale: Locale, textSize: String, theme: String) { + VectorLocale.saveApplicationLocale(context, locale) + FontScale.saveFontScale(context, textSize) + Locale.setDefault(locale) + + val config = Configuration(context.resources.configuration) + config.locale = locale + config.fontScale = FontScale.getFontScale(context) + context.resources.updateConfiguration(config, context.resources.displayMetrics) + + ThemeUtils.setApplicationTheme(context, theme) + // TODO PhoneNumberUtils.onLocaleUpdate() + } + + /** + * Update the application theme + * + * @param theme the new theme + */ + fun updateApplicationTheme(theme: String) { + ThemeUtils.setApplicationTheme(context, theme) + updateApplicationSettings(VectorLocale.applicationLocale, + FontScale.getFontScalePrefValue(context), + theme) + } + + /** + * Init the configuration from the saved one + */ + fun initConfiguration() { + VectorLocale.init(context) + + val locale = VectorLocale.applicationLocale + val fontScale = FontScale.getFontScale(context) + val theme = ThemeUtils.getApplicationTheme(context) + + Locale.setDefault(locale) + val config = Configuration(context.resources.configuration) + config.locale = locale + config.fontScale = fontScale + context.resources.updateConfiguration(config, context.resources.displayMetrics) + + // init the theme + ThemeUtils.setApplicationTheme(context, theme) + } + + /** + * Update the application locale + * + * @param locale + */ + // TODO Call from LanguagePickerActivity + fun updateApplicationLocale(locale: Locale) { + updateApplicationSettings(locale, FontScale.getFontScalePrefValue(context), ThemeUtils.getApplicationTheme(context)) + } + + /** + * Compute a localised context + * + * @param context the context + * @return the localised context + */ + @SuppressLint("NewApi") + fun getLocalisedContext(context: Context): Context { + try { + val resources = context.resources + val locale = VectorLocale.applicationLocale + val configuration = resources.configuration + configuration.fontScale = FontScale.getFontScale(context) + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + configuration.setLocale(locale) + configuration.setLayoutDirection(locale) + return context.createConfigurationContext(configuration) + } else { + configuration.locale = locale + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { + configuration.setLayoutDirection(locale) + } + resources.updateConfiguration(configuration, resources.displayMetrics) + return context + } + } catch (e: Exception) { + Timber.e(e, "## getLocalisedContext() failed") + } + + return context + } + + /** + * Compute the locale status value + * @param activity the activity + * @return the local status value + */ + // TODO Create data class for this + fun getHash(): String { + return (VectorLocale.applicationLocale.toString() + + "_" + FontScale.getFontScalePrefValue(context) + + "_" + ThemeUtils.getApplicationTheme(context)) + } +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/features/settings/VectorSettingsPreferencesFragment.kt b/vector/src/main/java/im/vector/riotredesign/features/settings/VectorSettingsPreferencesFragment.kt index d124af6bf0..c1c3065834 100755 --- a/vector/src/main/java/im/vector/riotredesign/features/settings/VectorSettingsPreferencesFragment.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/settings/VectorSettingsPreferencesFragment.kt @@ -54,6 +54,7 @@ import im.vector.riotredesign.core.preference.UserAvatarPreference import im.vector.riotredesign.core.preference.VectorPreference import im.vector.riotredesign.core.utils.* import im.vector.riotredesign.features.MainActivity +import im.vector.riotredesign.features.configuration.VectorConfiguration import im.vector.riotredesign.features.themes.ThemeUtils import org.koin.android.ext.android.inject import java.lang.ref.WeakReference @@ -99,6 +100,8 @@ class VectorSettingsPreferencesFragment : VectorPreferenceFragment(), SharedPref // used to avoid requesting to enter the password for each deletion private var mAccountPassword: String? = null + private val vectorConfiguration by inject() + // current publicised group list private var mPublicisedGroups: MutableSet? = null @@ -366,8 +369,10 @@ class VectorSettingsPreferencesFragment : VectorPreferenceFragment(), SharedPref findPreference(ThemeUtils.APPLICATION_THEME_KEY) .onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue -> if (newValue is String) { - // TODO VectorApp.updateApplicationTheme(newValue) + vectorConfiguration.updateApplicationTheme(newValue) + // Restart the Activity activity?.let { + // Note: recreate does not apply the color correctly it.startActivity(it.intent) it.finish() } @@ -1359,7 +1364,7 @@ class VectorSettingsPreferencesFragment : VectorPreferenceFragment(), SharedPref if (resultCode == Activity.RESULT_OK) { when (requestCode) { - REQUEST_CALL_RINGTONE -> { + REQUEST_CALL_RINGTONE -> { val callRingtoneUri: Uri? = data?.getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI) val thisActivity = activity if (callRingtoneUri != null && thisActivity != null) { @@ -1368,9 +1373,9 @@ class VectorSettingsPreferencesFragment : VectorPreferenceFragment(), SharedPref } } REQUEST_E2E_FILE_REQUEST_CODE -> importKeys(data) - REQUEST_NEW_PHONE_NUMBER -> refreshPhoneNumbersList() - REQUEST_PHONEBOOK_COUNTRY -> onPhonebookCountryUpdate(data) - REQUEST_LOCALE -> { + REQUEST_NEW_PHONE_NUMBER -> refreshPhoneNumbersList() + REQUEST_PHONEBOOK_COUNTRY -> onPhonebookCountryUpdate(data) + REQUEST_LOCALE -> { activity?.let { startActivity(it.intent) it.finish() diff --git a/vector/src/main/java/im/vector/riotredesign/features/themes/ThemeUtils.kt b/vector/src/main/java/im/vector/riotredesign/features/themes/ThemeUtils.kt index e199d508db..1e69b83b79 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/themes/ThemeUtils.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/themes/ThemeUtils.kt @@ -55,7 +55,7 @@ object ThemeUtils { */ fun getApplicationTheme(context: Context): String { return PreferenceManager.getDefaultSharedPreferences(context) - .getString(APPLICATION_THEME_KEY, THEME_LIGHT_VALUE) + .getString(APPLICATION_THEME_KEY, THEME_LIGHT_VALUE)!! } /** @@ -64,20 +64,14 @@ object ThemeUtils { * @param aTheme the new theme */ fun setApplicationTheme(context: Context, aTheme: String) { - PreferenceManager.getDefaultSharedPreferences(context) - .edit() - .putString(APPLICATION_THEME_KEY, aTheme) - .apply() - - /* TODO when (aTheme) { - THEME_DARK_VALUE -> VectorApp.getInstance().setTheme(R.style.AppTheme_Dark) - THEME_BLACK_VALUE -> VectorApp.getInstance().setTheme(R.style.AppTheme_Black) - THEME_STATUS_VALUE -> VectorApp.getInstance().setTheme(R.style.AppTheme_Status) - else -> VectorApp.getInstance().setTheme(R.style.AppTheme_Light) + THEME_DARK_VALUE -> context.setTheme(R.style.AppTheme_Dark) + THEME_BLACK_VALUE -> context.setTheme(R.style.AppTheme_Black) + THEME_STATUS_VALUE -> context.setTheme(R.style.AppTheme_Status) + else -> context.setTheme(R.style.AppTheme_Light) } - */ + // Clear the cache mColorByAttr.clear() }