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.settings.Settings
import at.bitfire.davdroid.settings.SettingsManager
import at.bitfire.davdroid.ui.intro.BatteryOptimizationsFragment
import at.bitfire.davdroid.ui.intro.OpenSourceFragment
import at.bitfire.davdroid.ui.intro.BatteryOptimizationsPage
import com.google.android.material.snackbar.Snackbar
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope
@ -272,10 +271,10 @@ class AppSettingsActivity: AppCompatActivity() {
}
private fun resetHints() {
settings.remove(BatteryOptimizationsFragment.Model.HINT_BATTERY_OPTIMIZATIONS)
settings.remove(BatteryOptimizationsFragment.Model.HINT_AUTOSTART_PERMISSION)
settings.remove(BatteryOptimizationsPage.Model.HINT_BATTERY_OPTIMIZATIONS)
settings.remove(BatteryOptimizationsPage.Model.HINT_AUTOSTART_PERMISSION)
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()
}

View file

@ -6,15 +6,14 @@ package at.bitfire.davdroid.ui.intro
import android.annotation.SuppressLint
import android.app.Application
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.PowerManager
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContract
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.clickable
@ -40,95 +39,103 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
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.ViewModel
import androidx.lifecycle.viewmodel.compose.viewModel
import at.bitfire.davdroid.App
import at.bitfire.davdroid.BuildConfig
import at.bitfire.davdroid.R
import at.bitfire.davdroid.settings.SettingsManager
import at.bitfire.davdroid.ui.intro.BatteryOptimizationsFragment.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_AUTOSTART_PERMISSION
import at.bitfire.davdroid.ui.intro.BatteryOptimizationsPage.Model.Companion.HINT_BATTERY_OPTIMIZATIONS
import at.bitfire.davdroid.ui.widget.SafeAndroidUriHandler
import com.google.accompanist.themeadapter.material.MdcTheme
import dagger.Binds
import dagger.Module
import dagger.hilt.EntryPoint
import dagger.hilt.InstallIn
import dagger.hilt.android.AndroidEntryPoint
import dagger.hilt.android.components.ActivityComponent
import dagger.hilt.android.EntryPointAccessors
import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.multibindings.IntoSet
import dagger.hilt.components.SingletonComponent
import org.apache.commons.text.WordUtils
import java.util.*
import java.util.Locale
import javax.inject.Inject
@AndroidEntryPoint
class BatteryOptimizationsFragment: Fragment() {
class BatteryOptimizationsPage: IntroPage {
val model by viewModels<Model>()
@EntryPoint
@InstallIn(SingletonComponent::class)
interface BatteryOptimizationsPageEntryPoint {
fun settingsManager(): SettingsManager
}
private val ignoreBatteryOptimizationsResultLauncher =
registerForActivityResult(IgnoreBatteryOptimizationsContract) {
override fun getShowPolicy(application: Application): IntroPage.ShowPolicy {
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()
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
return ComposeView(requireContext()).apply {
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
setContent {
MdcTheme {
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
)
}
}
}
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)
}
}
override fun onResume() {
super.onResume()
model.checkBatteryOptimizations()
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
)
}
}
@HiltViewModel
class Model @Inject constructor(
application: Application,
val context: Application,
val settings: SettingsManager
): AndroidViewModel(application) {
): ViewModel() {
companion object {
@ -178,8 +185,26 @@ class BatteryOptimizationsFragment: Fragment() {
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() {
val exempted = isExempted(getApplication())
val exempted = isExempted(context)
isExempted.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)
@ -391,4 +388,5 @@ private fun BatteryOptimizationsContent(
)
Spacer(modifier = Modifier.height(90.dp))
}
}
}

View file

