Added a donation popup to our application.

* The donation popup will be shown to the user every three months.
* If the user clicks the "Later" button, the popup will appear again after 3 days.
* The donation popup will only be shown when there is at least one book available in the library. If no ZIM file is present, it’s not ideal to ask for a donation, as the user has not yet used the application.
* The donation popup will only be shown for custom apps when the support_url is configured. If the support menu item is hidden in the sidebar (a feature we offer), the donation popup will not be displayed, as there is no support_url available for that custom app.
This commit is contained in:
MohitMaliFtechiz 2024-09-05 19:05:21 +05:30 committed by MohitMaliFtechiz
parent 681e3a04db
commit e55deb851f
8 changed files with 285 additions and 2 deletions

View File

@ -287,7 +287,7 @@ abstract class CoreMainActivity : BaseActivity(), WebViewProvider {
drawerContainerLayout.closeDrawer(drawerNavView) drawerContainerLayout.closeDrawer(drawerNavView)
} }
private fun openSupportKiwixExternalLink() { fun openSupportKiwixExternalLink() {
externalLinkOpener.openExternalUrl(KIWIX_SUPPORT_URL.toUri().browserIntent()) externalLinkOpener.openExternalUrl(KIWIX_SUPPORT_URL.toUri().browserIntent())
} }

View File

