Beg for subscriptions/donations periodically

pull/1823/head
Alex Baker 4 years ago
parent ed48ab15e3
commit d5ccc1aa8f

@ -66,6 +66,9 @@ class Firebase @Inject constructor(
val reviewCooldown: Boolean val reviewCooldown: Boolean
get() = preferences.lastReviewRequest + days("review_cooldown", 30L) > now() get() = preferences.lastReviewRequest + days("review_cooldown", 30L) > now()
val subscribeCooldown: Boolean
get() = preferences.lastSubscribeRequest + days("subscribe_cooldown", 30L) > now()
private fun days(key: String, default: Long): Long = private fun days(key: String, default: Long): Long =
TimeUnit.DAYS.toMillis(remoteConfig?.getLong(key) ?: default) TimeUnit.DAYS.toMillis(remoteConfig?.getLong(key) ?: default)

@ -9,4 +9,8 @@
<key>install_cooldown</key> <key>install_cooldown</key>
<value>14</value> <value>14</value>
</entry> </entry>
<entry>
<key>subscribe_cooldown</key>
<value>30</value>
</entry>
</defaultsMap> </defaultsMap>

@ -13,9 +13,7 @@ import android.os.Bundle
import android.view.View import android.view.View
import android.view.inputmethod.InputMethodManager import android.view.inputmethod.InputMethodManager
import androidx.appcompat.view.ActionMode import androidx.appcompat.view.ActionMode
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import com.todoroo.andlib.utility.AndroidUtilities import com.todoroo.andlib.utility.AndroidUtilities
import com.todoroo.astrid.activity.TaskEditFragment.Companion.newTaskEditFragment import com.todoroo.astrid.activity.TaskEditFragment.Companion.newTaskEditFragment
import com.todoroo.astrid.activity.TaskListFragment.TaskListFragmentCallbackHandler import com.todoroo.astrid.activity.TaskListFragment.TaskListFragmentCallbackHandler
@ -28,13 +26,15 @@ import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.tasks.BuildConfig import org.tasks.BuildConfig
import org.tasks.LocalBroadcastManager import org.tasks.LocalBroadcastManager
import org.tasks.R import org.tasks.R
import org.tasks.activities.TagSettingsActivity import org.tasks.activities.TagSettingsActivity
import org.tasks.analytics.Firebase
import org.tasks.billing.Inventory import org.tasks.billing.Inventory
import org.tasks.data.AlarmDao import org.tasks.data.AlarmDao
import org.tasks.data.LocationDao import org.tasks.data.LocationDao
@ -61,6 +61,8 @@ import org.tasks.ui.MainActivityEvent
import org.tasks.ui.MainActivityEventBus import org.tasks.ui.MainActivityEventBus
import org.tasks.ui.NavigationDrawerFragment import org.tasks.ui.NavigationDrawerFragment
import org.tasks.ui.NavigationDrawerFragment.Companion.newNavigationDrawer import org.tasks.ui.NavigationDrawerFragment.Companion.newNavigationDrawer
import org.tasks.ui.TaskListEvent
import org.tasks.ui.TaskListEventBus
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
@ -78,7 +80,9 @@ class MainActivity : InjectingAppCompatActivity(), TaskListFragmentCallbackHandl
@Inject lateinit var tagDataDao: TagDataDao @Inject lateinit var tagDataDao: TagDataDao
@Inject lateinit var alarmDao: AlarmDao @Inject lateinit var alarmDao: AlarmDao
@Inject lateinit var eventBus: MainActivityEventBus @Inject lateinit var eventBus: MainActivityEventBus
@Inject lateinit var taskListEventBus: TaskListEventBus
@Inject lateinit var playServices: PlayServices @Inject lateinit var playServices: PlayServices
@Inject lateinit var firebase: Firebase
private var currentNightMode = 0 private var currentNightMode = 0
private var currentPro = false private var currentPro = false
@ -101,11 +105,9 @@ class MainActivity : InjectingAppCompatActivity(), TaskListFragmentCallbackHandl
} }
handleIntent() handleIntent()
lifecycleScope.launch { eventBus
repeatOnLifecycle(Lifecycle.State.RESUMED) { .onEach(this::process)
eventBus.collect(this@MainActivity::process) .launchIn(lifecycleScope)
}
}
} }
private suspend fun process(event: MainActivityEvent) = when (event) { private suspend fun process(event: MainActivityEvent) = when (event) {
@ -477,6 +479,16 @@ class MainActivity : InjectingAppCompatActivity(), TaskListFragmentCallbackHandl
taskEditFragment!!.onRemoteListChanged(filter) taskEditFragment!!.onRemoteListChanged(filter)
} }
override fun onStart() {
super.onStart()
lifecycleScope.launch {
if (!inventory.hasPro && !firebase.subscribeCooldown) {
taskListEventBus.tryEmit(TaskListEvent.BegForSubscription)
}
}
}
companion object { companion object {
/** For indicating the new list screen should be launched at fragment setup time */ /** For indicating the new list screen should be launched at fragment setup time */
const val TOKEN_CREATE_NEW_LIST_NAME = "newListName" // $NON-NLS-1$ const val TOKEN_CREATE_NEW_LIST_NAME = "newListName" // $NON-NLS-1$

@ -23,6 +23,9 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.view.ActionMode import androidx.appcompat.view.ActionMode
import androidx.appcompat.widget.SearchView import androidx.appcompat.widget.SearchView
import androidx.appcompat.widget.Toolbar import androidx.appcompat.widget.Toolbar
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.app.ShareCompat import androidx.core.app.ShareCompat
import androidx.core.view.forEach import androidx.core.view.forEach
@ -30,9 +33,7 @@ import androidx.core.view.isVisible
import androidx.core.view.setMargins import androidx.core.view.setMargins
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels import androidx.fragment.app.viewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.paging.PagedList import androidx.paging.PagedList
import androidx.recyclerview.widget.DefaultItemAnimator import androidx.recyclerview.widget.DefaultItemAnimator
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
@ -41,9 +42,11 @@ import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout.OnRefreshListener import androidx.swiperefreshlayout.widget.SwipeRefreshLayout.OnRefreshListener
import com.google.android.material.appbar.AppBarLayout import com.google.android.material.appbar.AppBarLayout
import com.google.android.material.bottomappbar.BottomAppBar import com.google.android.material.bottomappbar.BottomAppBar
import com.google.android.material.composethemeadapter.MdcTheme
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import com.todoroo.andlib.utility.AndroidUtilities import com.todoroo.andlib.utility.AndroidUtilities
import com.todoroo.andlib.utility.DateUtilities import com.todoroo.andlib.utility.DateUtilities
import com.todoroo.andlib.utility.DateUtilities.now
import com.todoroo.astrid.adapter.TaskAdapter import com.todoroo.astrid.adapter.TaskAdapter
import com.todoroo.astrid.adapter.TaskAdapterProvider import com.todoroo.astrid.adapter.TaskAdapterProvider
import com.todoroo.astrid.api.AstridApiConstants.EXTRAS_OLD_DUE_DATE import com.todoroo.astrid.api.AstridApiConstants.EXTRAS_OLD_DUE_DATE
@ -69,12 +72,14 @@ import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.tasks.LocalBroadcastManager import org.tasks.LocalBroadcastManager
import org.tasks.R import org.tasks.R
import org.tasks.ShortcutManager import org.tasks.ShortcutManager
import org.tasks.Tasks.Companion.IS_GOOGLE_PLAY
import org.tasks.activities.FilterSettingsActivity import org.tasks.activities.FilterSettingsActivity
import org.tasks.activities.GoogleTaskListSettingsActivity import org.tasks.activities.GoogleTaskListSettingsActivity
import org.tasks.activities.ListPicker import org.tasks.activities.ListPicker
@ -82,7 +87,9 @@ import org.tasks.activities.ListPicker.Companion.newListPicker
import org.tasks.activities.PlaceSettingsActivity import org.tasks.activities.PlaceSettingsActivity
import org.tasks.activities.TagSettingsActivity import org.tasks.activities.TagSettingsActivity
import org.tasks.analytics.Firebase import org.tasks.analytics.Firebase
import org.tasks.billing.PurchaseActivity
import org.tasks.caldav.BaseCaldavCalendarSettingsActivity import org.tasks.caldav.BaseCaldavCalendarSettingsActivity
import org.tasks.compose.AnimatedBanner
import org.tasks.data.CaldavDao import org.tasks.data.CaldavDao
import org.tasks.data.TagDataDao import org.tasks.data.TagDataDao
import org.tasks.data.TaskContainer import org.tasks.data.TaskContainer
@ -169,6 +176,7 @@ class TaskListFragment : Fragment(), OnRefreshListener, Toolbar.OnMenuItemClickL
private lateinit var callbacks: TaskListFragmentCallbackHandler private lateinit var callbacks: TaskListFragmentCallbackHandler
private lateinit var binding: FragmentTaskListBinding private lateinit var binding: FragmentTaskListBinding
@OptIn(ExperimentalAnimationApi::class)
private fun process(event: TaskListEvent) = when (event) { private fun process(event: TaskListEvent) = when (event) {
is TaskListEvent.TaskCreated -> is TaskListEvent.TaskCreated ->
onTaskCreated(event.uuid) onTaskCreated(event.uuid)
@ -176,6 +184,33 @@ class TaskListFragment : Fragment(), OnRefreshListener, Toolbar.OnMenuItemClickL
makeSnackbar(R.string.calendar_event_created, event.title) makeSnackbar(R.string.calendar_event_created, event.title)
?.setAction(R.string.action_open) { context?.openUri(event.uri) } ?.setAction(R.string.action_open) { context?.openUri(event.uri) }
?.show() ?.show()
is TaskListEvent.BegForSubscription -> {
binding.banner.setContent {
val showBanner = rememberSaveable { mutableStateOf(true) }
MdcTheme {
AnimatedBanner(
isVisible = showBanner,
dismiss = {
preferences.lastSubscribeRequest = now()
showBanner.value = false
},
subscribe = {
purchase()
showBanner.value = false
},
)
}
}
}
}
private fun purchase() {
if (IS_GOOGLE_PLAY) {
startActivity(Intent(context, PurchaseActivity::class.java))
} else {
preferences.lastSubscribeRequest = now()
context?.openUri(R.string.url_donate)
}
} }
override fun onRefresh() { override fun onRefresh() {
@ -217,8 +252,16 @@ class TaskListFragment : Fragment(), OnRefreshListener, Toolbar.OnMenuItemClickL
outState.putLongArray(EXTRA_COLLAPSED, taskAdapter.getCollapsed().toLongArray()) outState.putLongArray(EXTRA_COLLAPSED, taskAdapter.getCollapsed().toLongArray())
} }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
taskListEventBus
.onEach(this::process)
.launchIn(viewLifecycleOwner.lifecycleScope)
}
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
binding = FragmentTaskListBinding.inflate(inflater, container, false) binding = FragmentTaskListBinding.inflate(inflater, container, false)
with (binding) { with (binding) {
swipeRefreshLayout = bodyStandard.swipeLayout swipeRefreshLayout = bodyStandard.swipeLayout
@ -283,12 +326,6 @@ class TaskListFragment : Fragment(), OnRefreshListener, Toolbar.OnMenuItemClickL
toolbar.setNavigationOnClickListener { callbacks.onNavigationIconClicked() } toolbar.setNavigationOnClickListener { callbacks.onNavigationIconClicked() }
setupMenu(toolbar) setupMenu(toolbar)
lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.RESUMED) {
taskListEventBus.collect(this@TaskListFragment::process)
}
}
return binding.root return binding.root
} }

