Rewrite intro pages to Compose (dropping all Fragments) (#626)

This commit is contained in:
Ricki Hirner 2024-03-09 14:25:24 +01:00 committed by GitHub
parent 1cbfedc9e4
commit 42f99e644d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 400 additions and 464 deletions

View file

@ -27,8 +27,7 @@ import at.bitfire.davdroid.R
import at.bitfire.davdroid.resource.TaskUtils import at.bitfire.davdroid.resource.TaskUtils
import at.bitfire.davdroid.settings.Settings import at.bitfire.davdroid.settings.Settings
import at.bitfire.davdroid.settings.SettingsManager import at.bitfire.davdroid.settings.SettingsManager
import at.bitfire.davdroid.ui.intro.BatteryOptimizationsFragment import at.bitfire.davdroid.ui.intro.BatteryOptimizationsPage
import at.bitfire.davdroid.ui.intro.OpenSourceFragment
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@ -272,10 +271,10 @@ class AppSettingsActivity: AppCompatActivity() {
} }
private fun resetHints() { private fun resetHints() {
settings.remove(BatteryOptimizationsFragment.Model.HINT_BATTERY_OPTIMIZATIONS) settings.remove(BatteryOptimizationsPage.Model.HINT_BATTERY_OPTIMIZATIONS)
settings.remove(BatteryOptimizationsFragment.Model.HINT_AUTOSTART_PERMISSION) settings.remove(BatteryOptimizationsPage.Model.HINT_AUTOSTART_PERMISSION)
settings.remove(TasksModel.HINT_OPENTASKS_NOT_INSTALLED) settings.remove(TasksModel.HINT_OPENTASKS_NOT_INSTALLED)
settings.remove(OpenSourceFragment.Model.SETTING_NEXT_DONATION_POPUP) //settings.remove(OpenSourceFragment.Model.SETTING_NEXT_DONATION_POPUP)
Snackbar.make(requireView(), R.string.app_settings_reset_hints_success, Snackbar.LENGTH_LONG).show() Snackbar.make(requireView(), R.string.app_settings_reset_hints_success, Snackbar.LENGTH_LONG).show()
} }

View file

