@@ -16,20 +16,52 @@ import android.view.LayoutInflater
16
16
import android.view.View
17
17
import android.view.ViewGroup
18
18
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
19
52
import androidx.core.content.getSystemService
20
- import androidx.databinding.ObservableBoolean
21
53
import androidx.fragment.app.Fragment
22
54
import androidx.fragment.app.viewModels
23
55
import androidx.lifecycle.AndroidViewModel
24
56
import androidx.lifecycle.MutableLiveData
25
57
import at.bitfire.davdroid.App
26
58
import at.bitfire.davdroid.BuildConfig
27
59
import at.bitfire.davdroid.R
28
- import at.bitfire.davdroid.databinding.IntroBatteryOptimizationsBinding
29
60
import at.bitfire.davdroid.settings.SettingsManager
30
61
import at.bitfire.davdroid.ui.UiUtils
31
62
import at.bitfire.davdroid.ui.intro.BatteryOptimizationsFragment.Model.Companion.HINT_AUTOSTART_PERMISSION
32
63
import at.bitfire.davdroid.ui.intro.BatteryOptimizationsFragment.Model.Companion.HINT_BATTERY_OPTIMIZATIONS
64
+ import com.google.accompanist.themeadapter.material.MdcTheme
33
65
import dagger.Binds
34
66
import dagger.Module
35
67
import dagger.hilt.InstallIn
@@ -53,28 +85,33 @@ class BatteryOptimizationsFragment: Fragment() {
53
85
54
86
55
87
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
+ }
73
114
}
74
-
75
- binding.infoLeaveUnchecked.text = getString(R .string.intro_leave_unchecked, getString(R .string.app_settings_reset_hints))
76
-
77
- return binding.root
78
115
}
79
116
80
117
override fun onResume () {
@@ -83,6 +120,163 @@ class BatteryOptimizationsFragment: Fragment() {
83
120
}
84
121
85
122
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
+
86
280
@HiltViewModel
87
281
class Model @Inject constructor(
88
282
application : Application ,
@@ -133,27 +327,13 @@ class BatteryOptimizationsFragment: Fragment() {
133
327
134
328
val shouldBeWhitelisted = MutableLiveData <Boolean >()
135
329
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 }
146
333
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 }
157
337
158
338
fun checkWhitelisted () {
159
339
val whitelisted = isWhitelisted(getApplication())
@@ -165,6 +345,36 @@ class BatteryOptimizationsFragment: Fragment() {
165
345
settings.remove(HINT_BATTERY_OPTIMIZATIONS )
166
346
}
167
347
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
+
168
378
}
169
379
170
380
0 commit comments