Skip to content

Commit c88d65e

Browse files
ArnyminerZrfc2822
authored andcommitted
Migrated to Jetpack Compose
Signed-off-by: Arnau Mora <[email protected]>
1 parent f6ac4e0 commit c88d65e

File tree

2 files changed

+253
-202
lines changed

2 files changed

+253
-202
lines changed

app/src/main/kotlin/at/bitfire/davdroid/ui/intro/BatteryOptimizationsFragment.kt

+253-43
Original file line numberDiff line numberDiff line change
@@ -16,20 +16,52 @@ import android.view.LayoutInflater
1616
import android.view.View
1717
import android.view.ViewGroup
1818
import androidx.activity.result.contract.ActivityResultContract
19+
import androidx.compose.animation.AnimatedVisibility
20+
import androidx.compose.foundation.clickable
21+
import androidx.compose.foundation.layout.Column
22+
import androidx.compose.foundation.layout.Row
23+
import androidx.compose.foundation.layout.Spacer
24+
import androidx.compose.foundation.layout.fillMaxSize
25+
import androidx.compose.foundation.layout.fillMaxWidth
26+
import androidx.compose.foundation.layout.height
27+
import androidx.compose.foundation.layout.padding
28+
import androidx.compose.foundation.rememberScrollState
29+
import androidx.compose.foundation.verticalScroll
30+
import androidx.compose.material.Card
31+
import androidx.compose.material.Checkbox
32+
import androidx.compose.material.MaterialTheme
33+
import androidx.compose.material.OutlinedButton
34+
import androidx.compose.material.Switch
35+
import androidx.compose.material.Text
36+
import androidx.compose.runtime.Composable
37+
import androidx.compose.runtime.DisposableEffect
38+
import androidx.compose.runtime.LaunchedEffect
39+
import androidx.compose.runtime.MutableState
40+
import androidx.compose.runtime.getValue
41+
import androidx.compose.runtime.livedata.observeAsState
42+
import androidx.compose.runtime.mutableStateOf
43+
import androidx.compose.runtime.remember
44+
import androidx.compose.runtime.setValue
45+
import androidx.compose.ui.Alignment
46+
import androidx.compose.ui.Modifier
47+
import androidx.compose.ui.platform.ComposeView
48+
import androidx.compose.ui.platform.ViewCompositionStrategy
49+
import androidx.compose.ui.res.stringResource
50+
import androidx.compose.ui.tooling.preview.Preview
51+
import androidx.compose.ui.unit.dp
1952
import androidx.core.content.getSystemService
20-
import androidx.databinding.ObservableBoolean
2153
import androidx.fragment.app.Fragment
2254
import androidx.fragment.app.viewModels
2355
import androidx.lifecycle.AndroidViewModel
2456
import androidx.lifecycle.MutableLiveData
2557
import at.bitfire.davdroid.App
2658
import at.bitfire.davdroid.BuildConfig
2759
import at.bitfire.davdroid.R
28-
import at.bitfire.davdroid.databinding.IntroBatteryOptimizationsBinding
2960
import at.bitfire.davdroid.settings.SettingsManager
3061
import at.bitfire.davdroid.ui.UiUtils
3162
import at.bitfire.davdroid.ui.intro.BatteryOptimizationsFragment.Model.Companion.HINT_AUTOSTART_PERMISSION
3263
import at.bitfire.davdroid.ui.intro.BatteryOptimizationsFragment.Model.Companion.HINT_BATTERY_OPTIMIZATIONS
64+
import com.google.accompanist.themeadapter.material.MdcTheme
3365
import dagger.Binds
3466
import dagger.Module
3567
import dagger.hilt.InstallIn
@@ -53,28 +85,33 @@ class BatteryOptimizationsFragment: Fragment() {
5385

5486

5587
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
56-
val binding = IntroBatteryOptimizationsBinding.inflate(inflater, container, false)
57-
binding.lifecycleOwner = viewLifecycleOwner
58-
binding.model = model
59-
60-
model.shouldBeWhitelisted.observe(viewLifecycleOwner) { shouldBeWhitelisted ->
61-
@SuppressLint("BatteryLife")
62-
if (shouldBeWhitelisted && !model.isWhitelisted.value!!)
63-
ignoreBatteryOptimizationsResultLauncher.launch(BuildConfig.APPLICATION_ID)
64-
}
65-
binding.batteryText.text = getString(R.string.intro_battery_text, getString(R.string.app_name))
66-
67-
binding.autostartHeading.text = getString(R.string.intro_autostart_title, WordUtils.capitalize(Build.MANUFACTURER))
68-
binding.autostartText.setText(R.string.intro_autostart_text)
69-
binding.autostartMoreInfo.setOnClickListener {
70-
UiUtils.launchUri(requireActivity(), App.homepageUrl(requireActivity()).buildUpon()
71-
.appendPath("faq").appendPath("synchronization-is-not-run-as-expected")
72-
.appendQueryParameter("manufacturer", Build.MANUFACTURER.lowercase(Locale.ROOT)).build())
88+
return ComposeView(requireContext()).apply {
89+
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
90+
setContent {
91+
MdcTheme {
92+
var dontShowBattery by model.dontShowBattery
93+
var dontShowAutostart by model.dontShowAutostart
94+
val shouldBeWhitelisted by model.shouldBeWhitelisted.observeAsState(false)
95+
val isWhitelisted by model.isWhitelisted.observeAsState(false)
96+
97+
LaunchedEffect(shouldBeWhitelisted, isWhitelisted) {
98+
if (shouldBeWhitelisted && !isWhitelisted)
99+
ignoreBatteryOptimizationsResultLauncher.launch(BuildConfig.APPLICATION_ID)
100+
}
101+
102+
Content(
103+
dontShowBattery = dontShowBattery,
104+
onChangeDontShowBattery = { dontShowBattery = it },
105+
isWhitelisted = isWhitelisted,
106+
shouldBeWhitelisted = shouldBeWhitelisted,
107+
onChangeShouldBeWhitelisted = model.shouldBeWhitelisted::postValue,
108+
dontShowAutostart = dontShowAutostart,
109+
onChangeDontShowAutostart = { dontShowAutostart = it },
110+
manufacturerWarning = Model.manufacturerWarning
111+
)
112+
}
113+
}
73114
}
74-
75-
binding.infoLeaveUnchecked.text = getString(R.string.intro_leave_unchecked, getString(R.string.app_settings_reset_hints))
76-
77-
return binding.root
78115
}
79116

80117
override fun onResume() {
@@ -83,6 +120,163 @@ class BatteryOptimizationsFragment: Fragment() {
83120
}
84121

85122

123+
@Preview(showBackground = true, showSystemUi = true)
124+
@Composable
125+
fun Content_Preview() {
126+
MdcTheme {
127+
Content(
128+
dontShowBattery = true,
129+
onChangeDontShowBattery = {},
130+
isWhitelisted = false,
131+
shouldBeWhitelisted = true,
132+
onChangeShouldBeWhitelisted = {},
133+
dontShowAutostart = false,
134+
onChangeDontShowAutostart = {},
135+
manufacturerWarning = true
136+
)
137+
}
138+
}
139+
140+
@Composable
141+
private fun Content(
142+
dontShowBattery: Boolean,
143+
onChangeDontShowBattery: (Boolean) -> Unit,
144+
isWhitelisted: Boolean,
145+
shouldBeWhitelisted: Boolean,
146+
onChangeShouldBeWhitelisted: (Boolean) -> Unit,
147+
dontShowAutostart: Boolean,
148+
onChangeDontShowAutostart: (Boolean) -> Unit,
149+
manufacturerWarning: Boolean
150+
) {
151+
Column(
152+
modifier = Modifier
153+
.fillMaxSize()
154+
.verticalScroll(rememberScrollState())
155+
.padding(8.dp)
156+
) {
157+
Card {
158+
Column(
159+
modifier = Modifier
160+
.fillMaxWidth()
161+
.padding(16.dp)
162+
) {
163+
Row(
164+
modifier = Modifier.fillMaxWidth(),
165+
verticalAlignment = Alignment.CenterVertically
166+
) {
167+
Text(
168+
text = stringResource(R.string.intro_battery_title),
169+
style = MaterialTheme.typography.h6,
170+
modifier = Modifier.weight(1f)
171+
)
172+
Switch(
173+
checked = shouldBeWhitelisted,
174+
onCheckedChange = {
175+
// Only accept click events if not whitelisted
176+
if (!isWhitelisted) {
177+
onChangeShouldBeWhitelisted(it)
178+
}
179+
},
180+
enabled = !dontShowBattery
181+
)
182+
}
183+
Text(
184+
text = stringResource(
185+
R.string.intro_battery_text,
186+
getString(R.string.app_name)
187+
),
188+
style = MaterialTheme.typography.body1,
189+
modifier = Modifier.padding(top = 12.dp)
190+
)
191+
AnimatedVisibility(visible = !isWhitelisted) {
192+
Row(
193+
verticalAlignment = Alignment.CenterVertically
194+
) {
195+
Checkbox(
196+
checked = dontShowBattery,
197+
onCheckedChange = onChangeDontShowBattery,
198+
enabled = !isWhitelisted
199+
)
200+
Text(
201+
text = stringResource(R.string.intro_battery_dont_show),
202+
style = MaterialTheme.typography.caption,
203+
modifier = Modifier
204+
.clickable { onChangeDontShowBattery(!dontShowBattery) }
205+
)
206+
}
207+
}
208+
}
209+
}
210+
AnimatedVisibility(visible = manufacturerWarning) {
211+
Card(
212+
modifier = Modifier.padding(top = 8.dp)
213+
) {
214+
Column(
215+
modifier = Modifier
216+
.fillMaxWidth()
217+
.padding(16.dp)
218+
) {
219+
Text(
220+
text = stringResource(
221+
R.string.intro_autostart_title,
222+
WordUtils.capitalize(Build.MANUFACTURER)
223+
),
224+
style = MaterialTheme.typography.h6,
225+
modifier = Modifier.fillMaxWidth()
226+
)
227+
Text(
228+
text = stringResource(R.string.intro_autostart_text),
229+
style = MaterialTheme.typography.body1,
230+
modifier = Modifier.padding(top = 12.dp)
231+
)
232+
OutlinedButton(
233+
onClick = {
234+
UiUtils.launchUri(
235+
requireActivity(),
236+
App.homepageUrl(requireActivity())
237+
.buildUpon()
238+
.appendPath("faq")
239+
.appendPath("synchronization-is-not-run-as-expected")
240+
.appendQueryParameter(
241+
"manufacturer",
242+
Build.MANUFACTURER.lowercase(Locale.ROOT)
243+
)
244+
.build()
245+
)
246+
}
247+
) {
248+
Text(stringResource(R.string.intro_more_info))
249+
}
250+
Row(
251+
verticalAlignment = Alignment.CenterVertically
252+
) {
253+
Checkbox(
254+
checked = dontShowAutostart,
255+
onCheckedChange = onChangeDontShowAutostart
256+
)
257+
Text(
258+
text = stringResource(R.string.intro_autostart_dont_show),
259+
style = MaterialTheme.typography.caption,
260+
modifier = Modifier
261+
.clickable { onChangeDontShowAutostart(!dontShowAutostart) }
262+
)
263+
}
264+
}
265+
}
266+
}
267+
Text(
268+
text = stringResource(
269+
R.string.intro_leave_unchecked,
270+
stringResource(R.string.app_settings_reset_hints)
271+
),
272+
style = MaterialTheme.typography.body2,
273+
modifier = Modifier.padding(top = 8.dp)
274+
)
275+
Spacer(modifier = Modifier.height(90.dp))
276+
}
277+
}
278+
279+
86280
@HiltViewModel
87281
class Model @Inject constructor(
88282
application: Application,
@@ -133,27 +327,13 @@ class BatteryOptimizationsFragment: Fragment() {
133327

134328
val shouldBeWhitelisted = MutableLiveData<Boolean>()
135329
val isWhitelisted = MutableLiveData<Boolean>()
136-
val dontShowBattery = object: ObservableBoolean() {
137-
override fun get() = settings.getBooleanOrNull(HINT_BATTERY_OPTIMIZATIONS) == false
138-
override fun set(dontShowAgain: Boolean) {
139-
if (dontShowAgain)
140-
settings.putBoolean(HINT_BATTERY_OPTIMIZATIONS, false)
141-
else
142-
settings.remove(HINT_BATTERY_OPTIMIZATIONS)
143-
notifyChange()
144-
}
145-
}
330+
val dontShowBattery: MutableState<Boolean>
331+
@Composable
332+
get() = observePreference(key = HINT_BATTERY_OPTIMIZATIONS) { it == false }
146333

147-
val dontShowAutostart = object: ObservableBoolean() {
148-
override fun get() = settings.getBooleanOrNull(HINT_AUTOSTART_PERMISSION) == false
149-
override fun set(dontShowAgain: Boolean) {
150-
if (dontShowAgain)
151-
settings.putBoolean(HINT_AUTOSTART_PERMISSION, false)
152-
else
153-
settings.remove(HINT_AUTOSTART_PERMISSION)
154-
notifyChange()
155-
}
156-
}
334+
val dontShowAutostart: MutableState<Boolean>
335+
@Composable
336+
get() = observePreference(key = HINT_AUTOSTART_PERMISSION) { it == false }
157337

158338
fun checkWhitelisted() {
159339
val whitelisted = isWhitelisted(getApplication())
@@ -165,6 +345,36 @@ class BatteryOptimizationsFragment: Fragment() {
165345
settings.remove(HINT_BATTERY_OPTIMIZATIONS)
166346
}
167347

348+
@Composable
349+
private fun observePreference(
350+
key: String,
351+
map: (Boolean?) -> Boolean = { it == true }
352+
): MutableState<Boolean> {
353+
val state = remember {
354+
mutableStateOf(
355+
settings.getBooleanOrNull(key).let(map)
356+
)
357+
}
358+
DisposableEffect(Unit) {
359+
val observer = object : SettingsManager.OnChangeListener {
360+
override fun onSettingsChanged() {
361+
state.value = settings.getBooleanOrNull(key).let(map)
362+
}
363+
}
364+
settings.addOnChangeListener(observer)
365+
onDispose {
366+
settings.removeOnChangeListener(observer)
367+
}
368+
}
369+
LaunchedEffect(state) {
370+
val value = settings.getBooleanOrNull(key).let(map)
371+
if (state.value != value) {
372+
settings.putBoolean(key, state.value.let(map))
373+
}
374+
}
375+
return state
376+
}
377+
168378
}
169379

170380

0 commit comments

Comments
 (0)