@ -6,15 +6,14 @@ package at.bitfire.davdroid.ui.intro
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.Application import android.app.Application
import android.content.BroadcastReceiver
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.IntentFilter
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
import android.os.Bundle
import android.os.PowerManager import android.os.PowerManager
import android.view.LayoutInflater import androidx.activity.compose.rememberLauncherForActivityResult
import android.view.View
import android.view.ViewGroup
import androidx.activity.result.contract.ActivityResultContract import androidx.activity.result.contract.ActivityResultContract
import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
@ -40,95 +39,103 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.core.content.getSystemService import androidx.core.content.getSystemService
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewmodel.compose.viewModel
import at.bitfire.davdroid.App import at.bitfire.davdroid.App
import at.bitfire.davdroid.BuildConfig import at.bitfire.davdroid.BuildConfig
import at.bitfire.davdroid.R import at.bitfire.davdroid.R
import at.bitfire.davdroid.settings.SettingsManager import at.bitfire.davdroid.settings.SettingsManager
import at.bitfire.davdroid.ui.intro.BatteryOptimizationsFragment.Model.Companion.HINT_AUTOSTART_PERMISSION import at.bitfire.davdroid.ui.intro.BatteryOptimizationsPage.Model.Companion.HINT_AUTOSTART_PERMISSION
import at.bitfire.davdroid.ui.intro.BatteryOptimizationsFragment.Model.Companion.HINT_BATTERY_OPTIMIZATIONS import at.bitfire.davdroid.ui.intro.BatteryOptimizationsPage.Model.Companion.HINT_BATTERY_OPTIMIZATIONS
import at.bitfire.davdroid.ui.widget.SafeAndroidUriHandler import at.bitfire.davdroid.ui.widget.SafeAndroidUriHandler
import com.google.accompanist.themeadapter.material.MdcTheme import com.google.accompanist.themeadapter.material.MdcTheme
import dagger.Binds import dagger.hilt.EntryPoint
import dagger.Module
import dagger.hilt.InstallIn import dagger.hilt.InstallIn
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.EntryPointAccessors
import dagger.hilt.android.components.ActivityComponent
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.multibindings.IntoSet import dagger.hilt.components.SingletonComponent
import org.apache.commons.text.WordUtils import org.apache.commons.text.WordUtils
import java.util.* import java.util.Locale
import javax.inject.Inject import javax.inject.Inject
@AndroidEntryPoint class BatteryOptimizationsPage: IntroPage {
class BatteryOptimizationsFragment: Fragment() {
val model by viewModels<Model>() @EntryPoint
@InstallIn(SingletonComponent::class)
interface BatteryOptimizationsPageEntryPoint {
fun settingsManager(): SettingsManager
}
private val ignoreBatteryOptimizationsResultLauncher = override fun getShowPolicy(application: Application): IntroPage.ShowPolicy {
registerForActivityResult(IgnoreBatteryOptimizationsContract) { val settingsManager = EntryPointAccessors.fromApplication(application, BatteryOptimizationsPageEntryPoint::class.java).settingsManager()
// show fragment when:
// 1. DAVx5 is not whitelisted yet and "don't show anymore" has not been clicked, and/or
// 2a. evil manufacturer AND
// 2b. "don't show anymore" has not been clicked
return if (
(!Model.isExempted(application) && settingsManager.getBooleanOrNull(HINT_BATTERY_OPTIMIZATIONS) != false) ||
(Model.manufacturerWarning && settingsManager.getBooleanOrNull(HINT_AUTOSTART_PERMISSION) != false)
)
IntroPage.ShowPolicy.SHOW_ALWAYS
else
IntroPage.ShowPolicy.DONT_SHOW
}
@Composable
override fun ComposePage() {
IntroPage_FromModel()
}
@Composable
private fun IntroPage_FromModel(
model: Model = viewModel()
) {
val ignoreBatteryOptimizationsResultLauncher = rememberLauncherForActivityResult(IgnoreBatteryOptimizationsContract) {
model.checkBatteryOptimizations() model.checkBatteryOptimizations()
} }
val hintBatteryOptimizations by model.hintBatteryOptimizations.observeAsState()
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { val shouldBeExempted by model.shouldBeExempted.observeAsState(false)
return ComposeView(requireContext()).apply { val isExempted by model.isExempted.observeAsState(false)
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) LaunchedEffect(shouldBeExempted, isExempted) {
setContent { if (shouldBeExempted && !isExempted)
MdcTheme { ignoreBatteryOptimizationsResultLauncher.launch(BuildConfig.APPLICATION_ID)
val hintBatteryOptimizations by model.hintBatteryOptimizations.observeAsState()
val shouldBeExempted by model.shouldBeExempted.observeAsState(false)
val isExempted by model.isExempted.observeAsState(false)
LaunchedEffect(shouldBeExempted, isExempted) {
if (shouldBeExempted && !isExempted)
ignoreBatteryOptimizationsResultLauncher.launch(BuildConfig.APPLICATION_ID)
}
val hintAutostartPermission by model.hintAutostartPermission.observeAsState()
val uriHandler = SafeAndroidUriHandler(LocalContext.current)
CompositionLocalProvider(LocalUriHandler provides uriHandler) {
BatteryOptimizationsContent(
dontShowBattery = hintBatteryOptimizations == false,
onChangeDontShowBattery = {
model.hintBatteryOptimizations.value = !it
},
isExempted = isExempted,
shouldBeExempted = shouldBeExempted,
onChangeShouldBeExempted = model.shouldBeExempted::postValue,
dontShowAutostart = hintAutostartPermission == false,
onChangeDontShowAutostart = {
model.hintAutostartPermission.value = !it
},
manufacturerWarning = Model.manufacturerWarning
)
}
}
}
} }
}
override fun onResume() { val hintAutostartPermission by model.hintAutostartPermission.observeAsState()
super.onResume() val uriHandler = SafeAndroidUriHandler(LocalContext.current)
model.checkBatteryOptimizations() CompositionLocalProvider(LocalUriHandler provides uriHandler) {
BatteryOptimizationsContent(
dontShowBattery = hintBatteryOptimizations == false,
onChangeDontShowBattery = {
model.hintBatteryOptimizations.value = !it
},
isExempted = isExempted,
shouldBeExempted = shouldBeExempted,
onChangeShouldBeExempted = model.shouldBeExempted::postValue,
dontShowAutostart = hintAutostartPermission == false,
onChangeDontShowAutostart = {
model.hintAutostartPermission.value = !it
},
manufacturerWarning = Model.manufacturerWarning
)
}
} }
@HiltViewModel @HiltViewModel
class Model @Inject constructor( class Model @Inject constructor(
application: Application, val context: Application,
val settings: SettingsManager val settings: SettingsManager
): AndroidViewModel(application) { ): ViewModel() {
companion object { companion object {
@ -178,8 +185,26 @@ class BatteryOptimizationsFragment: Fragment() {
val hintAutostartPermission = settings.getBooleanLive(HINT_AUTOSTART_PERMISSION) val hintAutostartPermission = settings.getBooleanLive(HINT_AUTOSTART_PERMISSION)
private val batteryOptimizationsReceiver = object: BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
checkBatteryOptimizations()
}
}
init {
// There's an undocumented intent that is sent when the battery optimization whitelist changes.
val intentFilter = IntentFilter("android.os.action.POWER_SAVE_WHITELIST_CHANGED")
context.registerReceiver(batteryOptimizationsReceiver, intentFilter)
checkBatteryOptimizations()
}
override fun onCleared() {
context.unregisterReceiver(batteryOptimizationsReceiver)
}
fun checkBatteryOptimizations() { fun checkBatteryOptimizations() {
val exempted = isExempted(getApplication()) val exempted = isExempted(context)
isExempted.value = exempted isExempted.value = exempted
shouldBeExempted.value = exempted shouldBeExempted.value = exempted
@ -205,34 +230,6 @@ class BatteryOptimizationsFragment: Fragment() {
} }
} }
@Module
@InstallIn(ActivityComponent::class)
abstract class BatteryOptimizationsFragmentModule {
@Binds @IntoSet
abstract fun getFactory(factory: Factory): IntroFragmentFactory
}
class Factory @Inject constructor(
val settingsManager: SettingsManager
): IntroFragmentFactory {
override fun getOrder(context: Context) =
// show fragment when:
// 1. DAVx5 is not whitelisted yet and "don't show anymore" has not been clicked, and/or
// 2a. evil manufacturer AND
// 2b. "don't show anymore" has not been clicked
if (
(!Model.isExempted(context) && settingsManager.getBooleanOrNull(HINT_BATTERY_OPTIMIZATIONS) != false) ||
(Model.manufacturerWarning && settingsManager.getBooleanOrNull(HINT_AUTOSTART_PERMISSION) != false)
)
100
else
IntroFragmentFactory.DONT_SHOW
override fun create() = BatteryOptimizationsFragment()
}
} }
@Preview(showBackground = true, showSystemUi = true) @Preview(showBackground = true, showSystemUi = true)
@ -391,4 +388,5 @@ private fun BatteryOptimizationsContent(
) )
Spacer(modifier = Modifier.height(90.dp)) Spacer(modifier = Modifier.height(90.dp))
} }
}
}

