"Add to home screen" shortcut in list settings

pull/3207/head
Alex Baker 12 months ago
parent ece1fd4ef3
commit 761ba583b5

@ -1,32 +1,56 @@
package org.tasks.activities
import android.app.PendingIntent
import android.content.Context
import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.Paint
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
import androidx.core.content.pm.ShortcutInfoCompat
import androidx.core.content.pm.ShortcutManagerCompat
import androidx.core.content.res.ResourcesCompat
import androidx.core.graphics.drawable.IconCompat
import androidx.lifecycle.lifecycleScope
import com.mikepenz.iconics.IconicsDrawable
import com.mikepenz.iconics.utils.colorInt
import com.mikepenz.iconics.utils.sizeDp
import kotlinx.coroutines.launch
import org.tasks.R
import org.tasks.analytics.Firebase
import org.tasks.compose.DeleteButton
import org.tasks.compose.IconPickerActivity.Companion.launchIconPicker
import org.tasks.compose.IconPickerActivity.Companion.registerForIconPickerResult
import org.tasks.compose.settings.BaseSettingsContent
import org.tasks.compose.settings.ListSettingsContent
import org.tasks.compose.settings.ListSettingsScaffold
import org.tasks.data.UUIDHelper
import org.tasks.dialogs.ColorPalettePicker
import org.tasks.dialogs.ColorPalettePicker.Companion.newColorPalette
import org.tasks.dialogs.ColorPickerAdapter.Palette
import org.tasks.dialogs.ColorWheelPicker
import org.tasks.extensions.addBackPressedCallback
import org.tasks.filters.Filter
import org.tasks.icons.OutlinedGoogleMaterial
import org.tasks.intents.TaskIntents
import org.tasks.preferences.DefaultFilterProvider
import org.tasks.themes.ColorProvider
import org.tasks.themes.Theme
import org.tasks.themes.ThemeColor
import org.tasks.themes.contentColorFor
import javax.inject.Inject
abstract class BaseListSettingsActivity : AppCompatActivity(), ColorPalettePicker.ColorPickedCallback, ColorWheelPicker.ColorPickedCallback {
@Inject lateinit var tasksTheme: Theme
@Inject lateinit var colorProvider: ColorProvider
@Inject lateinit var defaultFilterProvider: DefaultFilterProvider
@Inject lateinit var firebase: Firebase
protected abstract val defaultIcon: String
protected var selectedColor = 0
protected var selectedIcon = mutableStateOf<String?>(null)
@ -62,7 +86,10 @@ abstract class BaseListSettingsActivity : AppCompatActivity(), ColorPalettePicke
protected abstract fun hasChanges(): Boolean
protected abstract suspend fun save()
protected abstract val isNew: Boolean
protected val isNew: Boolean
get() = filter == null
protected abstract val filter: Filter?
protected abstract val toolbarTitle: String?
protected abstract suspend fun delete()
protected open fun discard() {
@ -117,8 +144,8 @@ abstract class BaseListSettingsActivity : AppCompatActivity(), ColorPalettePicke
optionButton: @Composable () -> Unit = {
if (!isNew) DeleteButton(toolbarTitle ?: "") { delete() }
},
extensionContent: @Composable ColumnScope.() -> Unit = {},
fab: @Composable () -> Unit = {},
extensionContent: @Composable ColumnScope.() -> Unit = {},
) {
ListSettingsScaffold(
title = title,
@ -134,12 +161,13 @@ abstract class BaseListSettingsActivity : AppCompatActivity(), ColorPalettePicke
actions = optionButton,
fab = fab,
) {
BaseSettingsContent(
ListSettingsContent(
color = colorState.value,
icon = selectedIcon.value ?: defaultIcon,
text = textState.value,
error = errorState.value,
requestKeyboard = requestKeyboard,
isNew = isNew,
setText = {
textState.value = it
errorState.value = ""
@ -147,14 +175,125 @@ abstract class BaseListSettingsActivity : AppCompatActivity(), ColorPalettePicke
pickColor = { showThemePicker() },
clearColor = { clearColor() },
pickIcon = { showIconPicker() },
addToHome = { createShortcut() },
extensionContent = extensionContent,
)
}
}
protected fun createShortcut() {
filter?.let {
val filterId = defaultFilterProvider.getFilterPreferenceValue(it)
val iconColor = if (colorState.value == Color.Unspecified)
Color(tasksTheme.themeColor.primaryColor)
else
colorState.value
val shortcutInfo = ShortcutInfoCompat.Builder(this, UUIDHelper.newUUID())
.setShortLabel(title)
.setIcon(
selectedIcon.value
?.let { icon ->
createShortcutIcon(
context = this,
backgroundColor = iconColor,
icon = icon,
iconColor = contentColorFor(iconColor.toArgb()),
)
}
?: createShortcutIcon(
this,
backgroundColor = iconColor
)
)
.setIntent(TaskIntents.getTaskListByIdIntent(this, filterId))
.build()
val pinnedShortcutCallbackIntent = ShortcutManagerCompat
.createShortcutResultIntent(this, shortcutInfo)
// Create callback intent
val successCallback = PendingIntent.getBroadcast(
this, 0,
pinnedShortcutCallbackIntent,
PendingIntent.FLAG_IMMUTABLE
)
ShortcutManagerCompat.requestPinShortcut(
this,
shortcutInfo,
successCallback.intentSender
)
firebase.logEvent(R.string.event_create_shortcut, R.string.param_type to "settings_activity")
}
}
companion object {
private const val EXTRA_SELECTED_THEME = "extra_selected_theme"
private const val EXTRA_SELECTED_ICON = "extra_selected_icon"
private const val FRAG_TAG_COLOR_PICKER = "frag_tag_color_picker"
fun createShortcutIcon(context: Context, backgroundColor: Color): IconCompat {
val size = context.resources.getDimensionPixelSize(android.R.dimen.app_icon_size)
val bitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888)
Canvas(bitmap).apply {
// Draw circular background
val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = backgroundColor.toArgb()
}
drawCircle(size/2f, size/2f, size/2f, paint)
// Draw foreground icon
val foreground = ResourcesCompat.getDrawable(
context.resources,
org.tasks.kmp.R.drawable.ic_launcher_no_shadow_foreground,
null
)
foreground?.let {
it.setBounds(0, 0, size, size)
it.draw(this)
}
}
return IconCompat.createWithBitmap(bitmap)
}
fun createShortcutIcon(
context: Context,
backgroundColor: Color,
icon: String,
iconColor: Color = Color.White,
iconSizeDp: Int = 24
): IconCompat {
val size = context.resources.getDimensionPixelSize(android.R.dimen.app_icon_size)
val bitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888)
Canvas(bitmap).apply {
// Draw circular background
val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = backgroundColor.toArgb()
}
drawCircle(size/2f, size/2f, size/2f, paint)
// Create and draw IconicsDrawable
val drawable = IconicsDrawable(context, OutlinedGoogleMaterial.getIcon("gmo_$icon")).apply {
colorInt = iconColor.toArgb()
sizeDp = iconSizeDp
}
// Center the icon
val iconSize = (size * 0.5f).toInt()
drawable.setBounds(
(size - iconSize) / 2,
(size - iconSize) / 2,
(size + iconSize) / 2,
(size + iconSize) / 2
)
drawable.draw(this)
}
return IconCompat.createWithBitmap(bitmap)
}
}
}

