mirror of
https://github.com/bitfireAT/davx5-ose
synced 2024-07-22 11:11:02 +00:00
Minor classes refactoring
This commit is contained in:
parent
77ab1801fa
commit
3edcc02a21
|
@ -28,6 +28,7 @@ import at.bitfire.davdroid.resource.TaskUtils
|
|||
import at.bitfire.davdroid.settings.Settings
|
||||
import at.bitfire.davdroid.settings.SettingsManager
|
||||
import at.bitfire.davdroid.ui.intro.BatteryOptimizationsPage
|
||||
import at.bitfire.davdroid.ui.intro.OpenSourcePage
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
@ -273,8 +274,8 @@ class AppSettingsActivity: AppCompatActivity() {
|
|||
private fun resetHints() {
|
||||
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(TasksActivity.Model.HINT_OPENTASKS_NOT_INSTALLED)
|
||||
settings.remove(OpenSourcePage.Model.SETTING_NEXT_DONATION_POPUP)
|
||||
Snackbar.make(requireView(), R.string.app_settings_reset_hints_success, Snackbar.LENGTH_LONG).show()
|
||||
}
|
||||
|
||||
|
|
|
@ -4,23 +4,65 @@
|
|||
|
||||
package at.bitfire.davdroid.ui
|
||||
|
||||
import android.Manifest
|
||||
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.widget.Toast
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.viewModels
|
||||
import androidx.annotation.MainThread
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.OutlinedButton
|
||||
import androidx.compose.material.Switch
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
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.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import at.bitfire.davdroid.BuildConfig
|
||||
import at.bitfire.davdroid.PackageChangedReceiver
|
||||
import at.bitfire.davdroid.R
|
||||
import at.bitfire.davdroid.log.Logger
|
||||
import at.bitfire.davdroid.ui.widget.CardWithImage
|
||||
import at.bitfire.davdroid.util.PermissionUtils
|
||||
import at.bitfire.ical4android.TaskProvider
|
||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||
import com.google.accompanist.permissions.rememberMultiplePermissionsState
|
||||
import com.google.accompanist.themeadapter.material.MdcTheme
|
||||
import java.util.logging.Level
|
||||
|
||||
class PermissionsActivity: AppCompatActivity() {
|
||||
|
||||
val model by viewModels<PermissionsFragment.Model>()
|
||||
val model by viewModels<Model>()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
setContent {
|
||||
MdcTheme {
|
||||
PermissionsFragmentContent(model)
|
||||
PermissionsContent(model)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -30,4 +72,271 @@ class PermissionsActivity: AppCompatActivity() {
|
|||
model.checkPermissions()
|
||||
}
|
||||
|
||||
|
||||
class Model(app: Application): AndroidViewModel(app) {
|
||||
|
||||
val needKeepPermissions = MutableLiveData<Boolean>()
|
||||
|
||||
val openTasksAvailable = MutableLiveData<Boolean>()
|
||||
val tasksOrgAvailable = MutableLiveData<Boolean>()
|
||||
val jtxAvailable = MutableLiveData<Boolean>()
|
||||
|
||||
private val tasksWatcher = object: PackageChangedReceiver(app) {
|
||||
@MainThread
|
||||
override fun onReceive(context: Context?, intent: Intent?) {
|
||||
checkPermissions()
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
checkPermissions()
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
tasksWatcher.close()
|
||||
}
|
||||
|
||||
@MainThread
|
||||
fun checkPermissions() {
|
||||
val pm = getApplication<Application>().packageManager
|
||||
|
||||
// auto-reset permissions
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
needKeepPermissions.value = pm.isAutoRevokeWhitelisted
|
||||
}
|
||||
|
||||
openTasksAvailable.value = pm.resolveContentProvider(TaskProvider.ProviderName.OpenTasks.authority, 0) != null
|
||||
tasksOrgAvailable.value = pm.resolveContentProvider(TaskProvider.ProviderName.TasksOrg.authority, 0) != null
|
||||
jtxAvailable.value = pm.resolveContentProvider(TaskProvider.ProviderName.JtxBoard.authority, 0) != null
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@Composable
|
||||
fun PermissionsContent(model: PermissionsActivity.Model = viewModel()) {
|
||||
val context = LocalContext.current
|
||||
|
||||
val keepPermissions by model.needKeepPermissions.observeAsState()
|
||||
val openTasksAvailable by model.openTasksAvailable.observeAsState()
|
||||
val tasksOrgAvailable by model.tasksOrgAvailable.observeAsState()
|
||||
val jtxAvailable by model.jtxAvailable.observeAsState()
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
PermissionsCard(
|
||||
keepPermissions,
|
||||
onKeepPermissionsRequested = {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
val intent = Intent(
|
||||
Intent.ACTION_AUTO_REVOKE_PERMISSIONS,
|
||||
Uri.fromParts("package", BuildConfig.APPLICATION_ID, null)
|
||||
)
|
||||
try {
|
||||
context.startActivity(intent)
|
||||
Toast.makeText(context, R.string.permissions_autoreset_instruction, Toast.LENGTH_LONG).show()
|
||||
} catch (e: Exception) {
|
||||
Logger.log.log(Level.WARNING, "Couldn't start Keep Permissions activity", e)
|
||||
}
|
||||
}
|
||||
},
|
||||
openTasksAvailable,
|
||||
tasksOrgAvailable,
|
||||
jtxAvailable
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@OptIn(ExperimentalPermissionsApi::class)
|
||||
fun PermissionSwitchRow(
|
||||
text: String,
|
||||
permissions: List<String>,
|
||||
summaryWhenGranted: String,
|
||||
summaryWhenNotGranted: String,
|
||||
modifier: Modifier = Modifier,
|
||||
fontWeight: FontWeight = FontWeight.Normal
|
||||
) {
|
||||
val state = rememberMultiplePermissionsState(permissions = permissions.toList())
|
||||
|
||||
PermissionSwitchRow(
|
||||
text = text,
|
||||
fontWeight = fontWeight,
|
||||
summaryWhenGranted = summaryWhenGranted,
|
||||
summaryWhenNotGranted = summaryWhenNotGranted,
|
||||
allPermissionsGranted = state.allPermissionsGranted,
|
||||
onLaunchRequest = state::launchMultiplePermissionRequest,
|
||||
modifier = modifier
|
||||
)
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
fun PermissionSwitchRow_Preview() {
|
||||
PermissionSwitchRow(
|
||||
text = "Contacts",
|
||||
allPermissionsGranted = false,
|
||||
summaryWhenGranted = "Granted",
|
||||
summaryWhenNotGranted = "Not granted",
|
||||
onLaunchRequest = {}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun PermissionSwitchRow(
|
||||
text: String,
|
||||
allPermissionsGranted: Boolean,
|
||||
summaryWhenGranted: String,
|
||||
summaryWhenNotGranted: String,
|
||||
modifier: Modifier = Modifier,
|
||||
fontWeight: FontWeight = FontWeight.Normal,
|
||||
onLaunchRequest: () -> Unit
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.weight(1f)
|
||||
) {
|
||||
Text(
|
||||
text = text,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
fontWeight = fontWeight,
|
||||
style = MaterialTheme.typography.body1
|
||||
)
|
||||
Text(
|
||||
text = if (allPermissionsGranted) summaryWhenGranted else summaryWhenNotGranted,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
style = MaterialTheme.typography.body2
|
||||
)
|
||||
}
|
||||
Switch(
|
||||
checked = allPermissionsGranted,
|
||||
enabled = !allPermissionsGranted,
|
||||
onCheckedChange = { checked ->
|
||||
if (checked) {
|
||||
onLaunchRequest()
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun PermissionsCard(
|
||||
keepPermissions: Boolean?,
|
||||
onKeepPermissionsRequested: () -> Unit,
|
||||
openTasksAvailable: Boolean?,
|
||||
tasksOrgAvailable: Boolean?,
|
||||
jtxAvailable: Boolean?
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
|
||||
CardWithImage(
|
||||
title = stringResource(R.string.permissions_title),
|
||||
message = stringResource(
|
||||
R.string.permissions_text,
|
||||
stringResource(R.string.app_name)
|
||||
),
|
||||
image = painterResource(R.drawable.intro_permissions),
|
||||
modifier = Modifier.padding(8.dp)
|
||||
) {
|
||||
if (keepPermissions != null) {
|
||||
PermissionSwitchRow(
|
||||
text = stringResource(R.string.permissions_autoreset_title),
|
||||
summaryWhenGranted = stringResource(R.string.permissions_autoreset_status_on),
|
||||
summaryWhenNotGranted = stringResource(R.string.permissions_autoreset_status_off),
|
||||
allPermissionsGranted = keepPermissions,
|
||||
onLaunchRequest = onKeepPermissionsRequested,
|
||||
modifier = Modifier.padding(vertical = 4.dp)
|
||||
)
|
||||
}
|
||||
|
||||
val allPermissions = mutableListOf<String>()
|
||||
allPermissions.addAll(PermissionUtils.CONTACT_PERMISSIONS)
|
||||
allPermissions.addAll(PermissionUtils.CALENDAR_PERMISSIONS)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU)
|
||||
allPermissions += Manifest.permission.POST_NOTIFICATIONS
|
||||
if (openTasksAvailable == true)
|
||||
allPermissions.addAll(TaskProvider.PERMISSIONS_OPENTASKS)
|
||||
if (tasksOrgAvailable == true)
|
||||
allPermissions.addAll(TaskProvider.PERMISSIONS_TASKS_ORG)
|
||||
if (jtxAvailable == true)
|
||||
allPermissions.addAll(TaskProvider.PERMISSIONS_JTX)
|
||||
PermissionSwitchRow(
|
||||
text = stringResource(R.string.permissions_all_title),
|
||||
permissions = allPermissions,
|
||||
summaryWhenGranted = stringResource(R.string.permissions_all_status_on),
|
||||
summaryWhenNotGranted = stringResource(R.string.permissions_all_status_off),
|
||||
fontWeight = FontWeight.Bold,
|
||||
modifier = Modifier.padding(vertical = 4.dp)
|
||||
)
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU)
|
||||
PermissionSwitchRow(
|
||||
text = stringResource(R.string.permissions_notification_title),
|
||||
summaryWhenGranted = stringResource(R.string.permissions_notification_status_on),
|
||||
summaryWhenNotGranted = stringResource(R.string.permissions_notification_status_off),
|
||||
permissions = listOf(Manifest.permission.POST_NOTIFICATIONS),
|
||||
modifier = Modifier.padding(vertical = 4.dp)
|
||||
)
|
||||
|
||||
PermissionSwitchRow(
|
||||
text = stringResource(R.string.permissions_calendar_title),
|
||||
summaryWhenGranted = stringResource(R.string.permissions_calendar_status_on),
|
||||
summaryWhenNotGranted = stringResource(R.string.permissions_calendar_status_off),
|
||||
permissions = PermissionUtils.CALENDAR_PERMISSIONS.toList(),
|
||||
modifier = Modifier.padding(vertical = 4.dp)
|
||||
)
|
||||
PermissionSwitchRow(
|
||||
text = stringResource(R.string.permissions_contacts_title),
|
||||
summaryWhenGranted = stringResource(R.string.permissions_contacts_status_on),
|
||||
summaryWhenNotGranted = stringResource(R.string.permissions_contacts_status_off),
|
||||
permissions = PermissionUtils.CONTACT_PERMISSIONS.toList(),
|
||||
modifier = Modifier.padding(vertical = 4.dp)
|
||||
)
|
||||
|
||||
if (jtxAvailable == true)
|
||||
PermissionSwitchRow(
|
||||
text = stringResource(R.string.permissions_jtx_title),
|
||||
summaryWhenGranted = stringResource(R.string.permissions_tasks_status_on),
|
||||
summaryWhenNotGranted = stringResource(R.string.permissions_tasks_status_off),
|
||||
permissions = TaskProvider.PERMISSIONS_JTX.toList(),
|
||||
modifier = Modifier.padding(vertical = 4.dp)
|
||||
)
|
||||
if (openTasksAvailable == true)
|
||||
PermissionSwitchRow(
|
||||
text = stringResource(R.string.permissions_opentasks_title),
|
||||
summaryWhenGranted = stringResource(R.string.permissions_tasks_status_on),
|
||||
summaryWhenNotGranted = stringResource(R.string.permissions_tasks_status_off),
|
||||
permissions = TaskProvider.PERMISSIONS_OPENTASKS.toList(),
|
||||
modifier = Modifier.padding(vertical = 4.dp)
|
||||
)
|
||||
if (tasksOrgAvailable == true)
|
||||
PermissionSwitchRow(
|
||||
text = stringResource(R.string.permissions_tasksorg_title),
|
||||
summaryWhenGranted = stringResource(R.string.permissions_tasks_status_on),
|
||||
summaryWhenNotGranted = stringResource(R.string.permissions_tasks_status_off),
|
||||
permissions = TaskProvider.PERMISSIONS_TASKS_ORG.toList(),
|
||||
modifier = Modifier.padding(vertical = 4.dp)
|
||||
)
|
||||
|
||||
Text(
|
||||
text = stringResource(R.string.permissions_app_settings_hint),
|
||||
style = MaterialTheme.typography.body1,
|
||||
modifier = Modifier.padding(top = 24.dp)
|
||||
)
|
||||
|
||||
OutlinedButton(
|
||||
modifier = Modifier.padding(top = 8.dp),
|
||||
onClick = { PermissionUtils.showAppSettings(context) }
|
||||
) {
|
||||
Text(stringResource(R.string.permissions_app_settings).uppercase())
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,347 +0,0 @@
|
|||
/***************************************************************************************************
|
||||
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
|
||||
**************************************************************************************************/
|
||||
|
||||
package at.bitfire.davdroid.ui
|
||||
|
||||
import android.Manifest
|
||||
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.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Toast
|
||||
import androidx.annotation.MainThread
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.OutlinedButton
|
||||
import androidx.compose.material.Switch
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
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.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import at.bitfire.davdroid.BuildConfig
|
||||
import at.bitfire.davdroid.PackageChangedReceiver
|
||||
import at.bitfire.davdroid.R
|
||||
import at.bitfire.davdroid.log.Logger
|
||||
import at.bitfire.davdroid.ui.widget.CardWithImage
|
||||
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 at.bitfire.ical4android.TaskProvider.ProviderName
|
||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||
import com.google.accompanist.permissions.rememberMultiplePermissionsState
|
||||
import com.google.accompanist.themeadapter.material.MdcTheme
|
||||
import java.util.logging.Level
|
||||
|
||||
class PermissionsFragment: Fragment() {
|
||||
|
||||
val model by viewModels<Model>()
|
||||
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
return ComposeView(requireContext()).apply {
|
||||
setContent {
|
||||
MdcTheme {
|
||||
PermissionsFragmentContent(model)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
model.checkPermissions()
|
||||
}
|
||||
|
||||
|
||||
class Model(app: Application): AndroidViewModel(app) {
|
||||
|
||||
val needKeepPermissions = MutableLiveData<Boolean>()
|
||||
|
||||
val openTasksAvailable = MutableLiveData<Boolean>()
|
||||
val tasksOrgAvailable = MutableLiveData<Boolean>()
|
||||
val jtxAvailable = MutableLiveData<Boolean>()
|
||||
|
||||
private val tasksWatcher = object: PackageChangedReceiver(app) {
|
||||
@MainThread
|
||||
override fun onReceive(context: Context?, intent: Intent?) {
|
||||
checkPermissions()
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
checkPermissions()
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
tasksWatcher.close()
|
||||
}
|
||||
|
||||
@MainThread
|
||||
fun checkPermissions() {
|
||||
val pm = getApplication<Application>().packageManager
|
||||
|
||||
// auto-reset permissions
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
needKeepPermissions.value = pm.isAutoRevokeWhitelisted
|
||||
}
|
||||
|
||||
openTasksAvailable.value = pm.resolveContentProvider(ProviderName.OpenTasks.authority, 0) != null
|
||||
tasksOrgAvailable.value = pm.resolveContentProvider(ProviderName.TasksOrg.authority, 0) != null
|
||||
jtxAvailable.value = pm.resolveContentProvider(ProviderName.JtxBoard.authority, 0) != null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun PermissionsFragmentContent(model: PermissionsFragment.Model = viewModel()) {
|
||||
val context = LocalContext.current
|
||||
|
||||
val keepPermissions by model.needKeepPermissions.observeAsState()
|
||||
val openTasksAvailable by model.openTasksAvailable.observeAsState()
|
||||
val tasksOrgAvailable by model.tasksOrgAvailable.observeAsState()
|
||||
val jtxAvailable by model.jtxAvailable.observeAsState()
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
PermissionsCard(
|
||||
keepPermissions,
|
||||
onKeepPermissionsRequested = {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
val intent = Intent(
|
||||
Intent.ACTION_AUTO_REVOKE_PERMISSIONS,
|
||||
Uri.fromParts("package", BuildConfig.APPLICATION_ID, null)
|
||||
)
|
||||
try {
|
||||
context.startActivity(intent)
|
||||
Toast.makeText(context, R.string.permissions_autoreset_instruction, Toast.LENGTH_LONG).show()
|
||||
} catch (e: Exception) {
|
||||
Logger.log.log(Level.WARNING, "Couldn't start Keep Permissions activity", e)
|
||||
}
|
||||
}
|
||||
},
|
||||
openTasksAvailable,
|
||||
tasksOrgAvailable,
|
||||
jtxAvailable
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@OptIn(ExperimentalPermissionsApi::class)
|
||||
fun PermissionSwitchRow(
|
||||
text: String,
|
||||
permissions: List<String>,
|
||||
summaryWhenGranted: String,
|
||||
summaryWhenNotGranted: String,
|
||||
modifier: Modifier = Modifier,
|
||||
fontWeight: FontWeight = FontWeight.Normal
|
||||
) {
|
||||
val state = rememberMultiplePermissionsState(permissions = permissions.toList())
|
||||
|
||||
PermissionSwitchRow(
|
||||
text = text,
|
||||
fontWeight = fontWeight,
|
||||
summaryWhenGranted = summaryWhenGranted,
|
||||
summaryWhenNotGranted = summaryWhenNotGranted,
|
||||
allPermissionsGranted = state.allPermissionsGranted,
|
||||
onLaunchRequest = state::launchMultiplePermissionRequest,
|
||||
modifier = modifier
|
||||
)
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
fun PermissionSwitchRow_Preview() {
|
||||
PermissionSwitchRow(
|
||||
text = "Contacts",
|
||||
allPermissionsGranted = false,
|
||||
summaryWhenGranted = "Granted",
|
||||
summaryWhenNotGranted = "Not granted",
|
||||
onLaunchRequest = {}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun PermissionSwitchRow(
|
||||
text: String,
|
||||
allPermissionsGranted: Boolean,
|
||||
summaryWhenGranted: String,
|
||||
summaryWhenNotGranted: String,
|
||||
modifier: Modifier = Modifier,
|
||||
fontWeight: FontWeight = FontWeight.Normal,
|
||||
onLaunchRequest: () -> Unit
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.weight(1f)
|
||||
) {
|
||||
Text(
|
||||
text = text,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
fontWeight = fontWeight,
|
||||
style = MaterialTheme.typography.body1
|
||||
)
|
||||
Text(
|
||||
text = if (allPermissionsGranted) summaryWhenGranted else summaryWhenNotGranted,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
style = MaterialTheme.typography.body2
|
||||
)
|
||||
}
|
||||
Switch(
|
||||
checked = allPermissionsGranted,
|
||||
enabled = !allPermissionsGranted,
|
||||
onCheckedChange = { checked ->
|
||||
if (checked) {
|
||||
onLaunchRequest()
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun PermissionsCard(
|
||||
keepPermissions: Boolean?,
|
||||
onKeepPermissionsRequested: () -> Unit,
|
||||
openTasksAvailable: Boolean?,
|
||||
tasksOrgAvailable: Boolean?,
|
||||
jtxAvailable: Boolean?
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
|
||||
CardWithImage(
|
||||
title = stringResource(R.string.permissions_title),
|
||||
message = stringResource(
|
||||
R.string.permissions_text,
|
||||
stringResource(R.string.app_name)
|
||||
),
|
||||
image = painterResource(R.drawable.intro_permissions),
|
||||
modifier = Modifier.padding(8.dp)
|
||||
) {
|
||||
if (keepPermissions != null) {
|
||||
PermissionSwitchRow(
|
||||
text = stringResource(R.string.permissions_autoreset_title),
|
||||
summaryWhenGranted = stringResource(R.string.permissions_autoreset_status_on),
|
||||
summaryWhenNotGranted = stringResource(R.string.permissions_autoreset_status_off),
|
||||
allPermissionsGranted = keepPermissions,
|
||||
onLaunchRequest = onKeepPermissionsRequested,
|
||||
modifier = Modifier.padding(vertical = 4.dp)
|
||||
)
|
||||
}
|
||||
|
||||
val allPermissions = mutableListOf<String>()
|
||||
allPermissions.addAll(CONTACT_PERMISSIONS)
|
||||
allPermissions.addAll(CALENDAR_PERMISSIONS)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU)
|
||||
allPermissions += Manifest.permission.POST_NOTIFICATIONS
|
||||
if (openTasksAvailable == true)
|
||||
allPermissions.addAll(TaskProvider.PERMISSIONS_OPENTASKS)
|
||||
if (tasksOrgAvailable == true)
|
||||
allPermissions.addAll(TaskProvider.PERMISSIONS_TASKS_ORG)
|
||||
if (jtxAvailable == true)
|
||||
allPermissions.addAll(TaskProvider.PERMISSIONS_JTX)
|
||||
PermissionSwitchRow(
|
||||
text = stringResource(R.string.permissions_all_title),
|
||||
permissions = allPermissions,
|
||||
summaryWhenGranted = stringResource(R.string.permissions_all_status_on),
|
||||
summaryWhenNotGranted = stringResource(R.string.permissions_all_status_off),
|
||||
fontWeight = FontWeight.Bold,
|
||||
modifier = Modifier.padding(vertical = 4.dp)
|
||||
)
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU)
|
||||
PermissionSwitchRow(
|
||||
text = stringResource(R.string.permissions_notification_title),
|
||||
summaryWhenGranted = stringResource(R.string.permissions_notification_status_on),
|
||||
summaryWhenNotGranted = stringResource(R.string.permissions_notification_status_off),
|
||||
permissions = listOf(Manifest.permission.POST_NOTIFICATIONS),
|
||||
modifier = Modifier.padding(vertical = 4.dp)
|
||||
)
|
||||
|
||||
PermissionSwitchRow(
|
||||
text = stringResource(R.string.permissions_calendar_title),
|
||||
summaryWhenGranted = stringResource(R.string.permissions_calendar_status_on),
|
||||
summaryWhenNotGranted = stringResource(R.string.permissions_calendar_status_off),
|
||||
permissions = CALENDAR_PERMISSIONS.toList(),
|
||||
modifier = Modifier.padding(vertical = 4.dp)
|
||||
)
|
||||
PermissionSwitchRow(
|
||||
text = stringResource(R.string.permissions_contacts_title),
|
||||
summaryWhenGranted = stringResource(R.string.permissions_contacts_status_on),
|
||||
summaryWhenNotGranted = stringResource(R.string.permissions_contacts_status_off),
|
||||
permissions = CONTACT_PERMISSIONS.toList(),
|
||||
modifier = Modifier.padding(vertical = 4.dp)
|
||||
)
|
||||
|
||||
if (jtxAvailable == true)
|
||||
PermissionSwitchRow(
|
||||
text = stringResource(R.string.permissions_jtx_title),
|
||||
summaryWhenGranted = stringResource(R.string.permissions_tasks_status_on),
|
||||
summaryWhenNotGranted = stringResource(R.string.permissions_tasks_status_off),
|
||||
permissions = TaskProvider.PERMISSIONS_JTX.toList(),
|
||||
modifier = Modifier.padding(vertical = 4.dp)
|
||||
)
|
||||
if (openTasksAvailable == true)
|
||||
PermissionSwitchRow(
|
||||
text = stringResource(R.string.permissions_opentasks_title),
|
||||
summaryWhenGranted = stringResource(R.string.permissions_tasks_status_on),
|
||||
summaryWhenNotGranted = stringResource(R.string.permissions_tasks_status_off),
|
||||
permissions = TaskProvider.PERMISSIONS_OPENTASKS.toList(),
|
||||
modifier = Modifier.padding(vertical = 4.dp)
|
||||
)
|
||||
if (tasksOrgAvailable == true)
|
||||
PermissionSwitchRow(
|
||||
text = stringResource(R.string.permissions_tasksorg_title),
|
||||
summaryWhenGranted = stringResource(R.string.permissions_tasks_status_on),
|
||||
summaryWhenNotGranted = stringResource(R.string.permissions_tasks_status_off),
|
||||
permissions = TaskProvider.PERMISSIONS_TASKS_ORG.toList(),
|
||||
modifier = Modifier.padding(vertical = 4.dp)
|
||||
)
|
||||
|
||||
Text(
|
||||
text = stringResource(R.string.permissions_app_settings_hint),
|
||||
style = MaterialTheme.typography.body1,
|
||||
modifier = Modifier.padding(top = 24.dp)
|
||||
)
|
||||
|
||||
OutlinedButton(
|
||||
modifier = Modifier.padding(top = 8.dp),
|
||||
onClick = { PermissionUtils.showAppSettings(context) }
|
||||
) {
|
||||
Text(stringResource(R.string.permissions_app_settings).uppercase())
|
||||
}
|
||||
}
|
||||
}
|
|
@ -4,16 +4,68 @@
|
|||
|
||||
package at.bitfire.davdroid.ui
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.viewModels
|
||||
import androidx.annotation.AnyThread
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.text.ClickableText
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.Checkbox
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Scaffold
|
||||
import androidx.compose.material.SnackbarDuration
|
||||
import androidx.compose.material.SnackbarHost
|
||||
import androidx.compose.material.SnackbarHostState
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
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.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.ExperimentalTextApi
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.text.HtmlCompat
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import at.bitfire.davdroid.BuildConfig
|
||||
import at.bitfire.davdroid.PackageChangedReceiver
|
||||
import at.bitfire.davdroid.R
|
||||
import at.bitfire.davdroid.resource.TaskUtils
|
||||
import at.bitfire.davdroid.settings.SettingsManager
|
||||
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
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class TasksActivity: AppCompatActivity() {
|
||||
val model: TasksModel by viewModels()
|
||||
val model: Model by viewModels()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
@ -24,4 +76,266 @@ class TasksActivity: AppCompatActivity() {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@HiltViewModel
|
||||
class Model @Inject constructor(
|
||||
application: Application,
|
||||
val settings: SettingsManager
|
||||
) : AndroidViewModel(application), SettingsManager.OnChangeListener {
|
||||
|
||||
companion object {
|
||||
|
||||
/**
|
||||
* Whether this fragment (which asks for OpenTasks installation) shall be shown.
|
||||
* If this setting is true or null/not set, the notice shall be shown. Only if this
|
||||
* setting is false, the notice shall not be shown.
|
||||
*/
|
||||
const val HINT_OPENTASKS_NOT_INSTALLED = "hint_OpenTasksNotInstalled"
|
||||
|
||||
}
|
||||
|
||||
val currentProvider = MutableLiveData<TaskProvider.ProviderName>()
|
||||
val openTasksInstalled = MutableLiveData<Boolean>()
|
||||
val openTasksRequested = MutableLiveData<Boolean>()
|
||||
val openTasksSelected = MutableLiveData<Boolean>()
|
||||
val tasksOrgInstalled = MutableLiveData<Boolean>()
|
||||
val tasksOrgRequested = MutableLiveData<Boolean>()
|
||||
val tasksOrgSelected = MutableLiveData<Boolean>()
|
||||
val jtxInstalled = MutableLiveData<Boolean>()
|
||||
val jtxRequested = MutableLiveData<Boolean>()
|
||||
val jtxSelected = MutableLiveData<Boolean>()
|
||||
|
||||
private val tasksWatcher = object: PackageChangedReceiver(application) {
|
||||
override fun onReceive(context: Context?, intent: Intent?) {
|
||||
checkInstalled()
|
||||
}
|
||||
}
|
||||
|
||||
val dontShow = MutableLiveData(
|
||||
settings.getBooleanOrNull(HINT_OPENTASKS_NOT_INSTALLED) == false
|
||||
)
|
||||
|
||||
private val dontShowObserver = Observer<Boolean> { value ->
|
||||
if (value)
|
||||
settings.putBoolean(HINT_OPENTASKS_NOT_INSTALLED, false)
|
||||
else
|
||||
settings.remove(HINT_OPENTASKS_NOT_INSTALLED)
|
||||
}
|
||||
|
||||
init {
|
||||
checkInstalled()
|
||||
settings.addOnChangeListener(this)
|
||||
dontShow.observeForever(dontShowObserver)
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
settings.removeOnChangeListener(this)
|
||||
tasksWatcher.close()
|
||||
dontShow.removeObserver(dontShowObserver)
|
||||
}
|
||||
|
||||
@AnyThread
|
||||
fun checkInstalled() {
|
||||
val taskProvider = TaskUtils.currentProvider(getApplication())
|
||||
currentProvider.postValue(taskProvider)
|
||||
|
||||
val openTasks = isInstalled(TaskProvider.ProviderName.OpenTasks.packageName)
|
||||
openTasksInstalled.postValue(openTasks)
|
||||
openTasksRequested.postValue(openTasks)
|
||||
openTasksSelected.postValue(taskProvider == TaskProvider.ProviderName.OpenTasks)
|
||||
|
||||
val tasksOrg = isInstalled(TaskProvider.ProviderName.TasksOrg.packageName)
|
||||
tasksOrgInstalled.postValue(tasksOrg)
|
||||
tasksOrgRequested.postValue(tasksOrg)
|
||||
tasksOrgSelected.postValue(taskProvider == TaskProvider.ProviderName.TasksOrg)
|
||||
|
||||
val jtxBoard = isInstalled(TaskProvider.ProviderName.JtxBoard.packageName)
|
||||
jtxInstalled.postValue(jtxBoard)
|
||||
jtxRequested.postValue(jtxBoard)
|
||||
jtxSelected.postValue(taskProvider == TaskProvider.ProviderName.JtxBoard)
|
||||
}
|
||||
|
||||
private fun isInstalled(packageName: String): Boolean =
|
||||
try {
|
||||
getApplication<Application>().packageManager.getPackageInfo(packageName, 0)
|
||||
true
|
||||
} catch (e: PackageManager.NameNotFoundException) {
|
||||
false
|
||||
}
|
||||
|
||||
fun selectPreferredProvider(provider: TaskProvider.ProviderName) {
|
||||
// Changes preferred task app setting, so onSettingsChanged() will be called
|
||||
TaskUtils.setPreferredProvider(getApplication(), provider)
|
||||
}
|
||||
|
||||
|
||||
override fun onSettingsChanged() {
|
||||
checkInstalled()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@OptIn(ExperimentalTextApi::class)
|
||||
@Composable
|
||||
fun TasksCard(
|
||||
modifier: Modifier = Modifier,
|
||||
model: TasksActivity.Model = viewModel()
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
|
||||
val snackbarHostState = remember { SnackbarHostState() }
|
||||
|
||||
val jtxInstalled by model.jtxInstalled.observeAsState(initial = false)
|
||||
val jtxSelected by model.jtxSelected.observeAsState(initial = false)
|
||||
val jtxRequested by model.jtxRequested.observeAsState(initial = false)
|
||||
|
||||
val tasksOrgInstalled by model.tasksOrgInstalled.observeAsState(initial = false)
|
||||
val tasksOrgSelected by model.tasksOrgSelected.observeAsState(initial = false)
|
||||
val tasksOrgRequested by model.tasksOrgRequested.observeAsState(initial = false)
|
||||
|
||||
val openTasksInstalled by model.openTasksInstalled.observeAsState(initial = false)
|
||||
val openTasksSelected by model.openTasksSelected.observeAsState(initial = false)
|
||||
val openTasksRequested by model.openTasksRequested.observeAsState(initial = false)
|
||||
|
||||
val dontShow by model.dontShow.observeAsState(initial = false)
|
||||
|
||||
fun installApp(packageName: String) {
|
||||
val uri = Uri.parse("market://details?id=$packageName&referrer=" +
|
||||
Uri.encode("utm_source=" + BuildConfig.APPLICATION_ID))
|
||||
val intent = Intent(Intent.ACTION_VIEW, uri)
|
||||
if (intent.resolveActivity(context.packageManager) != null)
|
||||
context.startActivity(intent)
|
||||
else
|
||||
coroutineScope.launch {
|
||||
snackbarHostState.showSnackbar(
|
||||
message = context.getString(R.string.intro_tasks_no_app_store),
|
||||
duration = SnackbarDuration.Long
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun onProviderSelected(provider: TaskProvider.ProviderName) {
|
||||
if (model.currentProvider.value != provider)
|
||||
model.selectPreferredProvider(provider)
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
modifier = modifier,
|
||||
snackbarHost = { SnackbarHost(snackbarHostState) }
|
||||
) { paddingValues ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxHeight()
|
||||
.padding(paddingValues)
|
||||
.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
CardWithImage(
|
||||
image = painterResource(R.drawable.intro_tasks),
|
||||
imageAlignment = BiasAlignment(0f, .1f),
|
||||
title = stringResource(R.string.intro_tasks_title),
|
||||
message = stringResource(R.string.intro_tasks_text1),
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 16.dp)
|
||||
.padding(top = 16.dp)
|
||||
) {
|
||||
RadioWithSwitch(
|
||||
title = stringResource(R.string.intro_tasks_jtx),
|
||||
summary = {
|
||||
Text(stringResource(R.string.intro_tasks_jtx_info))
|
||||
},
|
||||
isSelected = jtxSelected,
|
||||
isToggled = jtxRequested,
|
||||
enabled = jtxInstalled,
|
||||
onSelected = { onProviderSelected(TaskProvider.ProviderName.JtxBoard) },
|
||||
onToggled = { toggled ->
|
||||
if (toggled) installApp(TaskProvider.ProviderName.JtxBoard.packageName)
|
||||
},
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 12.dp)
|
||||
)
|
||||
|
||||
RadioWithSwitch(
|
||||
title = stringResource(R.string.intro_tasks_tasks_org),
|
||||
summary = {
|
||||
val summary = HtmlCompat.fromHtml(
|
||||
stringResource(R.string.intro_tasks_tasks_org_info),
|
||||
HtmlCompat.FROM_HTML_MODE_COMPACT
|
||||
).toAnnotatedString()
|
||||
|
||||
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))
|
||||
}
|
||||
}
|
||||
)
|
||||
},
|
||||
isSelected = tasksOrgSelected,
|
||||
isToggled = tasksOrgRequested,
|
||||
enabled = tasksOrgInstalled,
|
||||
onSelected = { onProviderSelected(TaskProvider.ProviderName.TasksOrg) },
|
||||
onToggled = { toggled ->
|
||||
if (toggled) installApp(TaskProvider.ProviderName.TasksOrg.packageName)
|
||||
},
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 12.dp)
|
||||
)
|
||||
|
||||
RadioWithSwitch(
|
||||
title = stringResource(R.string.intro_tasks_opentasks),
|
||||
summary = {
|
||||
Text(stringResource(R.string.intro_tasks_opentasks_info))
|
||||
},
|
||||
isSelected = openTasksSelected,
|
||||
isToggled = openTasksRequested,
|
||||
enabled = openTasksInstalled,
|
||||
onSelected = { onProviderSelected(TaskProvider.ProviderName.OpenTasks) },
|
||||
onToggled = { toggled ->
|
||||
if (toggled) installApp(TaskProvider.ProviderName.OpenTasks.packageName)
|
||||
},
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 12.dp)
|
||||
)
|
||||
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 12.dp)
|
||||
) {
|
||||
Checkbox(
|
||||
checked = dontShow,
|
||||
onCheckedChange = { model.dontShow.value = it }
|
||||
)
|
||||
Text(
|
||||
text = stringResource(R.string.intro_tasks_dont_show),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable { model.dontShow.value = !dontShow }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Text(
|
||||
text = stringResource(
|
||||
R.string.intro_leave_unchecked,
|
||||
stringResource(R.string.app_settings_reset_hints)
|
||||
),
|
||||
style = MaterialTheme.typography.caption,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 12.dp, vertical = 8.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,317 +0,0 @@
|
|||
/***************************************************************************************************
|
||||
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
|
||||
**************************************************************************************************/
|
||||
|
||||
package at.bitfire.davdroid.ui
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.net.Uri
|
||||
import androidx.annotation.AnyThread
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.text.ClickableText
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.Checkbox
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Scaffold
|
||||
import androidx.compose.material.SnackbarDuration
|
||||
import androidx.compose.material.SnackbarHost
|
||||
import androidx.compose.material.SnackbarHostState
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
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.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.ExperimentalTextApi
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.text.HtmlCompat
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import at.bitfire.davdroid.BuildConfig
|
||||
import at.bitfire.davdroid.PackageChangedReceiver
|
||||
import at.bitfire.davdroid.R
|
||||
import at.bitfire.davdroid.resource.TaskUtils
|
||||
import at.bitfire.davdroid.settings.SettingsManager
|
||||
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.ProviderName
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class TasksModel @Inject constructor(
|
||||
application: Application,
|
||||
val settings: SettingsManager
|
||||
) : AndroidViewModel(application), SettingsManager.OnChangeListener {
|
||||
|
||||
companion object {
|
||||
|
||||
/**
|
||||
* Whether this fragment (which asks for OpenTasks installation) shall be shown.
|
||||
* If this setting is true or null/not set, the notice shall be shown. Only if this
|
||||
* setting is false, the notice shall not be shown.
|
||||
*/
|
||||
const val HINT_OPENTASKS_NOT_INSTALLED = "hint_OpenTasksNotInstalled"
|
||||
|
||||
}
|
||||
|
||||
val currentProvider = MutableLiveData<ProviderName>()
|
||||
val openTasksInstalled = MutableLiveData<Boolean>()
|
||||
val openTasksRequested = MutableLiveData<Boolean>()
|
||||
val openTasksSelected = MutableLiveData<Boolean>()
|
||||
val tasksOrgInstalled = MutableLiveData<Boolean>()
|
||||
val tasksOrgRequested = MutableLiveData<Boolean>()
|
||||
val tasksOrgSelected = MutableLiveData<Boolean>()
|
||||
val jtxInstalled = MutableLiveData<Boolean>()
|
||||
val jtxRequested = MutableLiveData<Boolean>()
|
||||
val jtxSelected = MutableLiveData<Boolean>()
|
||||
|
||||
private val tasksWatcher = object: PackageChangedReceiver(application) {
|
||||
override fun onReceive(context: Context?, intent: Intent?) {
|
||||
checkInstalled()
|
||||
}
|
||||
}
|
||||
|
||||
val dontShow = MutableLiveData(
|
||||
settings.getBooleanOrNull(HINT_OPENTASKS_NOT_INSTALLED) == false
|
||||
)
|
||||
|
||||
private val dontShowObserver = Observer<Boolean> { value ->
|
||||
if (value)
|
||||
settings.putBoolean(HINT_OPENTASKS_NOT_INSTALLED, false)
|
||||
else
|
||||
settings.remove(HINT_OPENTASKS_NOT_INSTALLED)
|
||||
}
|
||||
|
||||
init {
|
||||
checkInstalled()
|
||||
settings.addOnChangeListener(this)
|
||||
dontShow.observeForever(dontShowObserver)
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
settings.removeOnChangeListener(this)
|
||||
tasksWatcher.close()
|
||||
dontShow.removeObserver(dontShowObserver)
|
||||
}
|
||||
|
||||
@AnyThread
|
||||
fun checkInstalled() {
|
||||
val taskProvider = TaskUtils.currentProvider(getApplication())
|
||||
currentProvider.postValue(taskProvider)
|
||||
|
||||
val openTasks = isInstalled(ProviderName.OpenTasks.packageName)
|
||||
openTasksInstalled.postValue(openTasks)
|
||||
openTasksRequested.postValue(openTasks)
|
||||
openTasksSelected.postValue(taskProvider == ProviderName.OpenTasks)
|
||||
|
||||
val tasksOrg = isInstalled(ProviderName.TasksOrg.packageName)
|
||||
tasksOrgInstalled.postValue(tasksOrg)
|
||||
tasksOrgRequested.postValue(tasksOrg)
|
||||
tasksOrgSelected.postValue(taskProvider == ProviderName.TasksOrg)
|
||||
|
||||
val jtxBoard = isInstalled(ProviderName.JtxBoard.packageName)
|
||||
jtxInstalled.postValue(jtxBoard)
|
||||
jtxRequested.postValue(jtxBoard)
|
||||
jtxSelected.postValue(taskProvider == ProviderName.JtxBoard)
|
||||
}
|
||||
|
||||
private fun isInstalled(packageName: String): Boolean =
|
||||
try {
|
||||
getApplication<Application>().packageManager.getPackageInfo(packageName, 0)
|
||||
true
|
||||
} catch (e: PackageManager.NameNotFoundException) {
|
||||
false
|
||||
}
|
||||
|
||||
fun selectPreferredProvider(provider: ProviderName) {
|
||||
// Changes preferred task app setting, so onSettingsChanged() will be called
|
||||
TaskUtils.setPreferredProvider(getApplication(), provider)
|
||||
}
|
||||
|
||||
|
||||
override fun onSettingsChanged() {
|
||||
checkInstalled()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalTextApi::class)
|
||||
@Composable
|
||||
fun TasksCard(
|
||||
modifier: Modifier = Modifier,
|
||||
model: TasksModel = viewModel()
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
|
||||
val snackbarHostState = remember { SnackbarHostState() }
|
||||
|
||||
val jtxInstalled by model.jtxInstalled.observeAsState(initial = false)
|
||||
val jtxSelected by model.jtxSelected.observeAsState(initial = false)
|
||||
val jtxRequested by model.jtxRequested.observeAsState(initial = false)
|
||||
|
||||
val tasksOrgInstalled by model.tasksOrgInstalled.observeAsState(initial = false)
|
||||
val tasksOrgSelected by model.tasksOrgSelected.observeAsState(initial = false)
|
||||
val tasksOrgRequested by model.tasksOrgRequested.observeAsState(initial = false)
|
||||
|
||||
val openTasksInstalled by model.openTasksInstalled.observeAsState(initial = false)
|
||||
val openTasksSelected by model.openTasksSelected.observeAsState(initial = false)
|
||||
val openTasksRequested by model.openTasksRequested.observeAsState(initial = false)
|
||||
|
||||
val dontShow by model.dontShow.observeAsState(initial = false)
|
||||
|
||||
fun installApp(packageName: String) {
|
||||
val uri = Uri.parse("market://details?id=$packageName&referrer=" +
|
||||
Uri.encode("utm_source=" + BuildConfig.APPLICATION_ID))
|
||||
val intent = Intent(Intent.ACTION_VIEW, uri)
|
||||
if (intent.resolveActivity(context.packageManager) != null)
|
||||
context.startActivity(intent)
|
||||
else
|
||||
coroutineScope.launch {
|
||||
snackbarHostState.showSnackbar(
|
||||
message = context.getString(R.string.intro_tasks_no_app_store),
|
||||
duration = SnackbarDuration.Long
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun onProviderSelected(provider: ProviderName) {
|
||||
if (model.currentProvider.value != provider)
|
||||
model.selectPreferredProvider(provider)
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
modifier = modifier,
|
||||
snackbarHost = { SnackbarHost(snackbarHostState) }
|
||||
) { paddingValues ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxHeight()
|
||||
.padding(paddingValues)
|
||||
.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
CardWithImage(
|
||||
image = painterResource(R.drawable.intro_tasks),
|
||||
imageAlignment = BiasAlignment(0f, .1f),
|
||||
title = stringResource(R.string.intro_tasks_title),
|
||||
message = stringResource(R.string.intro_tasks_text1),
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 16.dp)
|
||||
.padding(top = 16.dp)
|
||||
) {
|
||||
RadioWithSwitch(
|
||||
title = stringResource(R.string.intro_tasks_jtx),
|
||||
summary = {
|
||||
Text(stringResource(R.string.intro_tasks_jtx_info))
|
||||
},
|
||||
isSelected = jtxSelected,
|
||||
isToggled = jtxRequested,
|
||||
enabled = jtxInstalled,
|
||||
onSelected = { onProviderSelected(ProviderName.JtxBoard) },
|
||||
onToggled = { toggled ->
|
||||
if (toggled) installApp(ProviderName.JtxBoard.packageName)
|
||||
},
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 12.dp)
|
||||
)
|
||||
|
||||
RadioWithSwitch(
|
||||
title = stringResource(R.string.intro_tasks_tasks_org),
|
||||
summary = {
|
||||
val summary = HtmlCompat.fromHtml(
|
||||
stringResource(R.string.intro_tasks_tasks_org_info),
|
||||
HtmlCompat.FROM_HTML_MODE_COMPACT
|
||||
).toAnnotatedString()
|
||||
|
||||
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))
|
||||
}
|
||||
}
|
||||
)
|
||||
},
|
||||
isSelected = tasksOrgSelected,
|
||||
isToggled = tasksOrgRequested,
|
||||
enabled = tasksOrgInstalled,
|
||||
onSelected = { onProviderSelected(ProviderName.TasksOrg) },
|
||||
onToggled = { toggled ->
|
||||
if (toggled) installApp(ProviderName.TasksOrg.packageName)
|
||||
},
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 12.dp)
|
||||
)
|
||||
|
||||
RadioWithSwitch(
|
||||
title = stringResource(R.string.intro_tasks_opentasks),
|
||||
summary = {
|
||||
Text(stringResource(R.string.intro_tasks_opentasks_info))
|
||||
},
|
||||
isSelected = openTasksSelected,
|
||||
isToggled = openTasksRequested,
|
||||
enabled = openTasksInstalled,
|
||||
onSelected = { onProviderSelected(ProviderName.OpenTasks) },
|
||||
onToggled = { toggled ->
|
||||
if (toggled) installApp(ProviderName.OpenTasks.packageName)
|
||||
},
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 12.dp)
|
||||
)
|
||||
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 12.dp)
|
||||
) {
|
||||
Checkbox(
|
||||
checked = dontShow,
|
||||
onCheckedChange = { model.dontShow.value = it }
|
||||
)
|
||||
Text(
|
||||
text = stringResource(R.string.intro_tasks_dont_show),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable { model.dontShow.value = !dontShow }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Text(
|
||||
text = stringResource(
|
||||
R.string.intro_leave_unchecked,
|
||||
stringResource(R.string.app_settings_reset_hints)
|
||||
),
|
||||
style = MaterialTheme.typography.caption,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 12.dp, vertical = 8.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -6,7 +6,7 @@ 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.ui.PermissionsContent
|
||||
import at.bitfire.davdroid.util.PermissionUtils
|
||||
import at.bitfire.davdroid.util.PermissionUtils.CALENDAR_PERMISSIONS
|
||||
import at.bitfire.davdroid.util.PermissionUtils.CONTACT_PERMISSIONS
|
||||
|
@ -28,7 +28,7 @@ class PermissionsIntroPage: IntroPage {
|
|||
|
||||
@Composable
|
||||
override fun ComposePage() {
|
||||
PermissionsFragmentContent()
|
||||
PermissionsContent()
|
||||
}
|
||||
|
||||
}
|
|
@ -9,8 +9,8 @@ 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.TasksActivity
|
||||
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
|
||||
|
@ -27,7 +27,7 @@ class TasksIntroPage : IntroPage {
|
|||
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)
|
||||
return if (!TaskUtils.isAvailable(application) && settingsManager.getBooleanOrNull(TasksActivity.Model.HINT_OPENTASKS_NOT_INSTALLED) != false)
|
||||
IntroPage.ShowPolicy.SHOW_ALWAYS
|
||||
else
|
||||
IntroPage.ShowPolicy.DONT_SHOW
|
||||
|
@ -35,7 +35,7 @@ class TasksIntroPage : IntroPage {
|
|||
|
||||
@Composable
|
||||
override fun ComposePage() {
|
||||
TasksCard(model = viewModel<TasksModel>())
|
||||
TasksCard(model = viewModel<TasksActivity.Model>())
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in a new issue