View file

@ -5,65 +5,63 @@
package at.bitfire.davdroid.ui.intro package at.bitfire.davdroid.ui.intro
import android.app.Activity import android.app.Activity
import android.app.Application
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.activity.addCallback import androidx.activity.addCallback
import androidx.activity.result.contract.ActivityResultContract import androidx.activity.result.contract.ActivityResultContract
import androidx.activity.viewModels
import androidx.annotation.WorkerThread import androidx.annotation.WorkerThread
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.padding
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.compose.ui.res.dimensionResource
import androidx.core.content.res.ResourcesCompat import androidx.core.content.res.ResourcesCompat
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.AndroidViewModel
import at.bitfire.davdroid.OseIntroPageFactory
import at.bitfire.davdroid.R import at.bitfire.davdroid.R
import at.bitfire.davdroid.log.Logger import at.bitfire.davdroid.log.Logger
import com.github.appintro.AppIntro2 import com.github.appintro.AppIntro2
import dagger.hilt.EntryPoint import com.google.accompanist.themeadapter.material.MdcTheme
import dagger.hilt.InstallIn
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import dagger.hilt.android.EntryPointAccessors import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.components.ActivityComponent import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
class IntroActivity: AppIntro2() { class IntroActivity : AppIntro2() {
@EntryPoint
@InstallIn(ActivityComponent::class)
interface IntroActivityEntryPoint {
fun introFragmentFactories(): Set<@JvmSuppressWildcards IntroFragmentFactory>
}
companion object { companion object {
@WorkerThread @WorkerThread
fun shouldShowIntroActivity(activity: Activity): Boolean { fun shouldShowIntroActivity(activity: Activity): Boolean {
val factories = EntryPointAccessors.fromActivity(activity, IntroActivityEntryPoint::class.java).introFragmentFactories() val pages = OseIntroPageFactory().introPages
return factories.any { return pages.any {
it.getOrder(activity) > 0 it.getShowPolicy(activity.application) == IntroPage.ShowPolicy.SHOW_ALWAYS
} }
} }
} }
val model by viewModels<Model>()
private var currentSlide = 0 private var currentSlide = 0
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
val factories = EntryPointAccessors.fromActivity(this, IntroActivityEntryPoint::class.java).introFragmentFactories() model.pages.forEachIndexed { idx, _ ->
for (factory in factories) addSlide(PageFragment().apply {
Logger.log.fine("Found intro fragment factory ${factory::class.java} with order ${factory.getOrder(this)}") arguments = Bundle(1).apply {
putInt(PageFragment.ARG_PAGE_IDX, idx)
val factoriesWithOrder = factories }
.associateWith { it.getOrder(this) } })
.filterValues { it != IntroFragmentFactory.DONT_SHOW }
val anyPositiveOrder = factoriesWithOrder.values.any { it > 0 }
if (anyPositiveOrder) {
val factoriesSortedByOrder = factoriesWithOrder
.toList()
.sortedBy { (_, v) -> v } // sort by value (= getOrder())
for ((factory, _) in factoriesSortedByOrder)
addSlide(factory.create())
} }
setBarColor(ResourcesCompat.getColor(resources, R.color.primaryDarkColor, null)) setBarColor(ResourcesCompat.getColor(resources, R.color.primaryDarkColor, null))
@ -91,6 +89,31 @@ class IntroActivity: AppIntro2() {
} }
@AndroidEntryPoint
class PageFragment: Fragment() {
companion object {
const val ARG_PAGE_IDX = "page"
}
val model by activityViewModels<Model>()
val page by lazy { model.pages[requireArguments().getInt(ARG_PAGE_IDX)] }
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?) =
ComposeView(requireActivity()).apply {
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
setContent {
MdcTheme {
Box(Modifier.padding(bottom = dimensionResource(com.github.appintro.R.dimen.appintro2_bottombar_height))) {
page.ComposePage()
}
}
}
}
}
/** /**
* For launching the [IntroActivity]. Result is `true` when the user cancelled the intro. * For launching the [IntroActivity]. Result is `true` when the user cancelled the intro.
*/ */
@ -103,4 +126,46 @@ class IntroActivity: AppIntro2() {
} }
} }
@HiltViewModel
class Model @Inject constructor(
application: Application,
introPageFactory: IntroPageFactory
): AndroidViewModel(application) {
private val introPages = introPageFactory.introPages
private var _pages: List<IntroPage>? = null
val pages: List<IntroPage>
@Synchronized
get() {
_pages?.let { return it }
val newPages = calculatePages()
_pages = newPages
return newPages
}
private fun calculatePages(): List<IntroPage> {
for (page in introPages)
Logger.log.fine("Found intro page ${page::class.java} with order ${page.getShowPolicy(getApplication())}")
val activePages: Map<IntroPage, IntroPage.ShowPolicy> = introPages
.associateWith { it.getShowPolicy(getApplication()) }
.filterValues { it != IntroPage.ShowPolicy.DONT_SHOW }
val anyShowAlways = activePages.values.any { it == IntroPage.ShowPolicy.SHOW_ALWAYS }
return if (anyShowAlways) {
val pages = mutableListOf<IntroPage>()
activePages.filterValues { it != IntroPage.ShowPolicy.DONT_SHOW }.forEach { page, _ ->
pages += page
}
pages
} else
emptyList()
}
}
} }

