Improve homepage URL launching (#632)

* Move homepage and other Web URLs to Constants; minor refactoring

* Use AppTheme with built-in safe LocalUriHandler instead of MdcTheme; minor refactoring

* Account settings: add TODO for Compose rewrite

* Use UriHandler instead of UiUtils.launch when possible
This commit is contained in:
Ricki Hirner 2024-03-10 20:51:40 +01:00 committed by GitHub
parent af5c732adc
commit 377c0344da
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
26 changed files with 233 additions and 205 deletions

View file

@ -5,11 +5,7 @@
package at.bitfire.davdroid
import android.app.Application
import android.content.Context
import android.net.Uri
import android.os.StrictMode
import androidx.appcompat.content.res.AppCompatResources
import androidx.core.graphics.drawable.toBitmap
import androidx.hilt.work.HiltWorkerFactory
import androidx.work.Configuration
import at.bitfire.davdroid.log.Logger
@ -27,37 +23,6 @@ import kotlin.system.exitProcess
@HiltAndroidApp
class App: Application(), Thread.UncaughtExceptionHandler, Configuration.Provider {
companion object {
const val HOMEPAGE_PRIVACY = "privacy"
fun getLauncherBitmap(context: Context) =
AppCompatResources.getDrawable(context, R.mipmap.ic_launcher)?.toBitmap()
/**
* Gets the DAVx5 Web site URL that should be used to open in the user's browser.
* Package ID, version number and calling context name will be appended as arguments.
*
* @param context context name to use
* @param page optional page segment to append (for instance: [HOMEPAGE_PRIVACY]])
*
* @return the Uri for the browser
*/
fun homepageUrl(context: Context, page: String? = null): Uri {
val builder = Uri.parse(context.getString(R.string.homepage_url)).buildUpon()
if (page != null)
builder.appendPath(page)
return builder
.appendQueryParameter("pk_campaign", BuildConfig.APPLICATION_ID)
.appendQueryParameter("pk_kwd", context::class.java.simpleName)
.appendQueryParameter("app-version", BuildConfig.VERSION_NAME)
.build()
}
}
@Inject lateinit var accountsUpdatedListener: AccountsUpdatedListener
@Inject lateinit var storageLowReceiver: StorageLowReceiver

View file

@ -3,25 +3,43 @@
**************************************************************************************************/
package at.bitfire.davdroid
import android.net.Uri
import androidx.core.net.toUri
/**
* Brand-specific constants like (non-theme) colors, homepage URLs etc.
*/
object Constants {
const val DAVDROID_GREEN_RGBA = 0xFF8bc34a.toInt()
// gplay billing
const val BILLINGCLIENT_CONNECTION_MAX_RETRIES = 4
val HOMEPAGE_URL = "https://www.davx5.com".toUri()
const val HOMEPAGE_PATH_FAQ = "faq"
const val HOMEPAGE_PATH_FAQ_SYNC_NOT_RUN = "synchronization-is-not-run-as-expected"
const val HOMEPAGE_PATH_OPEN_SOURCE = "donate"
const val HOMEPAGE_PATH_PRIVACY = "privacy"
const val HOMEPAGE_PATH_TESTED_SERVICES = "tested-with"
val MANUAL_URL = "https://manual.davx5.com".toUri()
const val MANUAL_PATH_WEBDAV_MOUNTS = "webdav_mounts.html"
val COMMUNITY_URL = "https://github.com/bitfireAT/davx5-ose/discussions".toUri()
val FEDIVERSE_URL = "https://fosstodon.org/@davx5app".toUri()
/**
* Context label for [org.apache.commons.lang3.exception.ContextedException].
* Context value is the [at.bitfire.davdroid.resource.LocalResource]
* which is related to the exception cause.
* Appends query parameters for anonymized usage statistics (app ID, version).
* Can be used by the called Website to get an idea of which versions etc. are currently used.
*
* @param context optional info about from where the URL was opened (like a specific Activity)
*/
const val EXCEPTION_CONTEXT_LOCAL_RESOURCE = "localResource"
fun Uri.Builder.withStatParams(context: String? = null): Uri.Builder {
appendQueryParameter("pk_campaign", BuildConfig.APPLICATION_ID)
appendQueryParameter("app-version", BuildConfig.VERSION_NAME)
/**
* Context label for [org.apache.commons.lang3.exception.ContextedException].
* Context value is the [okhttp3.HttpUrl] of the remote resource
* which is related to the exception cause.
*/
const val EXCEPTION_CONTEXT_REMOTE_RESOURCE = "remoteResource"
if (context != null)
appendQueryParameter("pk_kwd", context)
}
return this
}
}

View file

@ -34,7 +34,6 @@ import at.bitfire.dav4jvm.property.caldav.GetCTag
import at.bitfire.dav4jvm.property.caldav.ScheduleTag
import at.bitfire.dav4jvm.property.webdav.GetETag
import at.bitfire.dav4jvm.property.webdav.SyncToken
import at.bitfire.davdroid.Constants
import at.bitfire.davdroid.InvalidAccountException
import at.bitfire.davdroid.R
import at.bitfire.davdroid.db.AppDatabase
@ -67,7 +66,6 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import okhttp3.HttpUrl
import okhttp3.RequestBody
import org.apache.commons.io.FileUtils
import org.apache.commons.lang3.exception.ContextedException
import org.dmfs.tasks.contract.TaskContract
import java.io.IOException
@ -105,14 +103,28 @@ abstract class SyncManager<ResourceType: LocalResource<*>, out CollectionType: L
}
companion object {
const val DEBUG_INFO_MAX_RESOURCE_DUMP_SIZE = 100*FileUtils.ONE_KB.toInt()
/** Maximum number of resources that are requested with one multiget request. */
const val MAX_MULTIGET_RESOURCES = 10
const val DELAY_UNTIL_DEFAULT = 15*60L // 15 min
const val DELAY_UNTIL_MIN = 1*60L // 1 min
const val DELAY_UNTIL_MAX = 2*60*60L // 2 hours
/**
* Context label for [org.apache.commons.lang3.exception.ContextedException].
* Context value is the [at.bitfire.davdroid.resource.LocalResource]
* which is related to the exception cause.
*/
const val EXCEPTION_CONTEXT_LOCAL_RESOURCE = "localResource"
/**
* Context label for [org.apache.commons.lang3.exception.ContextedException].
* Context value is the [okhttp3.HttpUrl] of the remote resource
* which is related to the exception cause.
*/
const val EXCEPTION_CONTEXT_REMOTE_RESOURCE = "remoteResource"
/**
* Returns appropriate sync retry delay in seconds, considering the servers suggestion
* ([DELAY_UNTIL_DEFAULT] if no server suggestion).
@ -865,11 +877,11 @@ abstract class SyncManager<ResourceType: LocalResource<*>, out CollectionType: L
try {
return body(local)
} catch (e: ContextedException) {
e.addContextValue(Constants.EXCEPTION_CONTEXT_LOCAL_RESOURCE, local)
e.addContextValue(EXCEPTION_CONTEXT_LOCAL_RESOURCE, local)
throw e
} catch (e: Throwable) {
if (local != null)
throw ContextedException(e).setContextValue(Constants.EXCEPTION_CONTEXT_LOCAL_RESOURCE, local)
throw ContextedException(e).setContextValue(EXCEPTION_CONTEXT_LOCAL_RESOURCE, local)
else
throw e
}
@ -879,10 +891,10 @@ abstract class SyncManager<ResourceType: LocalResource<*>, out CollectionType: L
try {
return body(remote)
} catch (e: ContextedException) {
e.addContextValue(Constants.EXCEPTION_CONTEXT_REMOTE_RESOURCE, remote.location)
e.addContextValue(EXCEPTION_CONTEXT_REMOTE_RESOURCE, remote.location)
throw e
} catch(e: Throwable) {
throw ContextedException(e).setContextValue(Constants.EXCEPTION_CONTEXT_REMOTE_RESOURCE, remote.location)
throw ContextedException(e).setContextValue(EXCEPTION_CONTEXT_REMOTE_RESOURCE, remote.location)
}
}
@ -890,10 +902,10 @@ abstract class SyncManager<ResourceType: LocalResource<*>, out CollectionType: L
try {
return body(remote)
} catch (e: ContextedException) {
e.addContextValue(Constants.EXCEPTION_CONTEXT_REMOTE_RESOURCE, remote.href)
e.addContextValue(EXCEPTION_CONTEXT_REMOTE_RESOURCE, remote.href)
throw e
} catch (e: Throwable) {
throw ContextedException(e).setContextValue(Constants.EXCEPTION_CONTEXT_REMOTE_RESOURCE, remote.href)
throw ContextedException(e).setContextValue(EXCEPTION_CONTEXT_REMOTE_RESOURCE, remote.href)
}
}
@ -914,13 +926,11 @@ abstract class SyncManager<ResourceType: LocalResource<*>, out CollectionType: L
if (ex is ContextedException) {
@Suppress("UNCHECKED_CAST")
// we want the innermost context value, which is the first one
(ex.getFirstContextValue(Constants.EXCEPTION_CONTEXT_LOCAL_RESOURCE) as? ResourceType)?.let {
if (local == null)
local = it
(ex.getFirstContextValue(EXCEPTION_CONTEXT_LOCAL_RESOURCE) as? ResourceType)?.let {
local = it
}
(ex.getFirstContextValue(Constants.EXCEPTION_CONTEXT_REMOTE_RESOURCE) as? HttpUrl)?.let {
if (remote == null)
remote = it
(ex.getFirstContextValue(EXCEPTION_CONTEXT_REMOTE_RESOURCE) as? HttpUrl)?.let {
remote = it
}
ex = ex.cause
}

View file

@ -6,7 +6,6 @@ package at.bitfire.davdroid.ui
import android.app.Application
import android.os.Bundle
import android.view.*
import androidx.activity.compose.setContent
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
@ -41,6 +40,7 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
@ -48,12 +48,12 @@ import androidx.compose.ui.unit.dp
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope
import at.bitfire.davdroid.App
import at.bitfire.davdroid.BuildConfig
import at.bitfire.davdroid.Constants
import at.bitfire.davdroid.Constants.withStatParams
import at.bitfire.davdroid.R
import at.bitfire.davdroid.log.Logger
import at.bitfire.davdroid.ui.widget.PixelBoxes
import com.google.accompanist.themeadapter.material.MdcTheme
import com.mikepenz.aboutlibraries.Libs
import com.mikepenz.aboutlibraries.ui.compose.LibrariesContainer
import com.mikepenz.aboutlibraries.util.withJson
@ -71,7 +71,9 @@ import java.time.LocalDateTime
import java.time.ZoneOffset
import java.time.format.DateTimeFormatter
import java.time.format.FormatStyle
import java.util.*
import java.util.LinkedList
import java.util.Locale
import java.util.Optional
import java.util.logging.Level
import javax.inject.Inject
import kotlin.jvm.optionals.getOrNull
@ -90,7 +92,9 @@ class AboutActivity: AppCompatActivity() {
super.onCreate(savedInstanceState)
setContent {
MdcTheme {
val uriHandler = LocalUriHandler.current
AppTheme {
Scaffold(
topBar = {
TopAppBar(
@ -107,8 +111,10 @@ class AboutActivity: AppCompatActivity() {
},
actions = {
IconButton(onClick = {
val context = this@AboutActivity
UiUtils.launchUri(context, App.homepageUrl(context))
uriHandler.openUri(Constants.HOMEPAGE_URL
.buildUpon()
.withStatParams("AboutActivity")
.build().toString())
}) {
Icon(
Icons.Default.Home,

View file

@ -143,7 +143,7 @@ class AccountsActivity: AppCompatActivity() {
val accounts by model.accountInfos.observeAsState()
MdcTheme {
AppTheme {
Scaffold(
scaffoldState = scaffoldState,
drawerContent = drawerContent(scope, scaffoldState),

View file

@ -38,7 +38,6 @@ import androidx.compose.material.icons.filled.InvertColors
import androidx.compose.material.icons.filled.Notifications
import androidx.compose.material.icons.filled.SyncProblem
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableStateOf
@ -71,13 +70,11 @@ import at.bitfire.davdroid.ui.intro.BatteryOptimizationsPage
import at.bitfire.davdroid.ui.intro.OpenSourcePage
import at.bitfire.davdroid.ui.widget.EditTextInputDialog
import at.bitfire.davdroid.ui.widget.MultipleChoiceInputDialog
import at.bitfire.davdroid.ui.widget.SafeAndroidUriHandler
import at.bitfire.davdroid.ui.widget.Setting
import at.bitfire.davdroid.ui.widget.SettingsHeader
import at.bitfire.davdroid.ui.widget.SwitchSetting
import at.bitfire.davdroid.util.PermissionUtils
import at.bitfire.ical4android.TaskProvider
import com.google.accompanist.themeadapter.material.MdcTheme
import dagger.hilt.android.AndroidEntryPoint
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.launch
@ -94,10 +91,8 @@ class AppSettingsActivity: AppCompatActivity() {
super.onCreate(savedInstanceState)
setContent {
MdcTheme {
CompositionLocalProvider(LocalUriHandler provides SafeAndroidUriHandler(this)) {
AppSettings()
}
AppTheme {
AppSettings()
}
}
}

View file

@ -181,7 +181,7 @@ class DebugInfoActivity : AppCompatActivity() {
}
setContent {
MdcTheme {
AppTheme {
val debugInfo by model.debugInfo.observeAsState()
val zipProgress by model.zipProgress.observeAsState(false)

View file

@ -57,7 +57,7 @@ class ExceptionInfoFragment: DialogFragment() {
setContentView(
ComposeView(requireContext()).apply {
setContent {
MdcTheme {
AppTheme {
ExceptionInfoDialog(
exception = exception,
account = account

View file

@ -6,9 +6,12 @@ package at.bitfire.davdroid.ui
import android.app.Activity
import android.content.Intent
import android.net.Uri
import android.view.MenuItem
import at.bitfire.davdroid.App
import at.bitfire.davdroid.Constants
import at.bitfire.davdroid.Constants.COMMUNITY_URL
import at.bitfire.davdroid.Constants.FEDIVERSE_URL
import at.bitfire.davdroid.Constants.MANUAL_URL
import at.bitfire.davdroid.Constants.withStatParams
import at.bitfire.davdroid.R
import at.bitfire.davdroid.ui.webdav.WebdavMountsActivity
import javax.inject.Inject
@ -18,18 +21,16 @@ import javax.inject.Inject
*/
class OseAccountsDrawerHandler @Inject constructor(): BaseAccountsDrawerHandler() {
companion object {
const val COMMUNITY_URL = "https://github.com/bitfireAT/davx5-ose/discussions"
const val MANUAL_URL = "https://manual.davx5.com"
}
override fun onNavigationItemSelected(activity: Activity, item: MenuItem) {
val homepageUrl = Constants.HOMEPAGE_URL.buildUpon()
.withStatParams("OseAccountsDrawerHandler")
when (item.itemId) {
R.id.nav_mastodon ->
UiUtils.launchUri(
activity,
Uri.parse("https://fosstodon.org/@davx5app")
FEDIVERSE_URL
)
R.id.nav_webdav_mounts ->
@ -38,29 +39,29 @@ class OseAccountsDrawerHandler @Inject constructor(): BaseAccountsDrawerHandler(
R.id.nav_website ->
UiUtils.launchUri(
activity,
App.homepageUrl(activity)
homepageUrl.build()
)
R.id.nav_manual ->
UiUtils.launchUri(
activity,
Uri.parse(MANUAL_URL)
MANUAL_URL
)
R.id.nav_faq ->
UiUtils.launchUri(
activity,
App.homepageUrl(activity, "faq")
homepageUrl.appendPath(Constants.HOMEPAGE_PATH_FAQ).build()
)
R.id.nav_community ->
UiUtils.launchUri(activity, Uri.parse(COMMUNITY_URL))
UiUtils.launchUri(activity, COMMUNITY_URL)
R.id.nav_donate ->
UiUtils.launchUri(
activity,
App.homepageUrl(activity, "donate")
homepageUrl.appendPath(Constants.HOMEPAGE_PATH_OPEN_SOURCE).build()
)
R.id.nav_privacy ->
UiUtils.launchUri(
activity,
App.homepageUrl(activity, App.HOMEPAGE_PRIVACY)
homepageUrl.appendPath(Constants.HOMEPAGE_PATH_PRIVACY).build()
)
else ->

View file

@ -61,7 +61,7 @@ class PermissionsActivity: AppCompatActivity() {
super.onCreate(savedInstanceState)
setContent {
MdcTheme {
AppTheme {
PermissionsContent(model)
}
}

View file

@ -39,6 +39,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.BiasAlignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.ExperimentalTextApi
@ -57,7 +58,6 @@ import at.bitfire.davdroid.ui.UiUtils.toAnnotatedString
import at.bitfire.davdroid.ui.widget.CardWithImage
import at.bitfire.davdroid.ui.widget.RadioWithSwitch
import at.bitfire.ical4android.TaskProvider
import com.google.accompanist.themeadapter.material.MdcTheme
import dagger.hilt.android.AndroidEntryPoint
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.launch
@ -71,7 +71,7 @@ class TasksActivity: AppCompatActivity() {
super.onCreate(savedInstanceState)
setContent {
MdcTheme {
AppTheme {
TasksCard(model = model)
}
}
@ -268,12 +268,13 @@ fun TasksCard(
HtmlCompat.FROM_HTML_MODE_COMPACT
).toAnnotatedString()
val uriHandler = LocalUriHandler.current
ClickableText(
text = summary,
onClick = { index ->
// Get the tapped position, and check if there's any link
summary.getUrlAnnotations(index, index).firstOrNull()?.item?.url?.let { url ->
UiUtils.launchUri(context, Uri.parse(url))
uriHandler.openUri(url)
}
}
)

View file

@ -0,0 +1,18 @@
package at.bitfire.davdroid.ui
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalUriHandler
import at.bitfire.davdroid.ui.widget.SafeAndroidUriHandler
import com.google.accompanist.themeadapter.material.MdcTheme
@Suppress("DEPRECATION")
@Composable
fun AppTheme(content: @Composable () -> Unit) {
CompositionLocalProvider(LocalUriHandler provides SafeAndroidUriHandler(LocalContext.current)) {
MdcTheme {
content()
}
}
}

View file

@ -86,7 +86,7 @@ object UiUtils {
* @return true on success, false if the Intent could not be resolved (for instance, because
* there is no user agent installed)
*/
@Deprecated("Use SafeAndroidUriHandler (Compose) instead")
@Deprecated("Use LocalUriHandler.open() instead (Compose)")
fun launchUri(context: Context, uri: Uri, action: String = Intent.ACTION_VIEW, toastInstallBrowser: Boolean = true): Boolean {
val intent = Intent(action, uri)
try {

View file

@ -80,11 +80,11 @@ import at.bitfire.davdroid.resource.TaskUtils
import at.bitfire.davdroid.servicedetection.RefreshCollectionsWorker
import at.bitfire.davdroid.settings.AccountSettings
import at.bitfire.davdroid.syncadapter.SyncWorker
import at.bitfire.davdroid.ui.AppTheme
import at.bitfire.davdroid.ui.PermissionsActivity
import at.bitfire.davdroid.ui.widget.ActionCard
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.rememberMultiplePermissionsState
import com.google.accompanist.themeadapter.material.MdcTheme
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch
import javax.inject.Inject
@ -125,7 +125,7 @@ class AccountActivity2 : AppCompatActivity() {
}
setContent {
MdcTheme {
AppTheme {
val cardDavSvc by model.cardDavSvc.observeAsState()
val canCreateAddressBook by model.canCreateAddressBook.observeAsState(false)
val cardDavRefreshing by model.cardDavRefreshingActive.observeAsState(false)

View file

@ -46,7 +46,7 @@ import at.bitfire.davdroid.db.AppDatabase
import at.bitfire.davdroid.db.Collection
import at.bitfire.davdroid.db.HomeSet
import at.bitfire.davdroid.db.Service
import com.google.accompanist.themeadapter.material.MdcTheme
import at.bitfire.davdroid.ui.AppTheme
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
@ -82,7 +82,7 @@ class CreateAddressBookActivity: AppCompatActivity() {
supportActionBar?.setDisplayHomeAsUpEnabled(true)
setContent {
MdcTheme {
AppTheme {
val displayName by model.displayName.observeAsState()
val description by model.description.observeAsState()
val homeSet by model.homeSet.observeAsState()

View file

@ -24,7 +24,12 @@ import androidx.lifecycle.MediatorLiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.preference.*
import androidx.preference.EditTextPreference
import androidx.preference.ListPreference
import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat
import androidx.preference.PreferenceGroup
import androidx.preference.SwitchPreferenceCompat
import at.bitfire.davdroid.InvalidAccountException
import at.bitfire.davdroid.R
import at.bitfire.davdroid.db.Credentials
@ -66,6 +71,7 @@ class SettingsActivity: AppCompatActivity() {
title = getString(R.string.settings_title, account.name)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
// TODO add help button that leads to manual
if (savedInstanceState == null)
supportFragmentManager.beginTransaction()

View file

@ -33,7 +33,6 @@ import androidx.compose.material.OutlinedButton
import androidx.compose.material.Switch
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
@ -48,15 +47,15 @@ import androidx.core.content.getSystemService
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.Constants
import at.bitfire.davdroid.Constants.withStatParams
import at.bitfire.davdroid.R
import at.bitfire.davdroid.settings.SettingsManager
import at.bitfire.davdroid.ui.AppTheme
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 at.bitfire.davdroid.util.PermissionUtils
import com.google.accompanist.themeadapter.material.MdcTheme
import dagger.hilt.EntryPoint
import dagger.hilt.InstallIn
import dagger.hilt.android.EntryPointAccessors
@ -112,23 +111,20 @@ class BatteryOptimizationsPage: IntroPage {
}
val hintAutostartPermission by model.hintAutostartPermission.observeAsState()
val uriHandler = SafeAndroidUriHandler(LocalContext.current)
CompositionLocalProvider(LocalUriHandler provides uriHandler) {
BatteryOptimizationsContent(
dontShowBattery = hintBatteryOptimizations == false,
onChangeDontShowBattery = {
model.settings.putBoolean(HINT_BATTERY_OPTIMIZATIONS, !it)
},
isExempted = isExempted,
shouldBeExempted = shouldBeExempted,
onChangeShouldBeExempted = model.shouldBeExempted::postValue,
dontShowAutostart = hintAutostartPermission == false,
onChangeDontShowAutostart = {
model.settings.putBoolean(HINT_AUTOSTART_PERMISSION, !it)
},
manufacturerWarning = Model.manufacturerWarning
)
}
BatteryOptimizationsContent(
dontShowBattery = hintBatteryOptimizations == false,
onChangeDontShowBattery = {
model.settings.putBoolean(HINT_BATTERY_OPTIMIZATIONS, !it)
},
isExempted = isExempted,
shouldBeExempted = shouldBeExempted,
onChangeShouldBeExempted = model.shouldBeExempted::postValue,
dontShowAutostart = hintAutostartPermission == false,
onChangeDontShowAutostart = {
model.settings.putBoolean(HINT_AUTOSTART_PERMISSION, !it)
},
manufacturerWarning = Model.manufacturerWarning
)
}
@ -234,7 +230,7 @@ class BatteryOptimizationsPage: IntroPage {
@Preview(showBackground = true, showSystemUi = true)
@Composable
private fun BatteryOptimizationsContent_Preview() {
MdcTheme {
AppTheme {
BatteryOptimizationsContent(
dontShowBattery = true,
onChangeDontShowBattery = {},
@ -346,14 +342,14 @@ private fun BatteryOptimizationsContent(
OutlinedButton(
onClick = {
uriHandler.openUri(
App.homepageUrl(context)
.buildUpon()
.appendPath("faq")
.appendPath("synchronization-is-not-run-as-expected")
Constants.HOMEPAGE_URL.buildUpon()
.appendPath(Constants.HOMEPAGE_PATH_FAQ)
.appendPath(Constants.HOMEPAGE_PATH_FAQ_SYNC_NOT_RUN)
.appendQueryParameter(
"manufacturer",
Build.MANUFACTURER.lowercase(Locale.ROOT)
)
.withStatParams("BatteryOptimizationsPage")
.build().toString()
)
}

View file

@ -27,8 +27,8 @@ import androidx.fragment.app.activityViewModels
import androidx.lifecycle.AndroidViewModel
import at.bitfire.davdroid.R
import at.bitfire.davdroid.log.Logger
import at.bitfire.davdroid.ui.AppTheme
import com.github.appintro.AppIntro2
import com.google.accompanist.themeadapter.material.MdcTheme
import dagger.hilt.EntryPoint
import dagger.hilt.InstallIn
import dagger.hilt.android.AndroidEntryPoint
@ -112,7 +112,7 @@ class IntroActivity : AppIntro2() {
ComposeView(requireActivity()).apply {
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
setContent {
MdcTheme {
AppTheme {
Box(Modifier.padding(bottom = dimensionResource(com.github.appintro.R.dimen.appintro2_bottombar_height))) {
page.ComposePage()
}

View file

@ -19,7 +19,6 @@ import androidx.compose.material.MaterialTheme
import androidx.compose.material.OutlinedButton
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
@ -27,7 +26,6 @@ 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.LocalContext
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
@ -36,11 +34,11 @@ import androidx.compose.ui.unit.dp
import androidx.databinding.ObservableBoolean
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewmodel.compose.viewModel
import at.bitfire.davdroid.App
import at.bitfire.davdroid.Constants
import at.bitfire.davdroid.Constants.withStatParams
import at.bitfire.davdroid.R
import at.bitfire.davdroid.settings.SettingsManager
import at.bitfire.davdroid.ui.widget.CardWithImage
import at.bitfire.davdroid.ui.widget.SafeAndroidUriHandler
import dagger.hilt.EntryPoint
import dagger.hilt.InstallIn
import dagger.hilt.android.EntryPointAccessors
@ -74,16 +72,13 @@ class OpenSourcePage : IntroPage {
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
}
)
}
PageContent(
dontShow = dontShow,
onChangeDontShow = {
model.dontShow.set(it)
dontShow = it
}
)
}
@Preview(
@ -95,7 +90,6 @@ class OpenSourcePage : IntroPage {
dontShow: Boolean = false,
onChangeDontShow: (Boolean) -> Unit = {}
) {
val context = LocalContext.current
val uriHandler = LocalUriHandler.current
Column(
@ -116,9 +110,9 @@ class OpenSourcePage : IntroPage {
OutlinedButton(
onClick = {
uriHandler.openUri(
App.homepageUrl(context)
.buildUpon()
.appendPath("donate")
Constants.HOMEPAGE_URL.buildUpon()
.appendPath(Constants.HOMEPAGE_PATH_OPEN_SOURCE)
.withStatParams("OpenSourcePage")
.build()
.toString()
)

View file

@ -32,8 +32,8 @@ import at.bitfire.davdroid.R
import at.bitfire.davdroid.db.Credentials
import at.bitfire.davdroid.log.Logger
import at.bitfire.davdroid.servicedetection.DavResourceFinder
import at.bitfire.davdroid.ui.AppTheme
import at.bitfire.davdroid.ui.DebugInfoActivity
import com.google.accompanist.themeadapter.material.MdcTheme
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import kotlinx.coroutines.CoroutineName
import kotlinx.coroutines.Dispatchers
@ -80,7 +80,7 @@ class DetectConfigurationFragment: Fragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?) =
ComposeView(requireContext()).apply {
setContent {
MdcTheme {
AppTheme {
DetectConfigurationView()
}

View file

@ -33,17 +33,15 @@ import androidx.compose.material.Card
import androidx.compose.material.MaterialTheme
import androidx.compose.material.OutlinedTextField
import androidx.compose.material.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Warning
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.ExperimentalTextApi
@ -52,7 +50,6 @@ import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.intl.Locale
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.core.net.toUri
import androidx.core.text.HtmlCompat
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
@ -60,14 +57,15 @@ import androidx.fragment.app.viewModels
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope
import at.bitfire.davdroid.App
import at.bitfire.davdroid.BuildConfig
import at.bitfire.davdroid.Constants
import at.bitfire.davdroid.Constants.withStatParams
import at.bitfire.davdroid.R
import at.bitfire.davdroid.db.Credentials
import at.bitfire.davdroid.log.Logger
import at.bitfire.davdroid.ui.UiUtils
import at.bitfire.davdroid.ui.AppTheme
import at.bitfire.davdroid.ui.UiUtils.toAnnotatedString
import com.google.accompanist.themeadapter.material.MdcTheme
import at.bitfire.davdroid.ui.setup.GoogleLoginFragment.Companion.URI_TESTED_WITH_GOOGLE
import com.google.android.material.snackbar.Snackbar
import dagger.hilt.android.AndroidEntryPoint
import dagger.hilt.android.lifecycle.HiltViewModel
@ -95,7 +93,11 @@ class GoogleLoginFragment(private val defaultEmail: String? = null): Fragment()
const val GOOGLE_POLICY_URL = "https://developers.google.com/terms/api-services-user-data-policy#additional_requirements_for_specific_api_scopes"
// Support site
val URI_TESTED_WITH_GOOGLE: Uri = Uri.parse("https://www.davx5.com/tested-with/google")
val URI_TESTED_WITH_GOOGLE: Uri =
Constants.HOMEPAGE_URL.buildUpon()
.appendPath(Constants.HOMEPAGE_PATH_TESTED_SERVICES)
.appendPath("google")
.build()
// davx5integration@gmail.com (for davx5-ose)
private const val CLIENT_ID = "1069050168830-eg09u4tk1cmboobevhm4k3bj1m4fav9i.apps.googleusercontent.com"
@ -228,7 +230,9 @@ fun GoogleLogin(
onLogin: (accountEmail: String, clientId: String?) -> Unit
) {
val context = LocalContext.current
MdcTheme {
val uriHandler = LocalUriHandler.current
AppTheme {
Column(
Modifier
.padding(8.dp)
@ -241,14 +245,19 @@ fun GoogleLogin(
Card(Modifier.fillMaxWidth()) {
Column(Modifier.padding(8.dp)) {
Row {
Image(Icons.Default.Warning, colorFilter = ColorFilter.tint(MaterialTheme.colors.onSurface), contentDescription = "",
modifier = Modifier.padding(top = 8.dp, end = 8.dp, bottom = 8.dp))
Text(stringResource(R.string.login_google_see_tested_with))
Text(
stringResource(R.string.login_google_see_tested_with),
style = MaterialTheme.typography.body2,
)
}
Text(stringResource(R.string.login_google_unexpected_warnings), modifier = Modifier.padding(vertical = 8.dp))
Text(
stringResource(R.string.login_google_unexpected_warnings),
style = MaterialTheme.typography.body2,
modifier = Modifier.padding(vertical = 8.dp)
)
Button(
onClick = {
UiUtils.launchUri(context, GoogleLoginFragment.URI_TESTED_WITH_GOOGLE)
uriHandler.openUri(URI_TESTED_WITH_GOOGLE.toString())
},
colors = ButtonDefaults.outlinedButtonColors(),
modifier = Modifier.wrapContentSize()
@ -339,14 +348,21 @@ fun GoogleLogin(
Spacer(Modifier.padding(8.dp))
val privacyPolicyUrl = Constants.HOMEPAGE_URL.buildUpon()
.appendPath(Constants.HOMEPAGE_PATH_PRIVACY)
.withStatParams("GoogleLoginFragment")
.build()
val privacyPolicyNote = HtmlCompat.fromHtml(
stringResource(R.string.login_google_client_privacy_policy, context.getString(R.string.app_name), App.homepageUrl(context, App.HOMEPAGE_PRIVACY)), 0).toAnnotatedString()
stringResource(R.string.login_google_client_privacy_policy,
context.getString(R.string.app_name),
privacyPolicyUrl.toString()
), 0).toAnnotatedString()
ClickableText(
privacyPolicyNote,
style = MaterialTheme.typography.body2,
onClick = { position ->
privacyPolicyNote.getUrlAnnotations(position, position).firstOrNull()?.let {
UiUtils.launchUri(context, it.item.url.toUri())
uriHandler.openUri(it.item.url)
}
}
)
@ -359,7 +375,7 @@ fun GoogleLogin(
modifier = Modifier.padding(top = 12.dp),
onClick = { position ->
limitedUseNote.getUrlAnnotations(position, position).firstOrNull()?.let {
UiUtils.launchUri(context, it.item.url.toUri())
uriHandler.openUri(it.item.url)
}
}
)

View file

@ -11,7 +11,8 @@ import android.view.MenuItem
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.MenuProvider
import androidx.fragment.app.Fragment
import at.bitfire.davdroid.App
import at.bitfire.davdroid.Constants
import at.bitfire.davdroid.Constants.withStatParams
import at.bitfire.davdroid.R
import at.bitfire.davdroid.log.Logger
import at.bitfire.davdroid.ui.UiUtils
@ -61,7 +62,10 @@ class LoginActivity: AppCompatActivity() {
override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
if (menuItem.itemId == R.id.help) {
UiUtils.launchUri(this@LoginActivity,
App.homepageUrl(this@LoginActivity).buildUpon().appendPath("tested-with").build())
Constants.HOMEPAGE_URL.buildUpon()
.appendPath(Constants.HOMEPAGE_PATH_TESTED_SERVICES)
.withStatParams("LoginActivity")
.build())
return true
}

View file

@ -55,9 +55,9 @@ import at.bitfire.davdroid.R
import at.bitfire.davdroid.db.Credentials
import at.bitfire.davdroid.log.Logger
import at.bitfire.davdroid.network.HttpClient
import at.bitfire.davdroid.ui.AppTheme
import at.bitfire.davdroid.ui.UiUtils.haveCustomTabs
import at.bitfire.vcard4android.GroupMethod
import com.google.accompanist.themeadapter.material.MdcTheme
import com.google.android.material.snackbar.Snackbar
import dagger.Binds
import dagger.Module
@ -124,7 +124,7 @@ class NextcloudLoginFlowFragment: Fragment() {
val view = ComposeView(requireActivity()).apply {
setContent {
MdcTheme {
AppTheme {
NextcloudLoginComposable(
onStart = { url ->
model.start(url)

View file

@ -34,6 +34,7 @@ import androidx.compose.material.Text
import androidx.compose.material.TextButton
import androidx.compose.material.TopAppBar
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.automirrored.filled.Help
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
@ -51,17 +52,18 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.lifecycleScope
import at.bitfire.dav4jvm.DavResource
import at.bitfire.dav4jvm.UrlUtils
import at.bitfire.davdroid.App
import at.bitfire.davdroid.Constants
import at.bitfire.davdroid.Constants.withStatParams
import at.bitfire.davdroid.R
import at.bitfire.davdroid.db.AppDatabase
import at.bitfire.davdroid.db.Credentials
import at.bitfire.davdroid.db.WebDavMount
import at.bitfire.davdroid.log.Logger
import at.bitfire.davdroid.network.HttpClient
import at.bitfire.davdroid.ui.AppTheme
import at.bitfire.davdroid.ui.widget.PasswordTextField
import at.bitfire.davdroid.webdav.CredentialsStore
import at.bitfire.davdroid.webdav.DavDocumentsProvider
import com.google.accompanist.themeadapter.material.MdcTheme
import dagger.hilt.android.AndroidEntryPoint
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
@ -92,7 +94,7 @@ class AddWebdavMountActivity : AppCompatActivity() {
val username by model.userName.observeAsState(initial = "")
val password by model.password.observeAsState(initial = "")
MdcTheme {
AppTheme {
Layout(
isLoading = isLoading,
error = error,
@ -146,16 +148,20 @@ class AddWebdavMountActivity : AppCompatActivity() {
Scaffold(
topBar = {
TopAppBar(
navigationIcon = {
IconButton(onClick = { onNavigateUp() }) {
Icon(Icons.AutoMirrored.Default.ArrowBack, stringResource(R.string.navigate_up))
}
},
title = { Text(stringResource(R.string.webdav_add_mount_title)) },
actions = {
IconButton(
onClick = {
uriHandler.openUri(
App.homepageUrl(context)
.buildUpon()
.appendPath("tested-with")
.build()
.toString()
Constants.HOMEPAGE_URL.buildUpon()
.appendPath(Constants.HOMEPAGE_PATH_TESTED_SERVICES)
.withStatParams("AddWebdavMountActivity")
.build().toString()
)
}
) {
@ -299,7 +305,7 @@ class AddWebdavMountActivity : AppCompatActivity() {
@Preview
@Composable
fun Layout_Preview() {
MdcTheme {
AppTheme {
Layout()
}
}

View file

@ -7,6 +7,7 @@ package at.bitfire.davdroid.ui.webdav
import android.app.Application
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.provider.DocumentsContract
@ -41,7 +42,6 @@ import androidx.compose.material.icons.automirrored.filled.Help
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Delete
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableStateOf
@ -60,18 +60,17 @@ import androidx.core.text.HtmlCompat
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.MediatorLiveData
import androidx.lifecycle.viewModelScope
import at.bitfire.davdroid.App
import at.bitfire.davdroid.Constants
import at.bitfire.davdroid.R
import at.bitfire.davdroid.db.AppDatabase
import at.bitfire.davdroid.db.WebDavDocument
import at.bitfire.davdroid.db.WebDavMount
import at.bitfire.davdroid.log.Logger
import at.bitfire.davdroid.ui.AppTheme
import at.bitfire.davdroid.ui.UiUtils.toAnnotatedString
import at.bitfire.davdroid.ui.widget.SafeAndroidUriHandler
import at.bitfire.davdroid.util.DavUtils
import at.bitfire.davdroid.webdav.CredentialsStore
import at.bitfire.davdroid.webdav.DavDocumentsProvider
import com.google.accompanist.themeadapter.material.MdcTheme
import dagger.hilt.android.AndroidEntryPoint
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
@ -85,12 +84,9 @@ import javax.inject.Inject
class WebdavMountsActivity: AppCompatActivity() {
companion object {
fun helpUrl(context: Context) =
App.homepageUrl(context).buildUpon()
.appendEncodedPath("manual/webdav_mounts.html")
.build()
val helpUrl: Uri = Constants.MANUAL_URL.buildUpon()
.appendPath(Constants.MANUAL_PATH_WEBDAV_MOUNTS)
.build()
}
private val model by viewModels<Model>()
@ -108,14 +104,9 @@ class WebdavMountsActivity: AppCompatActivity() {
super.onCreate(savedInstanceState)
setContent {
MdcTheme {
CompositionLocalProvider(
LocalUriHandler provides SafeAndroidUriHandler(this)
) {
val mountInfos by model.mountInfos.observeAsState(emptyList())
WebdavMountsContent(mountInfos)
}
AppTheme {
val mountInfos by model.mountInfos.observeAsState(emptyList())
WebdavMountsContent(mountInfos)
}
}
}
@ -142,7 +133,9 @@ class WebdavMountsActivity: AppCompatActivity() {
title = { Text(stringResource(R.string.webdav_mounts_title)) },
actions = {
IconButton(
onClick = { uriHandler.openUri(helpUrl(context).toString()) }
onClick = {
uriHandler.openUri(helpUrl.toString())
}
) {
Icon(
imageVector = Icons.AutoMirrored.Filled.Help,
@ -209,7 +202,7 @@ class WebdavMountsActivity: AppCompatActivity() {
val text = HtmlCompat.fromHtml(
stringResource(
R.string.webdav_add_mount_empty_more_info,
helpUrl(context).toString()
helpUrl.toString()
),
0
).toAnnotatedString()
@ -340,7 +333,7 @@ class WebdavMountsActivity: AppCompatActivity() {
@Preview(showBackground = true, showSystemUi = true)
@Composable
fun WebdavMountsContent_Preview() {
MdcTheme {
AppTheme {
WebdavMountsContent(emptyList())
}
}
@ -348,7 +341,7 @@ class WebdavMountsActivity: AppCompatActivity() {
@Preview(showBackground = true)
@Composable
fun WebdavMountsItem_Preview() {
MdcTheme {
AppTheme {
WebdavMountsItem(
info = MountInfo(
mount = WebDavMount(

View file

@ -2,7 +2,6 @@
<!-- common strings -->
<string name="app_name" translatable="false">DAVx⁵</string>
<string name="homepage_url" translatable="false">https://www.davx5.com/</string>
<string name="account_invalid">Account does not exist (anymore)</string>
<string name="account_type" translatable="false">bitfire.at.davdroid</string>