Compare commits

...

5 commits

Author SHA1 Message Date
Ricki Hirner d4b4981e26 Version bump to 4.4.1-alpha.3 2024-06-25 22:22:09 +02:00
Ricki Hirner e1f3785bc6 Add ProGuard rule to work around androidx-lifecycle 2.8.x + Compose 1.6 crash 2024-06-25 22:22:09 +02:00
Arnau Mora e92f261faf
Replace AppIntro by Compose Pager (#848)
* Migrated into to compose

Signed-off-by: Arnau Mora Gras <arnyminerz@proton.me>

* Got rid of AppIntro

Signed-off-by: Arnau Mora Gras <arnyminerz@proton.me>

* Imports cleanup

Signed-off-by: Arnau Mora Gras <arnyminerz@proton.me>

* Imports cleanup

Signed-off-by: Arnau Mora Gras <arnyminerz@proton.me>

* Removed padding

Signed-off-by: Arnau Mora <arnyminerz@proton.me>

* When launching intro, going back closes the app

Signed-off-by: Arnau Mora <arnyminerz@proton.me>

* Added content description

Signed-off-by: Arnau Mora Gras <arnyminerz@proton.me>

* Moved IntroActivity.Model to IntroModel

Signed-off-by: Arnau Mora Gras <arnyminerz@proton.me>

* Given fixed padding

Signed-off-by: Arnau Mora Gras <arnyminerz@proton.me>

* Moved intro composables together

Signed-off-by: Arnau Mora Gras <arnyminerz@proton.me>

* Do not create new task

Signed-off-by: Arnau Mora Gras <arnyminerz@proton.me>

* Minor changes

* Remove last XML styles that were required for AppIntro

---------

Signed-off-by: Arnau Mora Gras <arnyminerz@proton.me>
Signed-off-by: Arnau Mora <arnyminerz@proton.me>
Co-authored-by: Ricki Hirner <hirner@bitfire.at>
2024-06-25 15:12:25 +02:00
Ricki Hirner ea035fa931 Version bump to 4.4.1-alpha.2 2024-06-25 14:52:51 +02:00
Ricki Hirner 8167e8e3cb Update dependencies 2024-06-25 14:49:57 +02:00
10 changed files with 345 additions and 163 deletions

View file

@ -18,8 +18,8 @@ android {
defaultConfig {
applicationId = "at.bitfire.davdroid"
versionCode = 404010000
versionName = "4.4.1-alpha.1"
versionCode = 404010002
versionName = "4.4.1-alpha.3"
buildConfigField("long", "buildTime", "${System.currentTimeMillis()}L")
@ -184,7 +184,6 @@ dependencies {
implementation(libs.bitfire.vcard4android)
// third-party libs
implementation(libs.appintro)
implementation(libs.commons.collections)
@Suppress("RedundantSuppression")
implementation(libs.commons.io)

View file

@ -8,6 +8,10 @@
-dontobfuscate
-printusage build/reports/r8-usage.txt
# https://issuetracker.google.com/issues/336842920#comment33
# Should be removed as soon as Compose 1.7 (Compose BOM > 2024.06.00) is used
-keep class androidx.compose.ui.platform.AndroidCompositionLocals_androidKt { *; }
# ez-vcard: keep all vCard properties/parameters (used via reflection)
-keep class ezvcard.io.scribe.** { *; }
-keep class ezvcard.property.** { *; }

View file

@ -49,7 +49,7 @@
android:networkSecurityConfig="@xml/network_security_config"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme"
android:theme="@style/Theme.AppCompat.DayNight.NoActionBar"
android:resizeableActivity="true"
tools:ignore="UnusedAttribute"
android:supportsRtl="true">

View file

@ -8,98 +8,54 @@ import android.app.Activity
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.compose.BackHandler
import androidx.activity.compose.setContent
import androidx.activity.result.contract.ActivityResultContract
import androidx.activity.viewModels
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Surface
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.toArgb
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.activityViewModels
import androidx.lifecycle.ViewModel
import at.bitfire.davdroid.log.Logger
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.runtime.rememberCoroutineScope
import at.bitfire.davdroid.ui.AppTheme
import at.bitfire.davdroid.ui.M3ColorScheme
import com.github.appintro.AppIntro2
import dagger.hilt.android.AndroidEntryPoint
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
import kotlinx.coroutines.launch
@AndroidEntryPoint
class IntroActivity : AppIntro2() {
@OptIn(ExperimentalFoundationApi::class)
class IntroActivity : AppCompatActivity() {
val model by viewModels<Model>()
private var currentSlide = 0
val model by viewModels<IntroModel>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
model.pages.forEachIndexed { idx, _ ->
addSlide(PageFragment().apply {
arguments = Bundle(1).apply {
putInt(PageFragment.ARG_PAGE_IDX, idx)
}
})
}
val pages = model.pages
setBarColor(M3ColorScheme.primaryLight.toArgb())
isSkipButtonEnabled = false
setContent {
AppTheme {
val scope = rememberCoroutineScope()
val pagerState = rememberPagerState { pages.size }
onBackPressedDispatcher.addCallback(this) {
if (currentSlide == 0) {
setResult(Activity.RESULT_CANCELED)
finish()
} else {
goToPreviousSlide()
}
}
}
override fun onPageSelected(position: Int) {
super.onPageSelected(position)
currentSlide = position
}
override fun onDonePressed(currentFragment: Fragment?) {
super.onDonePressed(currentFragment)
setResult(Activity.RESULT_OK)
finish()
}
@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 {
AppTheme {
Surface(Modifier.fillMaxSize()) {
Box(Modifier.padding(bottom = dimensionResource(com.github.appintro.R.dimen.appintro2_bottombar_height))) {
page.ComposePage()
}
}
BackHandler {
if (pagerState.settledPage == 0) {
setResult(Activity.RESULT_CANCELED)
finish()
} else scope.launch {
pagerState.animateScrollToPage(pagerState.settledPage - 1)
}
}
}
IntroScreen(
pages = pages,
pagerState = pagerState,
onDonePressed = {
setResult(Activity.RESULT_OK)
finish()
}
)
}
}
}
@ -115,49 +71,4 @@ class IntroActivity : AppIntro2() {
}
}
@HiltViewModel
class Model @Inject constructor(
introPageFactory: IntroPageFactory
): ViewModel() {
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()}")
val activePages: Map<IntroPage, IntroPage.ShowPolicy> = introPages
.associateWith { page ->
page.getShowPolicy().also { policy ->
Logger.log.fine("IntroActivity: found intro page ${page::class.java} with $policy")
}
}
.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

@ -0,0 +1,45 @@
package at.bitfire.davdroid.ui.intro
import androidx.lifecycle.ViewModel
import at.bitfire.davdroid.log.Logger
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
@HiltViewModel
class IntroModel @Inject constructor(
introPageFactory: IntroPageFactory
): ViewModel() {
private val introPages = introPageFactory.introPages
val pages: List<IntroPage> by lazy {
calculatePages()
}
private fun calculatePages(): List<IntroPage> {
for (page in introPages)
Logger.log.fine("Found intro page ${page::class.java} with order ${page.getShowPolicy()}")
// Calculate which intro pages shall be shown
val activePages: Map<IntroPage, IntroPage.ShowPolicy> = introPages
.associateWith { page ->
page.getShowPolicy().also { policy ->
Logger.log.fine("IntroActivity: found intro page ${page::class.java} with $policy")
}
}
.filterValues { it != IntroPage.ShowPolicy.DONT_SHOW }
// Show intro screen when there's at least one page that shall [always] be shown
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

@ -0,0 +1,255 @@
package at.bitfire.davdroid.ui.intro
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.PagerState
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowForward
import androidx.compose.material.icons.filled.Check
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.contentColorFor
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.drawscope.translate
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import at.bitfire.davdroid.R
import at.bitfire.davdroid.ui.AppTheme
import kotlinx.coroutines.launch
@Composable
@OptIn(ExperimentalFoundationApi::class)
fun IntroScreen(
pages: List<IntroPage>,
pagerState: PagerState = rememberPagerState { pages.size },
onDonePressed: () -> Unit
) {
val scope = rememberCoroutineScope()
Column(modifier = Modifier.fillMaxSize()) {
HorizontalPager(
state = pagerState,
modifier = Modifier
.fillMaxWidth()
.weight(1f)
) { pages[it].ComposePage() }
Box(
modifier = Modifier
.fillMaxWidth()
.height(90.dp)
.background(MaterialTheme.colorScheme.primary)
) {
PositionIndicator(
index = pagerState.currentPage,
max = pages.size,
modifier = Modifier
.fillMaxHeight()
.padding(horizontal = 128.dp)
.align(Alignment.Center)
.fillMaxWidth(),
selectedIndicatorColor = MaterialTheme.colorScheme.onPrimary,
unselectedIndicatorColor = MaterialTheme.colorScheme.tertiary,
indicatorSize = 15f
)
ButtonWithIcon(
icon = if (pagerState.currentPage + 1 == pagerState.pageCount) {
Icons.Default.Check
} else {
Icons.AutoMirrored.Default.ArrowForward
},
contentDescription = stringResource(R.string.intro_next),
modifier = Modifier
.padding(end = 16.dp)
.align(Alignment.CenterEnd)
) {
if (pagerState.currentPage + 1 == pagerState.pageCount) {
onDonePressed()
} else scope.launch {
pagerState.animateScrollToPage(pagerState.currentPage + 1)
}
}
}
}
}
@Preview(
showSystemUi = true
)
@Composable
@OptIn(ExperimentalFoundationApi::class)
fun IntroScreen_Preview() {
AppTheme {
IntroScreen(
listOf(
object : IntroPage {
override fun getShowPolicy(): IntroPage.ShowPolicy =
IntroPage.ShowPolicy.SHOW_ALWAYS
@Composable
override fun ComposePage() {
Box(
modifier = Modifier
.fillMaxSize()
.background(MaterialTheme.colorScheme.surface)
)
}
},
object : IntroPage {
override fun getShowPolicy(): IntroPage.ShowPolicy =
IntroPage.ShowPolicy.SHOW_ALWAYS
@Composable
override fun ComposePage() {
Box(
modifier = Modifier
.fillMaxSize()
.background(MaterialTheme.colorScheme.primary)
)
}
}
),
onDonePressed = {}
)
}
}
@Composable
fun PositionIndicator(
index: Int,
max: Int,
modifier: Modifier = Modifier,
selectedIndicatorColor: Color = MaterialTheme.colorScheme.tertiary,
unselectedIndicatorColor: Color = contentColorFor(selectedIndicatorColor),
indicatorSize: Float = 20f,
indicatorPadding: Float = 20f
) {
val selectedPosition by animateFloatAsState(
targetValue = index.toFloat(),
label = "position"
)
Canvas(modifier = modifier) {
// idx * indicatorSize * 2 + idx * indicatorPadding + indicatorSize
// idx * (indicatorSize * 2 + indicatorPadding) + indicatorSize
val padding = indicatorSize * 2 + indicatorPadding
val totalWidth = indicatorSize * 2 * max + indicatorPadding * (max - 1)
translate(
left = size.width / 2 - totalWidth / 2
) {
for (idx in 0 until max) {
drawCircle(
color = unselectedIndicatorColor,
radius = indicatorSize,
center = Offset(
x = idx * padding + indicatorSize,
y = size.height / 2
)
)
}
drawCircle(
color = selectedIndicatorColor,
radius = indicatorSize,
center = Offset(
x = selectedPosition * padding + indicatorSize,
y = size.height / 2
)
)
}
}
}
@Preview(
showBackground = true,
backgroundColor = 0xff000000
)
@Composable
fun PositionIndicator_Preview() {
var index by remember { mutableIntStateOf(0) }
PositionIndicator(
index = index,
max = 5,
modifier = Modifier
.width(200.dp)
.height(50.dp)
.clickable { if (index == 4) index = 0 else index++ }
)
}
@Composable
fun ButtonWithIcon(
icon: ImageVector,
contentDescription: String?,
modifier: Modifier = Modifier,
size: Dp = 56.dp,
color: Color = MaterialTheme.colorScheme.tertiary,
onClick: () -> Unit
) {
Surface(
color = color,
contentColor = contentColorFor(backgroundColor = color),
modifier = modifier
.size(size)
.aspectRatio(1f),
onClick = onClick,
shape = CircleShape
) {
AnimatedContent(
targetState = icon,
label = "Button Icon"
) {
Icon(
imageVector = it,
contentDescription = contentDescription,
modifier = Modifier.padding(12.dp)
)
}
}
}
@Preview
@Composable
fun ButtonWithIcon_Preview() {
AppTheme {
ButtonWithIcon(
icon = Icons.AutoMirrored.Filled.ArrowForward,
contentDescription = null
) { }
}
}

View file

@ -22,7 +22,6 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
@ -84,11 +83,6 @@ class WelcomePage: IntroPage {
.fillMaxWidth()
.wrapContentHeight()
.padding(horizontal = 32.dp)
.padding(
bottom = dimensionResource(
com.github.appintro.R.dimen.appintro2_bottombar_height
)
)
)
Spacer(modifier = Modifier.weight(0.1f))
@ -128,12 +122,7 @@ class WelcomePage: IntroPage {
Row(
modifier = Modifier
.fillMaxSize()
.background(color = MaterialTheme.colorScheme.primary)
.padding(
bottom = dimensionResource(
com.github.appintro.R.dimen.appintro2_bottombar_height
)
),
.background(color = MaterialTheme.colorScheme.primary),
verticalAlignment = Alignment.CenterVertically
) {
Image(

View file

@ -60,6 +60,7 @@
<string name="intro_open_source_text">We\'re happy that you use %s, which is open-source software. Development, maintenance and support are hard work. Please consider contributing (there are many ways) or a donation. It would be highly appreciated!</string>
<string name="intro_open_source_details">How to contribute/donate</string>
<string name="intro_open_source_dont_show">Don\'t show in the near future</string>
<string name="intro_next">Next</string>
<!-- PermissionsActivity -->
<string name="permissions_title">Permissions</string>

View file

@ -1,20 +0,0 @@
<!--
~ Copyright (c) 2013 2016 Ricki Hirner (bitfire web engineering).
~ All rights reserved. This program and the accompanying materials
~ are made available under the terms of the GNU Public License v3.0
~ which accompanies this distribution, and is available at
~ http://www.gnu.org/licenses/gpl.html
-->
<resources>
<!-- app theme -->
<style name="AppTheme" parent="Theme.AppCompat.DayNight.NoActionBar">
<!-- These non-Compose theme variables are required for AppIntro as long it doesn't support Compose: -->
<item name="colorPrimary">@color/primaryColor</item>
<item name="colorPrimaryDark">@color/primaryDarkColor</item>
<item name="android:colorBackground">@android:color/white</item>
</style>
</resources>

View file

@ -13,15 +13,14 @@ androidx-lifecycle = "2.8.2"
androidx-paging = "3.3.0"
androidx-preference = "1.2.1"
androidx-security = "1.1.0-alpha06"
androidx-test-core = "1.5.0"
androidx-test-runner = "1.5.2"
androidx-test-rules = "1.5.0"
androidx-test-junit = "1.1.5"
androidx-test-core = "1.6.0"
androidx-test-runner = "1.6.0"
androidx-test-rules = "1.6.0"
androidx-test-junit = "1.2.0"
androidx-work = "2.9.0"
appIntro = "7.0.0-beta02"
bitfire-cert4android = "f1cc9b9ca3"
bitfire-dav4jvm = "b8be778202"
bitfire-ical4android = "ba5a013d69"
bitfire-ical4android = "83cda23ceb"
bitfire-vcard4android = "03a37a8284"
commons-collections = "4.4"
commons-lang = "3.14.0"
@ -73,7 +72,6 @@ androidx-test-rules = { module = "androidx.test:rules", version.ref = "androidx-
androidx-test-junit = { module = "androidx.test.ext:junit", version.ref = "androidx-test-junit" }
androidx-work-base = { module = "androidx.work:work-runtime-ktx", version.ref = "androidx-work" }
androidx-work-testing = { module = "androidx.work:work-testing", version.ref = "androidx-work" }
appintro = { module = "com.github.AppIntro:AppIntro", version.ref = "appIntro" }
bitfire-cert4android = { module = "com.github.bitfireAT:cert4android", version.ref = "bitfire-cert4android" }
bitfire-dav4jvm = { module = "com.github.bitfireAT:dav4jvm", version.ref = "bitfire-dav4jvm" }
bitfire-ical4android = { module = "com.github.bitfireAT:ical4android", version.ref = "bitfire-ical4android" }