View file

@ -1,38 +0,0 @@
/***************************************************************************************************
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
**************************************************************************************************/
package at.bitfire.davdroid.ui.intro
import android.content.Context
import androidx.fragment.app.Fragment
interface IntroFragmentFactory {
companion object {
const val DONT_SHOW = 0
}
/**
* Used to determine whether an intro fragment of this type (for instance,
* the [BatteryOptimizationsFragment]) should be shown.
*
* @param context used to determine whether the fragment shall be shown
*
* @return Order with which an instance of this fragment type shall be created and shown. Possible values:
*
* * <0: only show the fragment when there is at least one other fragment with positive order (lower numbers are shown first)
* * [DONT_SHOW] (0): don't show the fragment
* * 0: show the fragment (lower numbers are shown first)
*/
fun getOrder(context: Context): Int
/**
* Creates an instance of this intro fragment type. Will only be called when
* [getOrder] is true.
*
* @return the fragment (for instance, a [BatteryOptimizationsFragment]])
*/
fun create(): Fragment
}

View file

@ -0,0 +1,38 @@
/***************************************************************************************************
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
**************************************************************************************************/
package at.bitfire.davdroid.ui.intro
import android.app.Application
import androidx.compose.runtime.Composable
interface IntroPage {
enum class ShowPolicy {
DONT_SHOW,
SHOW_ALWAYS,
SHOW_ONLY_WITH_OTHERS
}
/**
* Used to determine whether an intro page of this type (for instance,
* the [BatteryOptimizationsPage]) should be shown.
*
* @param application used to determine whether the page shall be shown
*
* @return Order with which an instance of this page type shall be created and shown. Possible values:
*
* * < 0: only show the page when there is at least one other page with positive order (lower numbers are shown first)
* * [DONT_SHOW] (0): don't show the page
* * 0: show the page (lower numbers are shown first)
*/
fun getShowPolicy(application: Application): ShowPolicy
/**
* Composes this page. Will only be called when [getShowPolicy] is not [DONT_SHOW].
*/
@Composable
fun ComposePage()
}

