diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/AppSettingsActivity.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/AppSettingsActivity.kt index 705fa0fb..b20f3a48 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/ui/AppSettingsActivity.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/AppSettingsActivity.kt @@ -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() } diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/intro/BatteryOptimizationsFragment.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/intro/BatteryOptimizationsPage.kt similarity index 73% rename from app/src/main/kotlin/at/bitfire/davdroid/ui/intro/BatteryOptimizationsFragment.kt rename to app/src/main/kotlin/at/bitfire/davdroid/ui/intro/BatteryOptimizationsPage.kt index 65f2c09a..6991314b 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/ui/intro/BatteryOptimizationsFragment.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/intro/BatteryOptimizationsPage.kt @@ -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() + @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)) } -} + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/intro/IntroActivity.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/intro/IntroActivity.kt index de36c100..405a3ac3 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/ui/intro/IntroActivity.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/intro/IntroActivity.kt @@ -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() 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() + 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? = null + val pages: List + @Synchronized + get() { + _pages?.let { return it } + + val newPages = calculatePages() + _pages = newPages + + return newPages + } + + private fun calculatePages(): List { + for (page in introPages) + Logger.log.fine("Found intro page ${page::class.java} with order ${page.getShowPolicy(getApplication())}") + + val activePages: Map = 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() + activePages.filterValues { it != IntroPage.ShowPolicy.DONT_SHOW }.forEach { page, _ -> + pages += page + } + pages + } else + emptyList() + } + + } + } \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/intro/IntroFragmentFactory.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/intro/IntroFragmentFactory.kt deleted file mode 100644 index 3488f84d..00000000 --- a/app/src/main/kotlin/at/bitfire/davdroid/ui/intro/IntroFragmentFactory.kt +++ /dev/null @@ -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 - -} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/intro/IntroPage.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/intro/IntroPage.kt new file mode 100644 index 00000000..a87fa6d8 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/intro/IntroPage.kt @@ -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() + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/intro/IntroPageFactory.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/intro/IntroPageFactory.kt new file mode 100644 index 00000000..b13dde58 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/intro/IntroPageFactory.kt @@ -0,0 +1,7 @@ +package at.bitfire.davdroid.ui.intro + +interface IntroPageFactory { + + val introPages: Array + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/intro/OpenSourceFragment.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/intro/OpenSourcePage.kt similarity index 70% rename from app/src/main/kotlin/at/bitfire/davdroid/ui/intro/OpenSourceFragment.kt rename to app/src/main/kotlin/at/bitfire/davdroid/ui/intro/OpenSourcePage.kt index 6f4f1b9b..d66c5e4f 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/ui/intro/OpenSourceFragment.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/intro/OpenSourcePage.kt @@ -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() - - - 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() - - } - } \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/intro/PermissionsIntroFragment.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/intro/PermissionsIntroFragment.kt deleted file mode 100644 index 6fb91d14..00000000 --- a/app/src/main/kotlin/at/bitfire/davdroid/ui/intro/PermissionsIntroFragment.kt +++ /dev/null @@ -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() - - } - -} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/intro/PermissionsIntroPage.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/intro/PermissionsIntroPage.kt new file mode 100644 index 00000000..d3eed614 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/intro/PermissionsIntroPage.kt @@ -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() + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/intro/TasksIntroFragment.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/intro/TasksIntroFragment.kt deleted file mode 100644 index 116cd65e..00000000 --- a/app/src/main/kotlin/at/bitfire/davdroid/ui/intro/TasksIntroFragment.kt +++ /dev/null @@ -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() - - } - -} diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/intro/TasksIntroPage.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/intro/TasksIntroPage.kt new file mode 100644 index 00000000..1ce06836 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/intro/TasksIntroPage.kt @@ -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()) + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/intro/WelcomeFragment.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/intro/WelcomePage.kt similarity index 80% rename from app/src/main/kotlin/at/bitfire/davdroid/ui/intro/WelcomeFragment.kt rename to app/src/main/kotlin/at/bitfire/davdroid/ui/intro/WelcomePage.kt index 3ba7d013..bbf589d4 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/ui/intro/WelcomeFragment.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/intro/WelcomePage.kt @@ -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() - - } - -} +} \ No newline at end of file diff --git a/app/src/main/res/layout/activity_account.xml b/app/src/main/res/layout/activity_account.xml deleted file mode 100644 index 2fe72482..00000000 --- a/app/src/main/res/layout/activity_account.xml +++ /dev/null @@ -1,64 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/intro_permissions.xml b/app/src/main/res/layout/intro_permissions.xml deleted file mode 100644 index 60d774f3..00000000 --- a/app/src/main/res/layout/intro_permissions.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/src/ose/kotlin/at/bitfire/davdroid/OseFlavorModule.kt b/app/src/ose/kotlin/at/bitfire/davdroid/OseFlavorModule.kt index 33fe0ea8..506a35ea 100644 --- a/app/src/ose/kotlin/at/bitfire/davdroid/OseFlavorModule.kt +++ b/app/src/ose/kotlin/at/bitfire/davdroid/OseFlavorModule.kt @@ -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() } } \ No newline at end of file diff --git a/app/src/ose/kotlin/at/bitfire/davdroid/OseIntroPageFactory.kt b/app/src/ose/kotlin/at/bitfire/davdroid/OseIntroPageFactory.kt new file mode 100644 index 00000000..fc35adb8 --- /dev/null +++ b/app/src/ose/kotlin/at/bitfire/davdroid/OseIntroPageFactory.kt @@ -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() + ) + +} \ No newline at end of file