Skip to content
Permalink
Browse files

Initial implementation of dark mode

Current the UI setting a binary toggle, but the
internal logic is an enum. I'll move the setting
to a list in a dialog in the next CL.

I took a quick pass over the schedule UI to make it
semi-usable but the UI is pretty ugly right now.

Change-Id: I6bd79ad2b8f38b1ac36cec6575f41e6ef2744d80
  • Loading branch information...
chrisbanes authored and thagikura committed Jan 31, 2019
1 parent a662118 commit c635ca7abaf4bea91643a5568e360d227d459f94
Showing with 433 additions and 42 deletions.
  1. +4 −1 mobile/src/main/java/com/google/samples/apps/iosched/di/AppComponent.kt
  2. +10 −3 mobile/src/main/java/com/google/samples/apps/iosched/ui/MainActivity.kt
  3. +72 −0 mobile/src/main/java/com/google/samples/apps/iosched/ui/ThemedActivityDelegate.kt
  4. +28 −0 mobile/src/main/java/com/google/samples/apps/iosched/ui/ThemedActivityDelegateModule.kt
  5. +20 −2 mobile/src/main/java/com/google/samples/apps/iosched/ui/info/SettingsViewModel.kt
  6. +13 −0 mobile/src/main/java/com/google/samples/apps/iosched/ui/map/MapActivity.kt
  7. +2 −2 mobile/src/main/java/com/google/samples/apps/iosched/ui/map/MapFragment.kt
  8. +4 −2 mobile/src/main/java/com/google/samples/apps/iosched/ui/map/MapViewModel.kt
  9. +5 −2 mobile/src/main/java/com/google/samples/apps/iosched/ui/schedule/ScheduleViewModel.kt
  10. +12 −0 mobile/src/main/java/com/google/samples/apps/iosched/ui/sessiondetail/SessionDetailActivity.kt
  11. +0 −3 mobile/src/main/java/com/google/samples/apps/iosched/ui/sessiondetail/SessionDetailFragment.kt
  12. +5 −2 mobile/src/main/java/com/google/samples/apps/iosched/ui/sessiondetail/SessionDetailViewModel.kt
  13. +13 −0 mobile/src/main/java/com/google/samples/apps/iosched/ui/speaker/SpeakerActivity.kt
  14. +2 −1 mobile/src/main/java/com/google/samples/apps/iosched/ui/speaker/SpeakerFragment.kt
  15. +5 −2 mobile/src/main/java/com/google/samples/apps/iosched/ui/speaker/SpeakerViewModel.kt
  16. +9 −0 mobile/src/main/java/com/google/samples/apps/iosched/util/Extensions.kt
  17. +1 −0 mobile/src/main/res/layout/fragment_schedule_filter.xml
  18. +15 −2 mobile/src/main/res/layout/fragment_settings.xml
  19. +2 −0 mobile/src/main/res/values/strings.xml
  20. +4 −3 mobile/src/main/res/values/styles.xml
  21. +4 −3 mobile/src/test/java/com/google/samples/apps/iosched/test/util/fakes/FakePreferenceStorage.kt
  22. +27 −0 mobile/src/test/java/com/google/samples/apps/iosched/test/util/fakes/FakeThemedActivityDelegate.kt
  23. +3 −1 mobile/src/test/java/com/google/samples/apps/iosched/ui/map/MapViewModelTest.kt
  24. +6 −2 mobile/src/test/java/com/google/samples/apps/iosched/ui/schedule/ScheduleViewModelTest.kt
  25. +5 −2 mobile/src/test/java/com/google/samples/apps/iosched/ui/sessiondetail/SessionDetailViewModelTest.kt
  26. +6 −2 mobile/src/test/java/com/google/samples/apps/iosched/ui/speaker/SpeakerViewModelTest.kt
  27. +34 −0 model/src/main/java/com/google/samples/apps/iosched/model/Theme.kt
  28. +23 −4 shared/src/main/java/com/google/samples/apps/iosched/shared/data/prefs/PreferenceStorage.kt
  29. +29 −0 shared/src/main/java/com/google/samples/apps/iosched/shared/domain/settings/GetThemeUseCase.kt
  30. +34 −0 ...d/src/main/java/com/google/samples/apps/iosched/shared/domain/settings/ObserveThemeModeUseCase.kt
  31. +32 −0 shared/src/main/java/com/google/samples/apps/iosched/shared/domain/settings/SetThemeUseCase.kt
  32. +4 −3 shared/src/test/java/com/google/samples/apps/iosched/test/util/FakePreferenceStorage.kt