View file

@ -0,0 +1,7 @@
package at.bitfire.davdroid.ui.intro
interface IntroPageFactory {
val introPages: Array<IntroPage>
}

View file

@ -4,11 +4,7 @@
package at.bitfire.davdroid.ui.intro package at.bitfire.davdroid.ui.intro
import android.content.Context import android.app.Application
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
@ -31,67 +27,75 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.databinding.ObservableBoolean import androidx.databinding.ObservableBoolean
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewmodel.compose.viewModel
import at.bitfire.davdroid.App import at.bitfire.davdroid.App
import at.bitfire.davdroid.R import at.bitfire.davdroid.R
import at.bitfire.davdroid.settings.SettingsManager import at.bitfire.davdroid.settings.SettingsManager
import at.bitfire.davdroid.ui.intro.OpenSourceFragment.Model.Companion.SETTING_NEXT_DONATION_POPUP
import at.bitfire.davdroid.ui.widget.CardWithImage import at.bitfire.davdroid.ui.widget.CardWithImage
import at.bitfire.davdroid.ui.widget.SafeAndroidUriHandler import at.bitfire.davdroid.ui.widget.SafeAndroidUriHandler
import com.google.accompanist.themeadapter.material.MdcTheme import dagger.hilt.EntryPoint
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.InstallIn
import dagger.hilt.android.EntryPointAccessors
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.components.SingletonComponent
import javax.inject.Inject import javax.inject.Inject
@AndroidEntryPoint class OpenSourcePage : IntroPage {
class OpenSourceFragment: Fragment() {
val model by viewModels<Model>() @EntryPoint
@InstallIn(SingletonComponent::class)
interface OpenSourcePageEntryPoint {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { fun settingsManager(): SettingsManager
return ComposeView(requireContext()).apply {
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
setContent {
MdcTheme {
var dontShow by remember { mutableStateOf(model.dontShow.get()) }
val uriHandler = SafeAndroidUriHandler(LocalContext.current)
CompositionLocalProvider(LocalUriHandler provides uriHandler) {
FragmentContent(
dontShow = dontShow,
onChangeDontShow = {
model.dontShow.set(it)
dontShow = it
}
)
}
}
}
}
} }
override fun getShowPolicy(application: Application): IntroPage.ShowPolicy {
val settingsManager = EntryPointAccessors.fromApplication(application, OpenSourcePageEntryPoint::class.java).settingsManager()
return if (System.currentTimeMillis() > (settingsManager.getLongOrNull(Model.SETTING_NEXT_DONATION_POPUP) ?: 0))
IntroPage.ShowPolicy.SHOW_ALWAYS
else
IntroPage.ShowPolicy.DONT_SHOW
}
@Composable
override fun ComposePage() {
Page()
}
@Composable
private fun Page(model: Model = viewModel()) {
var dontShow by remember { mutableStateOf(model.dontShow.get()) }
val uriHandler = SafeAndroidUriHandler(LocalContext.current)
CompositionLocalProvider(LocalUriHandler provides uriHandler) {
PageContent(
dontShow = dontShow,
onChangeDontShow = {
model.dontShow.set(it)
dontShow = it
}
)
}
}
@Preview( @Preview(
showBackground = true, showBackground = true,
showSystemUi = true showSystemUi = true
) )
@Composable @Composable
fun FragmentContent( fun PageContent(
dontShow: Boolean = false, dontShow: Boolean = false,
onChangeDontShow: (Boolean) -> Unit = {} onChangeDontShow: (Boolean) -> Unit = {}
) { ) {
val context = LocalContext.current
val uriHandler = LocalUriHandler.current val uriHandler = LocalUriHandler.current
Column( Column(
@ -112,7 +116,7 @@ class OpenSourceFragment: Fragment() {
OutlinedButton( OutlinedButton(
onClick = { onClick = {
uriHandler.openUri( uriHandler.openUri(
App.homepageUrl(requireActivity()) App.homepageUrl(context)
.buildUpon() .buildUpon()
.appendPath("donate") .appendPath("donate")
.build() .build()
@ -163,19 +167,4 @@ class OpenSourceFragment: Fragment() {
} }
class Factory @Inject constructor(
val settingsManager: SettingsManager
): IntroFragmentFactory {
override fun getOrder(context: Context) =
if (System.currentTimeMillis() > (settingsManager.getLongOrNull(SETTING_NEXT_DONATION_POPUP) ?: 0))
500
else
0
override fun create() = OpenSourceFragment()
}
} }

View file

@ -1,44 +0,0 @@
/***************************************************************************************************
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
**************************************************************************************************/
package at.bitfire.davdroid.ui.intro
import android.content.Context
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import at.bitfire.davdroid.R
import at.bitfire.davdroid.util.PermissionUtils
import at.bitfire.davdroid.util.PermissionUtils.CALENDAR_PERMISSIONS
import at.bitfire.davdroid.util.PermissionUtils.CONTACT_PERMISSIONS
import at.bitfire.ical4android.TaskProvider
import javax.inject.Inject
class PermissionsIntroFragment : Fragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View =
inflater.inflate(R.layout.intro_permissions, container, false)
class Factory @Inject constructor(): IntroFragmentFactory {
override fun getOrder(context: Context): Int {
// show PermissionsFragment as intro fragment when no permissions are granted
val permissions = CONTACT_PERMISSIONS + CALENDAR_PERMISSIONS +
TaskProvider.PERMISSIONS_JTX +
TaskProvider.PERMISSIONS_OPENTASKS +
TaskProvider.PERMISSIONS_TASKS_ORG
return if (PermissionUtils.haveAnyPermission(context, permissions))
IntroFragmentFactory.DONT_SHOW
else
50
}
override fun create() = PermissionsIntroFragment()
}
}

View file

@ -0,0 +1,34 @@
/***************************************************************************************************
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
**************************************************************************************************/
package at.bitfire.davdroid.ui.intro
import android.app.Application
import androidx.compose.runtime.Composable
import at.bitfire.davdroid.ui.PermissionsFragmentContent
import at.bitfire.davdroid.util.PermissionUtils
import at.bitfire.davdroid.util.PermissionUtils.CALENDAR_PERMISSIONS
import at.bitfire.davdroid.util.PermissionUtils.CONTACT_PERMISSIONS
import at.bitfire.ical4android.TaskProvider
class PermissionsIntroPage: IntroPage {
override fun getShowPolicy(application: Application): IntroPage.ShowPolicy {
// show PermissionsFragment as intro fragment when no permissions are granted
val permissions = CONTACT_PERMISSIONS + CALENDAR_PERMISSIONS +
TaskProvider.PERMISSIONS_JTX +
TaskProvider.PERMISSIONS_OPENTASKS +
TaskProvider.PERMISSIONS_TASKS_ORG
return if (PermissionUtils.haveAnyPermission(application, permissions))
IntroPage.ShowPolicy.DONT_SHOW
else
IntroPage.ShowPolicy.SHOW_ALWAYS
}
@Composable
override fun ComposePage() {
PermissionsFragmentContent()
}
}

View file

@ -1,63 +0,0 @@
/***************************************************************************************************
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
**************************************************************************************************/
package at.bitfire.davdroid.ui.intro
import android.content.Context
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.compose.foundation.layout.padding
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.compose.ui.res.dimensionResource
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import at.bitfire.davdroid.resource.TaskUtils
import at.bitfire.davdroid.settings.SettingsManager
import at.bitfire.davdroid.ui.TasksCard
import at.bitfire.davdroid.ui.TasksModel
import com.github.appintro.R
import com.google.accompanist.themeadapter.material.MdcTheme
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
@AndroidEntryPoint
class TasksIntroFragment : Fragment() {
val model: TasksModel by viewModels()
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
return ComposeView(requireContext()).apply {
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
setContent {
MdcTheme {
TasksCard(
modifier = Modifier
.padding(bottom = dimensionResource(R.dimen.appintro2_bottombar_height)),
model = model
)
}
}
}
}
class Factory @Inject constructor(
val settingsManager: SettingsManager
): IntroFragmentFactory {
override fun getOrder(context: Context): Int {
return if (!TaskUtils.isAvailable(context) && settingsManager.getBooleanOrNull(TasksModel.HINT_OPENTASKS_NOT_INSTALLED) != false)
10
else
IntroFragmentFactory.DONT_SHOW
}
override fun create() = TasksIntroFragment()
}
}

View file

@ -0,0 +1,41 @@
/***************************************************************************************************
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
**************************************************************************************************/
package at.bitfire.davdroid.ui.intro
import android.app.Application
import androidx.compose.runtime.Composable
import androidx.lifecycle.viewmodel.compose.viewModel
import at.bitfire.davdroid.resource.TaskUtils
import at.bitfire.davdroid.settings.SettingsManager
import at.bitfire.davdroid.ui.TasksCard
import at.bitfire.davdroid.ui.TasksModel
import dagger.hilt.EntryPoint
import dagger.hilt.InstallIn
import dagger.hilt.android.EntryPointAccessors
import dagger.hilt.components.SingletonComponent
class TasksIntroPage : IntroPage {
@EntryPoint
@InstallIn(SingletonComponent::class)
interface TasksIntroPageEntryPoint {
fun settingsManager(): SettingsManager
}
override fun getShowPolicy(application: Application): IntroPage.ShowPolicy {
val settingsManager = EntryPointAccessors.fromApplication(application, TasksIntroPageEntryPoint::class.java).settingsManager()
return if (!TaskUtils.isAvailable(application) && settingsManager.getBooleanOrNull(TasksModel.HINT_OPENTASKS_NOT_INSTALLED) != false)
IntroPage.ShowPolicy.SHOW_ALWAYS
else
IntroPage.ShowPolicy.DONT_SHOW
}
@Composable
override fun ComposePage() {
TasksCard(model = viewModel<TasksModel>())
}
}

View file

@ -4,12 +4,8 @@
package at.bitfire.davdroid.ui.intro package at.bitfire.davdroid.ui.intro
import android.content.Context import android.app.Application
import android.content.res.Configuration import android.content.res.Configuration
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
@ -26,7 +22,6 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.res.colorResource import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.dimensionResource import androidx.compose.ui.res.dimensionResource
@ -36,28 +31,21 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.fragment.app.Fragment
import at.bitfire.davdroid.R import at.bitfire.davdroid.R
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.android.components.ActivityComponent
import dagger.multibindings.IntoSet
import javax.inject.Inject
class WelcomeFragment: Fragment() { class WelcomePage: IntroPage {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { override fun getShowPolicy(application: Application) = IntroPage.ShowPolicy.SHOW_ONLY_WITH_OTHERS
return ComposeView(requireContext()).apply {
setContent { @Composable
if (LocalConfiguration.current.orientation == Configuration.ORIENTATION_LANDSCAPE) override fun ComposePage() {
ContentLandscape() if (LocalConfiguration.current.orientation == Configuration.ORIENTATION_LANDSCAPE)
else ContentLandscape()
ContentPortrait() else
} ContentPortrait()
}
} }
@Preview( @Preview(
device = "id:3.7in WVGA (Nexus One)", device = "id:3.7in WVGA (Nexus One)",
showSystemUi = true showSystemUi = true
@ -164,20 +152,4 @@ class WelcomeFragment: Fragment() {
} }
} }
}
@Module
@InstallIn(ActivityComponent::class)
abstract class WelcomeFragmentModule {
@Binds @IntoSet
abstract fun getFactory(factory: Factory): IntroFragmentFactory
}
class Factory @Inject constructor() : IntroFragmentFactory {
override fun getOrder(context: Context) = -1000
override fun create() = WelcomeFragment()
}
}

View file

@ -1,64 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_height="match_parent"
android:layout_width="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
tools:context=".ui.account.AccountActivity">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="?attr/actionBarTheme">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:popupTheme="?attr/actionBarPopupTheme" />
<com.google.android.material.tabs.TabLayout
android:id="@+id/tab_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/Widget.MaterialComponents.TabLayout.Colored" />
</com.google.android.material.appbar.AppBarLayout>
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/view_pager"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_behavior="android.support.design.widget.AppBarLayout$ScrollingViewBehavior" />
</LinearLayout>
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/refresh"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="top|end"
android:layout_margin="@dimen/fab_margin"
app:backgroundTint="@android:color/white"
app:tint="@color/grey900"
app:srcCompat="@drawable/ic_folder_refresh_outline"
app:layout_anchor="@id/sync"
app:layout_anchorGravity="top|center"
tools:ignore="ContentDescription" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/sync"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_margin="@dimen/fab_margin"
android:contentDescription="@string/account_synchronize_now"
app:useCompatPadding="true"
app:srcCompat="@drawable/ic_sync" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View file

@ -1,14 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="@dimen/appintro2_bottombar_height">
<androidx.fragment.app.FragmentContainerView
android:id="@+id/frame_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:name="at.bitfire.davdroid.ui.PermissionsFragment"/>
</FrameLayout>

View file

@ -8,14 +8,15 @@ import at.bitfire.davdroid.ui.AboutActivity
import at.bitfire.davdroid.ui.AccountsDrawerHandler import at.bitfire.davdroid.ui.AccountsDrawerHandler
import at.bitfire.davdroid.ui.OpenSourceLicenseInfoProvider import at.bitfire.davdroid.ui.OpenSourceLicenseInfoProvider
import at.bitfire.davdroid.ui.OseAccountsDrawerHandler import at.bitfire.davdroid.ui.OseAccountsDrawerHandler
import at.bitfire.davdroid.ui.intro.IntroFragmentFactory import at.bitfire.davdroid.ui.intro.BatteryOptimizationsPage
import at.bitfire.davdroid.ui.intro.OpenSourceFragment import at.bitfire.davdroid.ui.intro.IntroPage
import at.bitfire.davdroid.ui.intro.PermissionsIntroFragment import at.bitfire.davdroid.ui.intro.IntroPageFactory
import at.bitfire.davdroid.ui.intro.TasksIntroFragment
import dagger.Binds import dagger.Binds
import dagger.Module import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn import dagger.hilt.InstallIn
import dagger.hilt.android.components.ActivityComponent import dagger.hilt.android.components.ActivityComponent
import dagger.hilt.components.SingletonComponent
import dagger.multibindings.IntoSet import dagger.multibindings.IntoSet
interface OseFlavorModules { interface OseFlavorModules {
@ -30,28 +31,22 @@ interface OseFlavorModules {
fun appLicenseInfoProvider(impl: OpenSourceLicenseInfoProvider): AboutActivity.AppLicenseInfoProvider fun appLicenseInfoProvider(impl: OpenSourceLicenseInfoProvider): AboutActivity.AppLicenseInfoProvider
} }
//// intro fragments ////
@Module @Module
@InstallIn(ActivityComponent::class) @InstallIn(SingletonComponent::class)
interface OpenSourceFragmentModule { interface Global {
@Binds @IntoSet @Binds
fun getFactory(factory: OpenSourceFragment.Factory): IntroFragmentFactory fun introPageFactory(impl: OseIntroPageFactory): IntroPageFactory
} }
@Module
@InstallIn(ActivityComponent::class) //// intro pages ////
interface PermissionsIntroFragmentModule {
@Binds @IntoSet
fun getFactory(factory: PermissionsIntroFragment.Factory): IntroFragmentFactory
}
@Module @Module
@InstallIn(ActivityComponent::class) @InstallIn(SingletonComponent::class)
interface TasksIntroFragmentModule { interface IntroPagesModule {
@Binds @IntoSet @Provides
fun getFactory(factory: TasksIntroFragment.Factory): IntroFragmentFactory @IntoSet
fun introPage(): IntroPage = BatteryOptimizationsPage()
} }
} }

View file

@ -0,0 +1,21 @@
package at.bitfire.davdroid
import at.bitfire.davdroid.ui.intro.BatteryOptimizationsPage
import at.bitfire.davdroid.ui.intro.IntroPageFactory
import at.bitfire.davdroid.ui.intro.OpenSourcePage
import at.bitfire.davdroid.ui.intro.PermissionsIntroPage
import at.bitfire.davdroid.ui.intro.TasksIntroPage
import at.bitfire.davdroid.ui.intro.WelcomePage
import javax.inject.Inject
class OseIntroPageFactory @Inject constructor(): IntroPageFactory {
override val introPages = arrayOf(
WelcomePage(),
TasksIntroPage(),
PermissionsIntroPage(),
BatteryOptimizationsPage(),
OpenSourcePage()
)
}