@ -67,7 +67,7 @@ class FilterSettingsActivity : BaseListSettingsActivity() {
private val newCriterionTypes: MutableState<List<CustomFilterCriterion>?> = mutableStateOf(null)
private val newCriterionOptions: MutableState<CriterionInstance?> = mutableStateOf(null)
private val filter: CustomFilter?
override val filter: CustomFilter?
get() = viewModel.viewState.value.filter
override fun onCreate(savedInstanceState: Bundle?) {
@ -100,9 +100,6 @@ class FilterSettingsActivity : BaseListSettingsActivity() {
}
}
override val isNew: Boolean
get() = viewModel.viewState.value.filter == null
override val toolbarTitle: String
get() = if (isNew) getString(R.string.FLA_new_filter) else viewModel.viewState.value.filter?.title ?: ""

@ -22,6 +22,7 @@ import org.tasks.compose.settings.Toaster
import org.tasks.data.dao.GoogleTaskListDao
import org.tasks.data.entity.CaldavAccount
import org.tasks.data.entity.CaldavCalendar
import org.tasks.filters.Filter
import org.tasks.filters.GtasksFilter
import org.tasks.themes.TasksIcons
import org.tasks.themes.TasksTheme
@ -78,8 +79,8 @@ class GoogleTaskListSettingsActivity : BaseListSettingsActivity() {
updateTheme()
}
override val isNew: Boolean
get() = isNewList
override val filter: Filter?
get() = if (isNewList) null else GtasksFilter(gtasksList)
override val toolbarTitle: String
get() = if (isNew) getString(R.string.new_list) else gtasksList.name!!
@ -131,10 +132,6 @@ class GoogleTaskListSettingsActivity : BaseListSettingsActivity() {
}
}
override fun finish() {
super.finish()
}
override fun promptDelete() {
if (!requestInProgress()) {
super.promptDelete()

@ -34,6 +34,7 @@ import org.tasks.data.dao.LocationDao
import org.tasks.data.entity.Place
import org.tasks.data.mapPosition
import org.tasks.extensions.formatNumber
import org.tasks.filters.Filter
import org.tasks.filters.PlaceFilter
import org.tasks.location.MapFragment
import org.tasks.preferences.Preferences
@ -108,8 +109,8 @@ class PlaceSettingsActivity : BaseListSettingsActivity(),
Text(stringResource(id = R.string.geofence_radius))
Row(horizontalArrangement = Arrangement.End) {
Text(getString(
R.string.location_radius_meters,
locale.formatNumber(sliderPos.floatValue.roundToInt()
R.string.location_radius_meters,
locale.formatNumber(sliderPos.floatValue.roundToInt()
)))
}
}
@ -183,8 +184,8 @@ class PlaceSettingsActivity : BaseListSettingsActivity(),
finish()
}
override val isNew: Boolean
get() = false
override val filter: Filter
get() = PlaceFilter(place)
override val toolbarTitle: String
get() = place.address ?: place.displayName

@ -18,6 +18,7 @@ import org.tasks.Strings.isNullOrEmpty
import org.tasks.data.dao.TagDao
import org.tasks.data.dao.TagDataDao
import org.tasks.data.entity.TagData
import org.tasks.filters.Filter
import org.tasks.filters.TagFilter
import org.tasks.themes.TasksIcons
import org.tasks.themes.TasksTheme
@ -55,8 +56,8 @@ class TagSettingsActivity : BaseListSettingsActivity() {
updateTheme()
}
override val isNew: Boolean
get() = isNewTag
override val filter: Filter?
get() = if (isNewTag) null else TagFilter(tagData)
override val toolbarTitle: String
get() = if (isNew) getString(R.string.new_tag) else tagData.name!!

@ -23,6 +23,7 @@ import org.tasks.data.dao.CaldavDao
import org.tasks.data.entity.CaldavAccount
import org.tasks.data.entity.CaldavCalendar
import org.tasks.filters.CaldavFilter
import org.tasks.filters.Filter
import org.tasks.themes.TasksIcons
import org.tasks.ui.DisplayableException
import java.net.ConnectException
@ -58,8 +59,8 @@ abstract class BaseCaldavCalendarSettingsActivity : BaseListSettingsActivity() {
updateTheme()
}
override val isNew: Boolean
get() = caldavCalendar == null
override val filter: Filter?
get() = caldavCalendar?.let { CaldavFilter(it) }
override val toolbarTitle: String
get() = if (isNew) getString(R.string.new_list) else caldavCalendar!!.name ?: ""

@ -0,0 +1,64 @@
package org.tasks.compose.settings
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Home
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalInspectionMode
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.core.content.pm.ShortcutManagerCompat
import org.tasks.R
import org.tasks.compose.Constants
import org.tasks.kmp.org.tasks.compose.settings.SettingRow
import org.tasks.themes.TasksTheme
@Composable
fun AddToHomeRow(onClick: () -> Unit) {
val context = LocalContext.current
val isRequestPinShortcutSupported = LocalInspectionMode.current || remember {
ShortcutManagerCompat.isRequestPinShortcutSupported(context)
}
if (isRequestPinShortcutSupported) {
SettingRow(
modifier = Modifier.clickable(onClick = onClick),
left = {
Box(
modifier = Modifier.size(48.dp),
contentAlignment = Alignment.Center
) {
Icon(
imageVector = Icons.Outlined.Home,
contentDescription = null,
tint = colorResource(R.color.icon_tint_with_alpha),
)
}
},
center = {
Text(
text = stringResource(R.string.add_to_home_screen),
modifier = Modifier.padding(start = Constants.KEYLINE_FIRST)
)
}
)
}
}
@Composable
@Preview(showBackground = true)
private fun AddToHomePreview() {
TasksTheme {
AddToHomeRow(onClick = {})
}
}

@ -1,47 +0,0 @@
package org.tasks.compose.settings
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import org.tasks.compose.Constants
@Composable
fun BaseSettingsContent(
color: Color,
icon: String,
text: String,
error: String,
requestKeyboard: Boolean,
setText: (String) -> Unit,
pickColor: () -> Unit,
clearColor: () -> Unit,
pickIcon: () -> Unit,
extensionContent: @Composable ColumnScope.() -> Unit,
) {
Column(
modifier = Modifier
.fillMaxSize(),
) {
TitleInput(
text = text,
error = error,
requestKeyboard = requestKeyboard,
modifier = Modifier.padding(horizontal = Constants.KEYLINE_FIRST),
setText = { setText(it) },
)
SelectColorRow(
color = color,
selectColor = { pickColor() },
clearColor = { clearColor() },
)
SelectIconRow(
icon = icon,
selectIcon = { pickIcon() },
)
extensionContent()
}
}

@ -0,0 +1,48 @@
package org.tasks.compose.settings
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import org.tasks.compose.Constants
@Composable
fun ColumnScope.ListSettingsContent(
color: Color,
icon: String,
text: String,
error: String,
requestKeyboard: Boolean,
isNew: Boolean,
setText: (String) -> Unit,
pickColor: () -> Unit,
clearColor: () -> Unit,
pickIcon: () -> Unit,
addToHome: () -> Unit,
extensionContent: @Composable ColumnScope.() -> Unit,
) {
TitleInput(
text = text,
error = error,
requestKeyboard = requestKeyboard,
modifier = Modifier.padding(horizontal = Constants.KEYLINE_FIRST),
setText = { setText(it) },
)
SelectColorRow(
color = color,
selectColor = { pickColor() },
clearColor = { clearColor() },
)
SelectIconRow(
icon = icon,
selectIcon = { pickIcon() },
)
if (!isNew) {
// TODO: support this for new filters too
AddToHomeRow(
onClick = { addToHome() },
)
}
extensionContent()
}

@ -3,9 +3,12 @@ package org.tasks.compose.settings
import androidx.activity.SystemBarStyle
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Save
import androidx.compose.material3.ExperimentalMaterial3Api
@ -21,6 +24,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import org.tasks.R
import org.tasks.extensions.Context.findActivity
import org.tasks.themes.colorOn
@ -37,7 +41,7 @@ fun ListSettingsScaffold(
discard: () -> Unit,
actions: @Composable () -> Unit = {},
fab: @Composable () -> Unit = {},
content: @Composable () -> Unit,
content: @Composable ColumnScope.() -> Unit,
) {
Scaffold(
topBar = {
@ -63,7 +67,11 @@ fun ListSettingsScaffold(
actionIconContentColor = contentColor,
),
title = {
Text(text = title)
Text(
text = title,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
},
navigationIcon = {
IconButton(
@ -84,7 +92,11 @@ fun ListSettingsScaffold(
},
floatingActionButton = { fab() },
) { paddingValues ->
Box(modifier = Modifier.padding(paddingValues)) {
Column(modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
.verticalScroll(rememberScrollState()),
) {
content()
}
PromptAction(

@ -7,6 +7,7 @@ import androidx.lifecycle.lifecycleScope
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch
import org.tasks.caldav.BaseCaldavCalendarSettingsActivity
import org.tasks.compose.settings.AddToHomeRow
import org.tasks.compose.settings.ListSettingsScaffold
import org.tasks.compose.settings.SelectIconRow
import org.tasks.compose.settings.Toaster
@ -35,6 +36,7 @@ class OpenTasksListSettingsActivity : BaseCaldavCalendarSettingsActivity() {
discard = { finish() },
) {
SelectIconRow(icon = selectedIcon.value?: defaultIcon) { showIconPicker() }
AddToHomeRow(onClick = { createShortcut() })
}
Toaster(state = snackbar)
}

@ -424,6 +424,7 @@
<string name="event_add_task">add_task</string>
<string name="event_complete_task">complete_task</string>
<string name="event_request_review">request_review</string>
<string name="event_create_shortcut">create_shortcut</string>
<string name="param_type">type</string>
<string name="p_map_theme">map_theme</string>
<string name="p_picker_mode_date">picker_mode_date</string>

@ -734,4 +734,5 @@ File %1$s contained %2$s.\n\n
<string name="sort_ascending">Ascending</string>
<string name="sort_descending">Descending</string>
<string name="sort_not_available">Not available for tags, filters, or places</string>
<string name="add_to_home_screen">Add to home screen</string>
</resources>

@ -1,9 +1,19 @@
package org.tasks.themes
import androidx.compose.ui.graphics.Color
import kotlin.math.max
import kotlin.math.min
import kotlin.math.pow
fun contentColorFor(backgroundColor: Int): Color =
if (backgroundColor == 0) {
Color.White
} else if (calculateContrast(WHITE, backgroundColor) < 3) {
Color.Black
} else {
Color.White
}
fun calculateContrast(foreground: Int, background: Int): Double {
var foreground = foreground
require(alpha(background) == 255) {

@ -41,16 +41,7 @@ private val wallpaperScheme = darkColorScheme.copy(
fun colorOn(color: Color) = colorOn(color.toArgb())
@Composable
fun colorOn(color: Int) =
remember (color) {
if (color == 0) {
Color.White
} else if (calculateContrast(WHITE, color) < 3) {
Color.Black
} else {
Color.White
}
}
fun colorOn(color: Int) = remember (color) { contentColorFor(color) }
@Composable
fun TasksTheme(

Loading…
Cancel
Save