@@ -20,6 +20,7 @@ import com.google.samples.apps.iosched.MainApplication
import com.google.samples.apps.iosched.shared.di.ServiceBindingModule
import com.google.samples.apps.iosched.shared.di.SharedModule
import com.google.samples.apps.iosched.shared.di.ViewModelModule
import com.google.samples.apps.iosched.ui.ThemedActivityDelegateModule
import com.google.samples.apps.iosched.ui.signin.SignInViewModelDelegateModule
import dagger.Component
import dagger.android.AndroidInjector
@@ -43,7 +44,9 @@ import javax.inject.Singleton
ServiceBindingModule::class,
SharedModule::class,
SignInModule::class,
SignInViewModelDelegateModule::class]
SignInViewModelDelegateModule::class,
ThemedActivityDelegateModule::class
]
)
interface AppComponent : AndroidInjector<MainApplication> {
@Component.Builder
@@ -24,6 +24,7 @@ import androidx.core.view.GravityCompat
import androidx.drawerlayout.widget.DrawerLayout
import androidx.drawerlayout.widget.DrawerLayout.DrawerListener
import androidx.fragment.app.Fragment
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider
import com.firebase.ui.auth.IdpResponse
import com.google.android.material.navigation.NavigationView
@@ -36,6 +37,7 @@ import com.google.samples.apps.iosched.ui.messages.SnackbarMessageManager
import com.google.samples.apps.iosched.ui.schedule.ScheduleFragment
import com.google.samples.apps.iosched.ui.schedule.ScheduleViewModel
import com.google.samples.apps.iosched.util.signin.FirebaseAuthErrorCodeConverter
import com.google.samples.apps.iosched.util.updateForTheme
import dagger.android.support.DaggerAppCompatActivity
import timber.log.Timber
import java.util.UUID
@@ -68,12 +70,15 @@ class MainActivity : DaggerAppCompatActivity(), DrawerListener {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
drawer = findViewById(R.id.drawer)
navigation = findViewById(R.id.navigation)

// This VM instance is shared between activity and fragments, as it's scoped to MainActivity
scheduleViewModel = viewModelProvider(viewModelFactory)
// Update for Dark Mode straight away
updateForTheme(scheduleViewModel.currentTheme)

setContentView(R.layout.activity_main)
drawer = findViewById(R.id.drawer)
navigation = findViewById(R.id.navigation)

drawer.addDrawerListener(this)
navigation.setNavigationItemSelectedListener {
@@ -93,6 +98,8 @@ class MainActivity : DaggerAppCompatActivity(), DrawerListener {
supportFragmentManager.findFragmentById(FRAGMENT_ID) as? MainNavigationFragment
?: throw IllegalStateException("Activity recreated, but no fragment found!")
}

scheduleViewModel.theme.observe(this, Observer(::updateForTheme))
}

override fun onRestoreInstanceState(savedInstanceState: Bundle?) {
@@ -0,0 +1,72 @@
/*
* Copyright 2019 Google LLC
*
* 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
*
* https://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 com.google.samples.apps.iosched.ui

import androidx.lifecycle.LiveData
import com.google.samples.apps.iosched.model.Theme
import com.google.samples.apps.iosched.shared.domain.settings.GetThemeUseCase
import com.google.samples.apps.iosched.shared.domain.settings.ObserveThemeModeUseCase
import com.google.samples.apps.iosched.shared.result.Result.Success
import com.google.samples.apps.iosched.shared.util.map
import javax.inject.Inject
import kotlin.LazyThreadSafetyMode.NONE

/**
* Interface to implement activity theming via a ViewModel.
*
* You can inject a implementation of this via Dagger2, then use the implementation as an interface
* delegate to add the functionality without writing any code
*
* Example usage:
* ```
* class MyViewModel @Inject constructor(
* themedActivityDelegate: ThemedActivityDelegate
* ) : ViewModel(), ThemedActivityDelegate by themedActivityDelegate {
* ```
*/
interface ThemedActivityDelegate {
/**
* Allows observing of the current theme
*/
val theme: LiveData<Theme>

/**
* Allows querying of the current theme synchronously
*/
val currentTheme: Theme
}

class ThemedActivityDelegateImpl @Inject constructor(
private val observeThemeUseCase: ObserveThemeModeUseCase,
private val getThemeUseCase: GetThemeUseCase
) : ThemedActivityDelegate {
override val theme: LiveData<Theme> by lazy(NONE) {
observeThemeUseCase.observe().map {
if (it is Success) it.data else Theme.SYSTEM
}
}

override val currentTheme: Theme
get() = getThemeUseCase.executeNow(Unit).let {
if (it is Success) it.data else Theme.SYSTEM
}

init {
// Observe updates in dark mode setting
observeThemeUseCase.execute(Unit)
}
}
@@ -0,0 +1,28 @@
/*
* Copyright 2019 Google LLC
*
* 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
*
* https://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 com.google.samples.apps.iosched.ui

import dagger.Binds
import dagger.Module
import javax.inject.Singleton

@Module
abstract class ThemedActivityDelegateModule {
@Singleton
@Binds
abstract fun provideThemedActivityDelegate(impl: ThemedActivityDelegateImpl): ThemedActivityDelegate
}
@@ -19,11 +19,14 @@ package com.google.samples.apps.iosched.ui.info
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.google.samples.apps.iosched.model.Theme
import com.google.samples.apps.iosched.shared.domain.prefs.NotificationsPrefSaveActionUseCase
import com.google.samples.apps.iosched.shared.domain.settings.GetAnalyticsSettingUseCase
import com.google.samples.apps.iosched.shared.domain.settings.GetThemeUseCase
import com.google.samples.apps.iosched.shared.domain.settings.GetNotificationsSettingUseCase
import com.google.samples.apps.iosched.shared.domain.settings.GetTimeZoneUseCase
import com.google.samples.apps.iosched.shared.domain.settings.SetAnalyticsSettingUseCase
import com.google.samples.apps.iosched.shared.domain.settings.SetThemeUseCase
import com.google.samples.apps.iosched.shared.domain.settings.SetTimeZoneUseCase
import com.google.samples.apps.iosched.shared.result.Result
import com.google.samples.apps.iosched.shared.result.Result.Success
@@ -36,21 +39,27 @@ class SettingsViewModel @Inject constructor(
val notificationsPrefSaveActionUseCase: NotificationsPrefSaveActionUseCase,
getNotificationsSettingUseCase: GetNotificationsSettingUseCase,
val setAnalyticsSettingUseCase: SetAnalyticsSettingUseCase,
getAnalyticsSettingUseCase: GetAnalyticsSettingUseCase
getAnalyticsSettingUseCase: GetAnalyticsSettingUseCase,
val setThemeUseCase: SetThemeUseCase,
getThemeUseCase: GetThemeUseCase
) : ViewModel() {

// Time Zone setting
private val preferConferenceTimeZoneResult = MutableLiveData<Result<Boolean>>()
val preferConferenceTimeZone: LiveData<Boolean>

// Notifications setting
val enableNotificationsResult = MutableLiveData<Result<Boolean>>()
private val enableNotificationsResult = MutableLiveData<Result<Boolean>>()
val enableNotifications: LiveData<Boolean>

// Analytics setting
private val sendUsageStatisticsResult = MutableLiveData<Result<Boolean>>()
val sendUsageStatistics: LiveData<Boolean>

// Theme setting
private val darkModeResult = MutableLiveData<Result<Theme>>()
val darkMode: LiveData<Boolean>

init {
getTimeZoneUseCase(Unit, preferConferenceTimeZoneResult)
preferConferenceTimeZone = preferConferenceTimeZoneResult.map {
@@ -66,6 +75,11 @@ class SettingsViewModel @Inject constructor(
enableNotifications = enableNotificationsResult.map {
(it as? Success<Boolean>)?.data ?: false
}

getThemeUseCase(Unit, darkModeResult)
darkMode = darkModeResult.map {
(it as? Success<Theme>)?.data == Theme.DARK
}
}

fun toggleTimeZone(checked: Boolean) {
@@ -79,4 +93,8 @@ class SettingsViewModel @Inject constructor(
fun toggleEnableNotifications(checked: Boolean) {
notificationsPrefSaveActionUseCase(checked, enableNotificationsResult)
}

fun toggleDarkMode(checked: Boolean) {
setThemeUseCase(if (checked) Theme.DARK else Theme.LIGHT, darkModeResult)
}
}
@@ -20,15 +20,22 @@ import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.text.TextUtils
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider
import com.google.samples.apps.iosched.R
import com.google.samples.apps.iosched.shared.util.inTransaction
import com.google.samples.apps.iosched.shared.util.viewModelProvider
import com.google.samples.apps.iosched.util.updateForTheme
import dagger.android.support.DaggerAppCompatActivity
import javax.inject.Inject

/** Shell activity hosting a [MapFragment] */
class MapActivity : DaggerAppCompatActivity() {

private lateinit var fragment: MapFragment

@Inject lateinit var viewModelFactory: ViewModelProvider.Factory

companion object {
const val EXTRA_FEATURE_ID = "extra.FEATURE_ID"
const val FRAGMENT_ID = R.id.fragment_container
@@ -42,6 +49,10 @@ class MapActivity : DaggerAppCompatActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

val viewModel: MapViewModel = viewModelProvider(viewModelFactory)
updateForTheme(viewModel.currentTheme)

setContentView(R.layout.activity_map)

if (savedInstanceState == null) {
@@ -57,6 +68,8 @@ class MapActivity : DaggerAppCompatActivity() {
} else {
fragment = supportFragmentManager.findFragmentById(FRAGMENT_ID) as MapFragment
}

viewModel.theme.observe(this, Observer(::updateForTheme))
}

override fun onBackPressed() {
@@ -29,7 +29,7 @@ import com.google.android.gms.maps.MapView
import com.google.android.gms.maps.model.Marker
import com.google.samples.apps.iosched.databinding.FragmentMapBinding
import com.google.samples.apps.iosched.shared.analytics.AnalyticsHelper
import com.google.samples.apps.iosched.shared.util.viewModelProvider
import com.google.samples.apps.iosched.shared.util.activityViewModelProvider
import com.google.samples.apps.iosched.ui.MainNavigationFragment
import com.google.samples.apps.iosched.widget.BottomSheetBehavior
import com.google.samples.apps.iosched.widget.BottomSheetBehavior.BottomSheetCallback
@@ -75,7 +75,7 @@ class MapFragment : DaggerFragment(), MainNavigationFragment, OnMarkerClickListe
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
viewModel = viewModelProvider(viewModelFactory)
viewModel = activityViewModelProvider(viewModelFactory)
binding = FragmentMapBinding.inflate(inflater, container, false).apply {
setLifecycleOwner(this@MapFragment)
viewModel = this@MapFragment.viewModel
@@ -36,14 +36,16 @@ import com.google.samples.apps.iosched.shared.result.Event
import com.google.samples.apps.iosched.shared.result.Result
import com.google.samples.apps.iosched.shared.result.Result.Success
import com.google.samples.apps.iosched.shared.util.map
import com.google.samples.apps.iosched.ui.ThemedActivityDelegate
import com.google.samples.apps.iosched.widget.BottomSheetBehavior
import javax.inject.Inject

class MapViewModel @Inject constructor(
loadMapTileProviderUseCase: LoadMapTileProviderUseCase,
private val loadGeoJsonFeaturesUseCase: LoadGeoJsonFeaturesUseCase,
private val analyticsHelper: AnalyticsHelper
) : ViewModel() {
private val analyticsHelper: AnalyticsHelper,
themedActivityDelegate: ThemedActivityDelegate
) : ViewModel(), ThemedActivityDelegate by themedActivityDelegate {

/**
* Area covered by the venue. Determines the viewport of the map.
@@ -52,6 +52,7 @@ import com.google.samples.apps.iosched.shared.util.TimeUtils
import com.google.samples.apps.iosched.shared.util.TimeUtils.ConferenceDays
import com.google.samples.apps.iosched.shared.util.map
import com.google.samples.apps.iosched.ui.SnackbarMessage
import com.google.samples.apps.iosched.ui.ThemedActivityDelegate
import com.google.samples.apps.iosched.ui.messages.SnackbarMessageManager
import com.google.samples.apps.iosched.ui.schedule.filters.EventFilter
import com.google.samples.apps.iosched.ui.schedule.filters.EventFilter.MyEventsFilter
@@ -84,8 +85,10 @@ class ScheduleViewModel @Inject constructor(
observeConferenceDataUseCase: ObserveConferenceDataUseCase,
loadSelectedFiltersUseCase: LoadSelectedFiltersUseCase,
private val saveSelectedFiltersUseCase: SaveSelectedFiltersUseCase,
private val analyticsHelper: AnalyticsHelper
) : ViewModel(), ScheduleEventListener, SignInViewModelDelegate by signInViewModelDelegate {
private val analyticsHelper: AnalyticsHelper,
themedActivityDelegate: ThemedActivityDelegate
) : ViewModel(), ScheduleEventListener, SignInViewModelDelegate by signInViewModelDelegate,
ThemedActivityDelegate by themedActivityDelegate {

val isLoading: LiveData<Boolean>

@@ -20,13 +20,17 @@ import android.app.Activity
import android.content.Context
import android.content.Intent
import android.os.Bundle
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider
import com.firebase.ui.auth.IdpResponse
import com.google.samples.apps.iosched.R
import com.google.samples.apps.iosched.model.SessionId
import com.google.samples.apps.iosched.shared.util.inTransaction
import com.google.samples.apps.iosched.shared.util.viewModelProvider
import com.google.samples.apps.iosched.ui.SnackbarMessage
import com.google.samples.apps.iosched.ui.messages.SnackbarMessageManager
import com.google.samples.apps.iosched.util.signin.FirebaseAuthErrorCodeConverter
import com.google.samples.apps.iosched.util.updateForTheme
import dagger.android.support.DaggerAppCompatActivity
import timber.log.Timber
import java.util.UUID
@@ -37,8 +41,14 @@ class SessionDetailActivity : DaggerAppCompatActivity() {
@Inject
lateinit var snackbarMessageManager: SnackbarMessageManager

@Inject lateinit var viewModelFactory: ViewModelProvider.Factory

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

val viewModel: SessionDetailViewModel = viewModelProvider(viewModelFactory)
updateForTheme(viewModel.currentTheme)

setContentView(R.layout.activity_session_detail)

if (savedInstanceState == null) {
@@ -47,6 +57,8 @@ class SessionDetailActivity : DaggerAppCompatActivity() {
add(R.id.session_detail_container, SessionDetailFragment.newInstance(sessionId))
}
}

viewModel.theme.observe(this, Observer(::updateForTheme))
}

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
@@ -86,9 +86,6 @@ class SessionDetailFragment : DaggerFragment() {
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {

// TODO: Scoping the VM to the activity because of bug
// https://issuetracker.google.com/issues/74139250 (fixed in Supportlib 28.0.0-alpha1)
sessionDetailViewModel = activityViewModelProvider(viewModelFactory)

val binding = FragmentSessionDetailBinding.inflate(inflater, container, false).apply {

0 comments on commit c635ca7

Please sign in to comment.
You can’t perform that action at this time.