@ -5,65 +5,63 @@
package at.bitfire.davdroid.ui.intro
import android.app.Activity
import android.app.Application
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.activity.addCallback
import androidx.activity.result.contract.ActivityResultContract
import androidx.activity.viewModels
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.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.log.Logger
import com.github.appintro.AppIntro2
import dagger.hilt.EntryPoint
import dagger.hilt.InstallIn
import com.google.accompanist.themeadapter.material.MdcTheme
import dagger.hilt.android.AndroidEntryPoint
import dagger.hilt.android.EntryPointAccessors
import dagger.hilt.android.components.ActivityComponent
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
@AndroidEntryPoint
class IntroActivity: AppIntro2() {
@EntryPoint
@InstallIn(ActivityComponent::class)
interface IntroActivityEntryPoint {
fun introFragmentFactories(): Set<@JvmSuppressWildcards IntroFragmentFactory>
}
class IntroActivity : AppIntro2() {
companion object {
@WorkerThread
fun shouldShowIntroActivity(activity: Activity): Boolean {
val factories = EntryPointAccessors.fromActivity(activity, IntroActivityEntryPoint::class.java).introFragmentFactories()
return factories.any {
it.getOrder(activity) > 0
val pages = OseIntroPageFactory().introPages
return pages.any {
it.getShowPolicy(activity.application) == IntroPage.ShowPolicy.SHOW_ALWAYS
}
}
}
val model by viewModels<Model>()
private var currentSlide = 0
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val factories = EntryPointAccessors.fromActivity(this, IntroActivityEntryPoint::class.java).introFragmentFactories()
for (factory in factories)
Logger.log.fine("Found intro fragment factory ${factory::class.java} with order ${factory.getOrder(this)}")
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())
model.pages.forEachIndexed { idx, _ ->
addSlide(PageFragment().apply {
arguments = Bundle(1).apply {
putInt(PageFragment.ARG_PAGE_IDX, idx)
}
})
}
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.
*/
@ -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
import android.content.Context
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.app.Application
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
@ -31,67 +27,75 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.databinding.ObservableBoolean
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewmodel.compose.viewModel
import at.bitfire.davdroid.App
import at.bitfire.davdroid.R
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.SafeAndroidUriHandler
import com.google.accompanist.themeadapter.material.MdcTheme
import dagger.hilt.android.AndroidEntryPoint
import dagger.hilt.EntryPoint
import dagger.hilt.InstallIn
import dagger.hilt.android.EntryPointAccessors
import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.components.SingletonComponent
import javax.inject.Inject
@AndroidEntryPoint
class OpenSourceFragment: Fragment() {
class OpenSourcePage : IntroPage {
val model by viewModels<Model>()
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
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
}
)
}
}
}
}
@EntryPoint
@InstallIn(SingletonComponent::class)
interface OpenSourcePageEntryPoint {
fun settingsManager(): SettingsManager
}
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(
showBackground = true,
showSystemUi = true
)
@Composable
fun FragmentContent(
fun PageContent(
dontShow: Boolean = false,
onChangeDontShow: (Boolean) -> Unit = {}
) {
val context = LocalContext.current
val uriHandler = LocalUriHandler.current
Column(
@ -112,7 +116,7 @@ class OpenSourceFragment: Fragment() {
OutlinedButton(
onClick = {
uriHandler.openUri(
App.homepageUrl(requireActivity())
App.homepageUrl(context)
.buildUpon()
.appendPath("donate")
.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
import android.content.Context
import android.app.Application
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.background
import androidx.compose.foundation.layout.Column
@ -26,7 +22,6 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.res.colorResource
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.unit.dp
import androidx.compose.ui.unit.sp
import androidx.fragment.app.Fragment
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 {
return ComposeView(requireContext()).apply {
setContent {
if (LocalConfiguration.current.orientation == Configuration.ORIENTATION_LANDSCAPE)
ContentLandscape()
else
ContentPortrait()
}
}
override fun getShowPolicy(application: Application) = IntroPage.ShowPolicy.SHOW_ONLY_WITH_OTHERS
@Composable
override fun ComposePage() {
if (LocalConfiguration.current.orientation == Configuration.ORIENTATION_LANDSCAPE)
ContentLandscape()
else
ContentPortrait()
}
@Preview(
device = "id:3.7in WVGA (Nexus One)",
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.OpenSourceLicenseInfoProvider
import at.bitfire.davdroid.ui.OseAccountsDrawerHandler
import at.bitfire.davdroid.ui.intro.IntroFragmentFactory
import at.bitfire.davdroid.ui.intro.OpenSourceFragment
import at.bitfire.davdroid.ui.intro.PermissionsIntroFragment
import at.bitfire.davdroid.ui.intro.TasksIntroFragment
import at.bitfire.davdroid.ui.intro.BatteryOptimizationsPage
import at.bitfire.davdroid.ui.intro.IntroPage
import at.bitfire.davdroid.ui.intro.IntroPageFactory
import dagger.Binds
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.components.ActivityComponent
import dagger.hilt.components.SingletonComponent
import dagger.multibindings.IntoSet
interface OseFlavorModules {
@ -30,28 +31,22 @@ interface OseFlavorModules {
fun appLicenseInfoProvider(impl: OpenSourceLicenseInfoProvider): AboutActivity.AppLicenseInfoProvider
}
//// intro fragments ////
@Module
@InstallIn(ActivityComponent::class)
interface OpenSourceFragmentModule {
@Binds @IntoSet
fun getFactory(factory: OpenSourceFragment.Factory): IntroFragmentFactory
@InstallIn(SingletonComponent::class)
interface Global {
@Binds
fun introPageFactory(impl: OseIntroPageFactory): IntroPageFactory
}
@Module
@InstallIn(ActivityComponent::class)
interface PermissionsIntroFragmentModule {
@Binds @IntoSet
fun getFactory(factory: PermissionsIntroFragment.Factory): IntroFragmentFactory
}
//// intro pages ////
@Module
@InstallIn(ActivityComponent::class)
interface TasksIntroFragmentModule {
@Binds @IntoSet
fun getFactory(factory: TasksIntroFragment.Factory): IntroFragmentFactory
@InstallIn(SingletonComponent::class)
interface IntroPagesModule {
@Provides
@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()
)
}