@ -114,6 +114,7 @@ class Upgrader @Inject constructor(
} else { } else {
setInstallDetails(to) setInstallDetails(to)
} }
preferences.lastSubscribeRequest = 0L
preferences.setCurrentVersion(to) preferences.setCurrentVersion(to)
} }

@ -9,6 +9,7 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.Dialog
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.google.android.material.composethemeadapter.MdcTheme import com.google.android.material.composethemeadapter.MdcTheme
import com.todoroo.andlib.utility.DateUtilities
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.tasks.LocalBroadcastManager import org.tasks.LocalBroadcastManager
@ -16,6 +17,7 @@ import org.tasks.Tasks.Companion.IS_GENERIC
import org.tasks.compose.PurchaseText.PurchaseText import org.tasks.compose.PurchaseText.PurchaseText
import org.tasks.extensions.Context.toast import org.tasks.extensions.Context.toast
import org.tasks.injection.InjectingAppCompatActivity import org.tasks.injection.InjectingAppCompatActivity
import org.tasks.preferences.Preferences
import org.tasks.themes.Theme import org.tasks.themes.Theme
import javax.inject.Inject import javax.inject.Inject
@ -25,6 +27,7 @@ class PurchaseActivity : InjectingAppCompatActivity(), OnPurchasesUpdated {
@Inject lateinit var billingClient: BillingClient @Inject lateinit var billingClient: BillingClient
@Inject lateinit var localBroadcastManager: LocalBroadcastManager @Inject lateinit var localBroadcastManager: LocalBroadcastManager
@Inject lateinit var inventory: Inventory @Inject lateinit var inventory: Inventory
@Inject lateinit var preferences: Preferences
private var currentSubscription: Purchase? = null private var currentSubscription: Purchase? = null
private val purchaseReceiver: BroadcastReceiver = object : BroadcastReceiver() { private val purchaseReceiver: BroadcastReceiver = object : BroadcastReceiver() {
@ -38,6 +41,8 @@ class PurchaseActivity : InjectingAppCompatActivity(), OnPurchasesUpdated {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
preferences.lastSubscribeRequest = DateUtilities.now()
val github = intent?.extras?.getBoolean(EXTRA_GITHUB) ?: false val github = intent?.extras?.getBoolean(EXTRA_GITHUB) ?: false
theme.applyToContext(this) theme.applyToContext(this)

@ -0,0 +1,108 @@
package org.tasks.compose
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.animation.expandVertically
import androidx.compose.animation.shrinkVertically
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material.Divider
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.material.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
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.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.delay
import org.tasks.R
import org.tasks.Tasks.Companion.IS_GENERIC
@ExperimentalAnimationApi
@Composable
fun AnimatedBanner(
isVisible: MutableState<Boolean>,
dismiss: () -> Unit,
subscribe: () -> Unit,
) {
var show by rememberSaveable { mutableStateOf(false) }
if (isVisible.value) {
LaunchedEffect(key1 = isVisible, block = {
delay(500)
show = true
})
} else {
show = false
}
AnimatedVisibility(
visible = show,
enter = expandVertically(
expandFrom = Alignment.Top
),
exit = shrinkVertically(),
) {
Column(
modifier = Modifier
.fillMaxWidth(),
) {
Spacer(modifier = Modifier.height(16.dp))
Text(
text = stringResource(
id = if (IS_GENERIC) {
R.string.enjoying_tasks
} else {
R.string.tasks_needs_your_support
}
),
style = MaterialTheme.typography.body1,
modifier = Modifier.padding(horizontal = 16.dp),
)
Spacer(modifier = Modifier.height(4.dp))
Text(
text = stringResource(
id = if (IS_GENERIC) {
R.string.tasks_needs_your_support
} else {
R.string.support_development_subscribe
}
),
style = MaterialTheme.typography.body2,
modifier = Modifier.padding(horizontal = 16.dp),
)
Row(
modifier = Modifier
.padding(vertical = 8.dp, horizontal = 16.dp)
.align(Alignment.End),
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
TextButton(onClick = dismiss) {
Text(text = stringResource(id = R.string.dismiss))
}
TextButton(onClick = subscribe) {
Text(
text = stringResource(
id = if (IS_GENERIC) {
R.string.TLA_menu_donate
} else {
R.string.button_subscribe
}
)
)
}
}
Divider()
}
}
}

@ -536,6 +536,10 @@ class Preferences @JvmOverloads constructor(
get() = getLong(R.string.p_last_review_request, 0L) get() = getLong(R.string.p_last_review_request, 0L)
set(value) = setLong(R.string.p_last_review_request, value) set(value) = setLong(R.string.p_last_review_request, value)
var lastSubscribeRequest: Long
get() = getLong(R.string.p_last_subscribe_request, 0L)
set(value) = setLong(R.string.p_last_subscribe_request, value)
companion object { companion object {
private const val PREF_SORT_SORT = "sort_sort" // $NON-NLS-1$ private const val PREF_SORT_SORT = "sort_sort" // $NON-NLS-1$

@ -7,4 +7,5 @@ typealias TaskListEventBus = MutableSharedFlow<TaskListEvent>
sealed interface TaskListEvent { sealed interface TaskListEvent {
data class TaskCreated(val uuid: String) : TaskListEvent data class TaskCreated(val uuid: String) : TaskListEvent
data class CalendarEventCreated(val title: String?, val uri: String) : TaskListEvent data class CalendarEventCreated(val title: String?, val uri: String) : TaskListEvent
object BegForSubscription : TaskListEvent
} }

@ -38,6 +38,10 @@
android:orientation="vertical" android:orientation="vertical"
app:layout_behavior="@string/appbar_scrolling_view_behavior"> app:layout_behavior="@string/appbar_scrolling_view_behavior">
<androidx.compose.ui.platform.ComposeView
android:id="@+id/banner"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"

@ -411,6 +411,7 @@
<string name="p_show_whats_new">show_whats_new</string> <string name="p_show_whats_new">show_whats_new</string>
<string name="p_just_updated">just_updated</string> <string name="p_just_updated">just_updated</string>
<string name="p_last_review_request">last_review_request</string> <string name="p_last_review_request">last_review_request</string>
<string name="p_last_subscribe_request">last_subscribe_request</string>
<string name="p_backups_enabled">backups_enabled</string> <string name="p_backups_enabled">backups_enabled</string>
<string name="p_backups_ignore_warnings">backups_ignore_warnings</string> <string name="p_backups_ignore_warnings">backups_ignore_warnings</string>
<string name="p_backups_android_backup_enabled">backups_android_backup_enabled</string> <string name="p_backups_android_backup_enabled">backups_android_backup_enabled</string>

@ -733,4 +733,5 @@ File %1$s contained %2$s.\n\n
<string name="caldav_server_other">Other</string> <string name="caldav_server_other">Other</string>
<string name="caldav_server_type">Server type</string> <string name="caldav_server_type">Server type</string>
<string name="filter_snoozed">Snoozed</string> <string name="filter_snoozed">Snoozed</string>
<string name="dismiss">Dismiss</string>
</resources> </resources>

Loading…
Cancel
Save