diff --git a/app/build.gradle b/app/build.gradle
index c54b385d..8ee8c89d 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -87,6 +87,9 @@ dependencies {
// Places
implementation 'com.google.android.libraries.places:places:1.1.0'
+ // Billing / Subscriptions
+ implementation 'com.android.billingclient:billing:1.2.2'
+
// Firebase
implementation 'com.google.firebase:firebase-core:16.0.8'
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 44851fd3..dcb562ca 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -4,6 +4,7 @@
+
+
+
diff --git a/app/src/main/java/net/pokeranalytics/android/PokerAnalyticsApplication.kt b/app/src/main/java/net/pokeranalytics/android/PokerAnalyticsApplication.kt
index 6a1d8b1c..2ed38ca4 100644
--- a/app/src/main/java/net/pokeranalytics/android/PokerAnalyticsApplication.kt
+++ b/app/src/main/java/net/pokeranalytics/android/PokerAnalyticsApplication.kt
@@ -26,6 +26,9 @@ class PokerAnalyticsApplication : Application() {
super.onCreate()
UserDefaults.init(this)
+ // AppGuard / Billing services
+// AppGuard.load(this.applicationContext)
+
// Realm
Realm.init(this)
val realmConfiguration = RealmConfiguration.Builder()
@@ -52,7 +55,7 @@ class PokerAnalyticsApplication : Application() {
if (BuildConfig.DEBUG) {
Timber.d("UserPreferences.defaultCurrency: ${UserDefaults.currency.symbol}")
- this.createFakeSessions()
+// this.createFakeSessions()
}
Patcher.patchBreaks()
diff --git a/app/src/main/java/net/pokeranalytics/android/calculus/bankroll/BankrollReport.kt b/app/src/main/java/net/pokeranalytics/android/calculus/bankroll/BankrollReport.kt
index c37b6b66..33cb6cd8 100644
--- a/app/src/main/java/net/pokeranalytics/android/calculus/bankroll/BankrollReport.kt
+++ b/app/src/main/java/net/pokeranalytics/android/calculus/bankroll/BankrollReport.kt
@@ -203,8 +203,8 @@ class BankrollReport(var setup: BankrollReportSetup) : RowRepresentable {
fun lineDataSet(context: Context): LineDataSet {
val entries = mutableListOf()
- this.evolutionPoints.forEach {
- val entry = Entry(it.date.time.toFloat(), it.value.toFloat(), it.data)
+ this.evolutionPoints.forEachIndexed { index, point ->
+ val entry = Entry(index.toFloat(), point.value.toFloat(), point.data)
entries.add(entry)
}
return DataSetFactory.lineDataSetInstance(entries, "", context)
diff --git a/app/src/main/java/net/pokeranalytics/android/ui/activity/BillingActivity.kt b/app/src/main/java/net/pokeranalytics/android/ui/activity/BillingActivity.kt
new file mode 100644
index 00000000..9a313130
--- /dev/null
+++ b/app/src/main/java/net/pokeranalytics/android/ui/activity/BillingActivity.kt
@@ -0,0 +1,36 @@
+package net.pokeranalytics.android.ui.activity
+
+import android.content.Context
+import android.content.Intent
+import android.os.Bundle
+import net.pokeranalytics.android.R
+import net.pokeranalytics.android.ui.activity.components.PokerAnalyticsActivity
+import net.pokeranalytics.android.util.billing.AppGuard
+
+enum class IAPProducts(var identifier: String) {
+ PRO("pro")
+}
+
+class BillingActivity : PokerAnalyticsActivity() {
+
+ companion object {
+ fun newInstance(context: Context) {
+ val intent = Intent(context, BillingActivity::class.java)
+ context.startActivity(intent)
+ }
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.activity_billing)
+ }
+
+ override fun onResume() {
+ super.onResume()
+
+ AppGuard.restorePurchases()
+ }
+
+}
+
+
diff --git a/app/src/main/java/net/pokeranalytics/android/ui/fragment/SessionFragment.kt b/app/src/main/java/net/pokeranalytics/android/ui/fragment/SessionFragment.kt
index d80eb28b..ed1dc111 100644
--- a/app/src/main/java/net/pokeranalytics/android/ui/fragment/SessionFragment.kt
+++ b/app/src/main/java/net/pokeranalytics/android/ui/fragment/SessionFragment.kt
@@ -40,6 +40,7 @@ class SessionFragment : PokerAnalyticsFragment(), RowRepresentableDelegate {
private val oldRows: ArrayList = ArrayList()
private var sessionHasBeenCustomized = false
private val handler: Handler = Handler()
+
private val refreshTimer: Runnable = object : Runnable {
override fun run() {
// Refresh header each 30 seconds
@@ -263,6 +264,7 @@ class SessionFragment : PokerAnalyticsFragment(), RowRepresentableDelegate {
when (currentSession.getState()) {
SessionState.PENDING, SessionState.PLANNED, SessionState.PAUSED -> {
currentSession.startOrContinue()
+ this.recyclerView.smoothScrollToPosition(0)
}
SessionState.STARTED -> {
currentSession.pause()
diff --git a/app/src/main/java/net/pokeranalytics/android/ui/fragment/SubscriptionFragment.kt b/app/src/main/java/net/pokeranalytics/android/ui/fragment/SubscriptionFragment.kt
new file mode 100644
index 00000000..b7a8cdcc
--- /dev/null
+++ b/app/src/main/java/net/pokeranalytics/android/ui/fragment/SubscriptionFragment.kt
@@ -0,0 +1,68 @@
+package net.pokeranalytics.android.ui.fragment
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.Toast
+import com.android.billingclient.api.BillingClient
+import com.android.billingclient.api.SkuDetails
+import com.android.billingclient.api.SkuDetailsResponseListener
+import kotlinx.android.synthetic.main.fragment_subscription.*
+import net.pokeranalytics.android.R
+import net.pokeranalytics.android.ui.activity.IAPProducts
+import net.pokeranalytics.android.ui.fragment.components.PokerAnalyticsFragment
+import net.pokeranalytics.android.util.billing.AppGuard
+
+class SubscriptionFragment : PokerAnalyticsFragment(), SkuDetailsResponseListener {
+
+ var selectedProduct: SkuDetails? = null
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ if (!AppGuard.requestProducts(this)) {
+ Toast.makeText(requireContext(), R.string.billingclient_unavailable, Toast.LENGTH_LONG).show()
+ }
+
+ }
+
+ override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
+ return inflater.inflate(R.layout.fragment_subscription, container, false)
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+
+ initUI()
+
+ }
+
+ // SkuDetailsResponseListener
+ override fun onSkuDetailsResponse(responseCode: Int, skuDetailsList: MutableList?) {
+
+ if (responseCode == BillingClient.BillingResponse.OK && skuDetailsList != null) {
+ selectedProduct = skuDetailsList.first { it.sku == IAPProducts.PRO.identifier }
+ updatePurchaseButtonState()
+ }
+
+ }
+
+ private fun initUI() {
+
+ this.purchase.setOnClickListener {
+
+ this.selectedProduct?.let {
+ AppGuard.initiatePurchase(this.requireActivity(), it)
+ } ?: run {
+ throw IllegalStateException("Attempt to initiate purchase while no product has been chosen")
+ }
+ }
+
+ }
+
+ private fun updatePurchaseButtonState() {
+ this.purchase.isEnabled = (this.selectedProduct != null)
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/net/pokeranalytics/android/util/billing/AppGuard.kt b/app/src/main/java/net/pokeranalytics/android/util/billing/AppGuard.kt
new file mode 100644
index 00000000..0fb29408
--- /dev/null
+++ b/app/src/main/java/net/pokeranalytics/android/util/billing/AppGuard.kt
@@ -0,0 +1,168 @@
+package net.pokeranalytics.android.util.billing
+
+import android.app.Activity
+import android.content.Context
+import com.android.billingclient.api.*
+import net.pokeranalytics.android.ui.activity.IAPProducts
+
+/**
+ * the AppGuard object is in charge of contacting the Billing services to retrieve products,
+ * initiating purchases and verifying transactions
+ */
+object AppGuard : PurchasesUpdatedListener {
+
+ /**
+ * The Billing Client making requests with Google Billing services
+ */
+ lateinit private var billingClient: BillingClient
+
+ /**
+ * Whether the billing client is available
+ */
+ private var billingClientAvailable: Boolean = false
+
+ /**
+ * Returns whether the user has the pro subscription
+ */
+ var isProUser: Boolean = false
+ private set
+
+ /**
+ * Initialization of AppGuard
+ * Connects to billing services and restores purchases
+ */
+ fun load(context: Context) {
+
+ billingClient = BillingClient.newBuilder(context).setListener(this).build()
+
+ this.startConnection(Runnable {
+ this.restorePurchases()
+ })
+
+ }
+
+ private fun startConnection(executeOnSuccess: Runnable) {
+
+ billingClient.startConnection(object : BillingClientStateListener {
+ override fun onBillingSetupFinished(@BillingClient.BillingResponse billingResponseCode: Int) {
+ if (billingResponseCode == BillingClient.BillingResponse.OK) {
+ // The BillingClient is ready. You can query purchases here.
+ billingClientAvailable = true
+ executeOnSuccess.run()
+ }
+ }
+
+ override fun onBillingServiceDisconnected() {
+ billingClientAvailable = false
+ }
+ })
+
+ }
+
+ private fun executeServiceRequest(runnable: Runnable) {
+
+ if (billingClientAvailable) {
+ runnable.run()
+ } else {
+ this.startConnection(runnable)
+ }
+
+ }
+
+ /**
+ * Restore purchases
+ */
+ fun restorePurchases() {
+ // Automatically checks for purchases (when switching devices for example)
+ val purchasesResult =
+ billingClient.queryPurchases(BillingClient.SkuType.SUBS)
+ purchasesResult.purchasesList.forEach {
+ this.handlePurchase(it)
+ }
+ }
+
+ /**
+ * Requests the product descriptions
+ */
+ fun requestProducts(listener: SkuDetailsResponseListener): Boolean {
+
+ if (this.billingClientAvailable) {
+
+ val skuList = ArrayList()
+ skuList.add(IAPProducts.PRO.identifier)
+ val params = SkuDetailsParams.newBuilder()
+ params.setSkusList(skuList).setType(BillingClient.SkuType.SUBS)
+
+ this.executeServiceRequest(Runnable {
+ billingClient.querySkuDetailsAsync(params.build(), listener)
+ })
+
+ return true
+ }
+ return false
+ }
+
+ /**
+ * Initiates purchase with the product [skuDetails]
+ */
+ fun initiatePurchase(activity: Activity, skuDetails: SkuDetails) {
+
+ val flowParams = BillingFlowParams.newBuilder()
+ .setSkuDetails(skuDetails)
+ .build()
+
+ this.executeServiceRequest(Runnable {
+ val responseCode = billingClient.launchBillingFlow(activity, flowParams)
+ })
+
+ }
+
+ // PurchasesUpdatedListener
+
+ /**
+ * Purchase callback
+ */
+ override fun onPurchasesUpdated(responseCode: Int, purchases: MutableList?) {
+
+ if (responseCode == BillingClient.BillingResponse.OK && purchases != null) {
+ for (purchase in purchases) {
+ handlePurchase(purchase)
+ }
+ } else if (responseCode == BillingClient.BillingResponse.USER_CANCELED) {
+ // Handle an error caused by a user cancelling the purchase flow.
+ } else {
+ // Handle any other error codes.
+ }
+
+ }
+
+ /**
+ * Method called when a purchase has been made
+ */
+ private fun handlePurchase(purchase: Purchase) {
+
+ val token = purchase.purchaseToken
+ val oj = purchase.originalJson
+
+ if (this.verifyPurchase(purchase)) {
+ when (purchase.sku) {
+ IAPProducts.PRO.identifier -> {
+ this.isProUser = true
+ }
+ else -> {
+ }
+ }
+ } else {
+ // invalid purchase
+ }
+
+ }
+
+ /**
+ * Verifies the validity of a purchase
+ */
+ private fun verifyPurchase(purchase: Purchase): Boolean {
+ return true
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_billing.xml b/app/src/main/res/layout/activity_billing.xml
new file mode 100644
index 00000000..08d1f651
--- /dev/null
+++ b/app/src/main/res/layout/activity_billing.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_subscription.xml b/app/src/main/res/layout/fragment_subscription.xml
new file mode 100644
index 00000000..90b15285
--- /dev/null
+++ b/app/src/main/res/layout/fragment_subscription.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 73666f07..26f159bb 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -11,6 +11,7 @@
Initial Value
Can\'t show because there is less than two values to display!
The object you\'re trying to access is invalid
+ The billing service is unavailable at the moment. Please check your internet connection and retry later.
Address
Naming suggestions