@ -46,6 +46,7 @@ import android.view.MenuInflater
import android.view.MenuItem import android.view.MenuItem
import android.view.MotionEvent import android.view.MotionEvent
import android.view.View import android.view.View
import android.view.View.GONE
import android.view.View.VISIBLE import android.view.View.VISIBLE
import android.view.ViewGroup import android.view.ViewGroup
import android.view.animation.AnimationUtils import android.view.animation.AnimationUtils
@ -133,6 +134,8 @@ import org.kiwix.kiwixmobile.core.reader.ZimReaderSource
import org.kiwix.kiwixmobile.core.search.viewmodel.effects.SearchItemToOpen import org.kiwix.kiwixmobile.core.search.viewmodel.effects.SearchItemToOpen
import org.kiwix.kiwixmobile.core.utils.AnimationUtils.rotate import org.kiwix.kiwixmobile.core.utils.AnimationUtils.rotate
import org.kiwix.kiwixmobile.core.utils.DimenUtils.getToolbarHeight import org.kiwix.kiwixmobile.core.utils.DimenUtils.getToolbarHeight
import org.kiwix.kiwixmobile.core.utils.DonationDialogHandler
import org.kiwix.kiwixmobile.core.utils.DonationDialogHandler.ShowDonationDialogCallback
import org.kiwix.kiwixmobile.core.utils.ExternalLinkOpener import org.kiwix.kiwixmobile.core.utils.ExternalLinkOpener
import org.kiwix.kiwixmobile.core.utils.LanguageUtils import org.kiwix.kiwixmobile.core.utils.LanguageUtils
import org.kiwix.kiwixmobile.core.utils.LanguageUtils.Companion.getCurrentLocale import org.kiwix.kiwixmobile.core.utils.LanguageUtils.Companion.getCurrentLocale
@ -173,7 +176,8 @@ abstract class CoreReaderFragment :
FragmentActivityExtensions, FragmentActivityExtensions,
WebViewProvider, WebViewProvider,
ReadAloudCallbacks, ReadAloudCallbacks,
NavigationHistoryClickListener { NavigationHistoryClickListener,
ShowDonationDialogCallback {
protected val webViewList: MutableList<KiwixWebView> = ArrayList() protected val webViewList: MutableList<KiwixWebView> = ArrayList()
private val webUrlsProcessor = BehaviorProcessor.create<String>() private val webUrlsProcessor = BehaviorProcessor.create<String>()
private var fragmentReaderBinding: FragmentReaderBinding? = null private var fragmentReaderBinding: FragmentReaderBinding? = null
@ -227,6 +231,10 @@ abstract class CoreReaderFragment :
@Inject @Inject
var alertDialogShower: DialogShower? = null var alertDialogShower: DialogShower? = null
@JvmField
@Inject
var donationDialogHandler: DonationDialogHandler? = null
@JvmField @JvmField
@Inject @Inject
var painter: DarkModeViewPainter? = null var painter: DarkModeViewPainter? = null
@ -297,6 +305,7 @@ abstract class CoreReaderFragment :
private var tableDrawerAdapter: TableDrawerAdapter? = null private var tableDrawerAdapter: TableDrawerAdapter? = null
private var tableDrawerRight: RecyclerView? = null private var tableDrawerRight: RecyclerView? = null
private var tabCallback: ItemTouchHelper.Callback? = null private var tabCallback: ItemTouchHelper.Callback? = null
private var donationLayout: FrameLayout? = null
private var bookmarkingDisposable: Disposable? = null private var bookmarkingDisposable: Disposable? = null
private var isBookmarked = false private var isBookmarked = false
private lateinit var serviceConnection: ServiceConnection private lateinit var serviceConnection: ServiceConnection
@ -389,6 +398,7 @@ abstract class CoreReaderFragment :
) { ) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
setupMenu() setupMenu()
donationDialogHandler?.showDonationDialogCallBack(this)
val activity = requireActivity() as AppCompatActivity? val activity = requireActivity() as AppCompatActivity?
activity?.let { activity?.let {
WebView(it).destroy() // Workaround for buggy webViews see #710 WebView(it).destroy() // Workaround for buggy webViews see #710
@ -511,6 +521,7 @@ abstract class CoreReaderFragment :
tabRecyclerView = findViewById(R.id.tab_switcher_recycler_view) tabRecyclerView = findViewById(R.id.tab_switcher_recycler_view)
snackBarRoot = findViewById(R.id.snackbar_root) snackBarRoot = findViewById(R.id.snackbar_root)
bottomToolbarToc = findViewById(R.id.bottom_toolbar_toc) bottomToolbarToc = findViewById(R.id.bottom_toolbar_toc)
donationLayout = findViewById(R.id.donation_layout)
} }
} }
} }
@ -1213,6 +1224,8 @@ abstract class CoreReaderFragment :
unRegisterReadAloudService() unRegisterReadAloudService()
storagePermissionForNotesLauncher?.unregister() storagePermissionForNotesLauncher?.unregister()
storagePermissionForNotesLauncher = null storagePermissionForNotesLauncher = null
donationDialogHandler?.showDonationDialogCallBack(null)
donationDialogHandler = null
} }
private fun unBindViewsAndBinding() { private fun unBindViewsAndBinding() {
@ -1243,6 +1256,8 @@ abstract class CoreReaderFragment :
closeAllTabsButton = null closeAllTabsButton = null
tableDrawerRightContainer = null tableDrawerRightContainer = null
fragmentReaderBinding = null fragmentReaderBinding = null
donationLayout?.removeAllViews()
donationLayout = null
} }
private fun updateTableOfContents() { private fun updateTableOfContents() {
@ -1846,6 +1861,53 @@ abstract class CoreReaderFragment :
if (tts == null) { if (tts == null) {
setUpTTS() setUpTTS()
} }
donationDialogHandler?.attemptToShowDonationPopup()
}
@Suppress("InflateParams", "MagicNumber")
protected open fun showDonationLayout() {
val donationCardView = layoutInflater.inflate(R.layout.layout_donation_bottom_sheet, null)
val layoutParams = FrameLayout.LayoutParams(
FrameLayout.LayoutParams.MATCH_PARENT,
FrameLayout.LayoutParams.WRAP_CONTENT
)
val bottomToolBarHeight =
requireActivity()
.findViewById<BottomAppBar>(org.kiwix.kiwixmobile.core.R.id.bottom_toolbar).measuredHeight
layoutParams.setMargins(16, 0, 16, bottomToolBarHeight + 10)
donationCardView.layoutParams = layoutParams
donationLayout?.apply {
removeAllViews()
addView(donationCardView)
setDonationLayoutVisibility(VISIBLE)
}
donationCardView.findViewById<TextView>(R.id.descriptionText).apply {
text = getString(
R.string.donation_dialog_description,
(requireActivity() as CoreMainActivity).appName
)
}
val donateButton: TextView = donationCardView.findViewById(R.id.donateButton)
donateButton.setOnClickListener {
donationDialogHandler?.updateLastDonationPopupShownTime()
setDonationLayoutVisibility(GONE)
openKiwixSupportUrl()
}
val laterButton: TextView = donationCardView.findViewById(R.id.laterButton)
laterButton.setOnClickListener {
donationDialogHandler?.donateLater()
setDonationLayoutVisibility(GONE)
}
}
protected open fun openKiwixSupportUrl() {
(requireActivity() as CoreMainActivity).openSupportKiwixExternalLink()
}
private fun setDonationLayoutVisibility(visibility: Int) {
donationLayout?.visibility = visibility
} }
private fun openFullScreenIfEnabled() { private fun openFullScreenIfEnabled() {
@ -2388,6 +2450,10 @@ abstract class CoreReaderFragment :
unbindService() unbindService()
} }
override fun showDonationDialog() {
showDonationLayout()
}
private fun bindService() { private fun bindService() {
requireActivity().bindService( requireActivity().bindService(
Intent(requireActivity(), ReadAloudService::class.java), serviceConnection, Intent(requireActivity(), ReadAloudService::class.java), serviceConnection,

View File

@ -0,0 +1,82 @@
/*
* Kiwix Android
* Copyright (c) 2024 Kiwix <android.kiwix.org>
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
package org.kiwix.kiwixmobile.core.utils
import android.app.Activity
import org.kiwix.kiwixmobile.core.dao.NewBookDao
import org.kiwix.kiwixmobile.core.extensions.ActivityExtensions.isCustomApp
import javax.inject.Inject
const val THREE_DAYS_IN_MILLISECONDS = 3 * 24 * 60 * 60 * 1000L
const val THREE_MONTHS_IN_MILLISECONDS = 90 * 24 * 60 * 60 * 1000L
class DonationDialogHandler @Inject constructor(
private val activity: Activity,
private val sharedPreferenceUtil: SharedPreferenceUtil,
private val newBookDao: NewBookDao
) {
private var showDonationDialogCallback: ShowDonationDialogCallback? = null
fun showDonationDialogCallBack(showDonationDialogCallback: ShowDonationDialogCallback?) {
this.showDonationDialogCallback = showDonationDialogCallback
}
fun attemptToShowDonationPopup() {
val currentMilliSeconds = System.currentTimeMillis()
val lastPopupMillis = sharedPreferenceUtil.lastDonationPopupShownInMilliSeconds
val timeDifference = currentMilliSeconds - lastPopupMillis
if (shouldShowInitialPopup(lastPopupMillis) || timeDifference >= THREE_MONTHS_IN_MILLISECONDS) {
if (isZimFilesAvailableInLibrary() && isTimeToShowDonation(currentMilliSeconds)) {
showDonationDialogCallback?.showDonationDialog()
resetDonateLater()
}
}
}
private fun shouldShowInitialPopup(lastPopupMillis: Long): Boolean = lastPopupMillis == 0L
private fun isTimeToShowDonation(currentMillis: Long): Boolean =
isLaterNotClicked() || isLaterPeriodOver(currentMillis)
private fun isLaterNotClicked(): Boolean = sharedPreferenceUtil.laterClickedMilliSeconds == 0L
private fun isLaterPeriodOver(currentMillis: Long): Boolean {
val timeDifference = currentMillis - sharedPreferenceUtil.laterClickedMilliSeconds
return timeDifference >= THREE_DAYS_IN_MILLISECONDS
}
private fun isZimFilesAvailableInLibrary(): Boolean =
if (activity.isCustomApp()) true else newBookDao.getBooks().isNotEmpty()
fun updateLastDonationPopupShownTime() {
sharedPreferenceUtil.lastDonationPopupShownInMilliSeconds = System.currentTimeMillis()
}
fun donateLater() {
sharedPreferenceUtil.laterClickedMilliSeconds = System.currentTimeMillis()
}
private fun resetDonateLater() {
sharedPreferenceUtil.laterClickedMilliSeconds = 0L
}
interface ShowDonationDialogCallback {
fun showDonationDialog()
}
}

View File

@ -271,6 +271,22 @@ class SharedPreferenceUtil @Inject constructor(val context: Context) {
} }
} }
var lastDonationPopupShownInMilliSeconds: Long
get() = sharedPreferences.getLong(PREF_LAST_DONATION_POPUP_SHOWN_IN_MILLISECONDS, 0L)
set(value) {
sharedPreferences.edit {
putLong(PREF_LAST_DONATION_POPUP_SHOWN_IN_MILLISECONDS, value)
}
}
var laterClickedMilliSeconds: Long
get() = sharedPreferences.getLong(PREF_LATER_CLICKED_MILLIS, 0L)
set(value) {
sharedPreferences.edit {
putLong(PREF_LATER_CLICKED_MILLIS, value)
}
}
fun getPublicDirectoryPath(path: String): String = fun getPublicDirectoryPath(path: String): String =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
path path
@ -321,5 +337,8 @@ class SharedPreferenceUtil @Inject constructor(val context: Context) {
const val PREF_NOTES_MIGRATED = "pref_notes_migrated" const val PREF_NOTES_MIGRATED = "pref_notes_migrated"
const val PREF_APP_DIRECTORY_TO_PUBLIC_MIGRATED = "pref_app_directory_to_public_migrated" const val PREF_APP_DIRECTORY_TO_PUBLIC_MIGRATED = "pref_app_directory_to_public_migrated"
const val PREF_COPY_MOVE_PERMISSION = "pref_copy_move_permission" const val PREF_COPY_MOVE_PERMISSION = "pref_copy_move_permission"
private const val PREF_LATER_CLICKED_MILLIS = "pref_later_clicked_millis"
private const val PREF_LAST_DONATION_POPUP_SHOWN_IN_MILLISECONDS =
"pref_last_donation_shown_in_milliseconds"
} }
} }

View File

@ -57,6 +57,14 @@
android:layout_height="match_parent" android:layout_height="match_parent"
android:background="@android:color/black" android:background="@android:color/black"
android:visibility="invisible" /> android:visibility="invisible" />
<FrameLayout
android:id="@+id/donation_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="visible"
app:layout_constraintBottom_toBottomOf="parent"
android:layout_margin="@dimen/activity_horizontal_margin" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>
</androidx.drawerlayout.widget.DrawerLayout> </androidx.drawerlayout.widget.DrawerLayout>

View File

@ -0,0 +1,87 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Kiwix Android
~ Copyright (c) 2024 Kiwix <android.kiwix.org>
~ This program is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ (at your option) any later version.
~
~ This program is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with this program. If not, see <http://www.gnu.org/licenses/>.
~
-->
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:padding="16dp"
app:cardCornerRadius="12dp"
app:cardElevation="6dp">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/heart_icon"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_alignParentStart="true"
android:layout_margin="@dimen/activity_horizontal_margin"
android:layout_marginEnd="12dp"
android:contentDescription="@string/donation_dialog_title"
android:src="@drawable/ic_support_24px"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/titleText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/fullscreen_control_button_margin"
android:text="@string/donation_dialog_title"
android:textSize="16sp"
android:textAppearance="@style/TextAppearance.M3.Sys.Typescale.TitleMedium"
android:textColor="@color/mine_shaft_gray900"
app:layout_constraintStart_toEndOf="@id/heart_icon"
app:layout_constraintTop_toTopOf="@+id/heart_icon" />
<TextView
android:id="@+id/descriptionText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text="@string/donation_dialog_description"
android:textSize="14sp"
app:layout_constraintStart_toStartOf="@id/titleText"
app:layout_constraintTop_toBottomOf="@+id/titleText" />
<TextView
android:id="@+id/laterButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="@dimen/activity_horizontal_margin"
android:text="@string/rate_dialog_neutral"
android:textColor="@color/denim_blue800"
app:layout_constraintEnd_toStartOf="@+id/donateButton"
app:layout_constraintTop_toBottomOf="@id/descriptionText" />
<TextView
android:id="@+id/donateButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="@dimen/activity_horizontal_margin"
android:text="@string/make_donation"
android:textColor="@color/denim_blue800"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/descriptionText" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.cardview.widget.CardView>

View File

@ -389,4 +389,7 @@
<string name="toolbar_back_button_content_description">Go to previous screen</string> <string name="toolbar_back_button_content_description">Go to previous screen</string>
<string name="save_or_open_unsupported_files_dialog_title">Save or Open this file?</string> <string name="save_or_open_unsupported_files_dialog_title">Save or Open this file?</string>
<string name="save_or_open_unsupported_files_dialog_message">Choosing Open will open this file in external reader application.</string> <string name="save_or_open_unsupported_files_dialog_message">Choosing Open will open this file in external reader application.</string>
<string name="donation_dialog_title">Donate Today</string>
<string name="donation_dialog_description">%s needs your help.</string>
<string name="make_donation">Make a donation</string>
</resources> </resources>

View File

@ -28,10 +28,12 @@ import android.view.View.VISIBLE
import android.widget.ImageView import android.widget.ImageView
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.Toolbar import androidx.appcompat.widget.Toolbar
import androidx.core.net.toUri
import androidx.drawerlayout.widget.DrawerLayout import androidx.drawerlayout.widget.DrawerLayout
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import org.kiwix.kiwixmobile.core.R.dimen import org.kiwix.kiwixmobile.core.R.dimen
import org.kiwix.kiwixmobile.core.base.BaseActivity import org.kiwix.kiwixmobile.core.base.BaseActivity
import org.kiwix.kiwixmobile.core.extensions.browserIntent
import org.kiwix.kiwixmobile.core.extensions.getResizedDrawable import org.kiwix.kiwixmobile.core.extensions.getResizedDrawable
import org.kiwix.kiwixmobile.core.extensions.isFileExist import org.kiwix.kiwixmobile.core.extensions.isFileExist
import org.kiwix.kiwixmobile.core.main.CoreReaderFragment import org.kiwix.kiwixmobile.core.main.CoreReaderFragment
@ -328,6 +330,22 @@ class CustomReaderFragment : CoreReaderFragment() {
newMainPageTab() newMainPageTab()
} }
/**
* Overrides the method to show the donation popup. When the "Support url" is disabled
* in a custom app, this function stop to show the donationPopup.
*/
override fun showDonationLayout() {
if (BuildConfig.SUPPORT_URL.isNotEmpty()) {
super.showDonationLayout()
}
}
override fun openKiwixSupportUrl() {
if (BuildConfig.SUPPORT_URL.isNotEmpty()) {
openExternalUrl(BuildConfig.SUPPORT_URL.toUri().browserIntent())
}
}
override fun onDestroyView() { override fun onDestroyView() {
super.onDestroyView() super.onDestroyView()
permissionRequiredDialog = null permissionRequiredDialog = null