Merge branch 'dev' of gitlab.com:stax-river/poker-analytics into dev

dev
Aurelien Hubert 7 years ago
commit dec608474c
  1. 3
      app/build.gradle
  2. 6
      app/src/main/AndroidManifest.xml
  3. 5
      app/src/main/java/net/pokeranalytics/android/PokerAnalyticsApplication.kt
  4. 4
      app/src/main/java/net/pokeranalytics/android/calculus/bankroll/BankrollReport.kt
  5. 36
      app/src/main/java/net/pokeranalytics/android/ui/activity/BillingActivity.kt
  6. 2
      app/src/main/java/net/pokeranalytics/android/ui/fragment/SessionFragment.kt
  7. 68
      app/src/main/java/net/pokeranalytics/android/ui/fragment/SubscriptionFragment.kt
  8. 168
      app/src/main/java/net/pokeranalytics/android/util/billing/AppGuard.kt
  9. 15
      app/src/main/res/layout/activity_billing.xml
  10. 19
      app/src/main/res/layout/fragment_subscription.xml
  11. 1
      app/src/main/res/values/strings.xml

@ -87,6 +87,9 @@ dependencies {
// Places // Places
implementation 'com.google.android.libraries.places:places:1.1.0' implementation 'com.google.android.libraries.places:places:1.1.0'
// Billing / Subscriptions
implementation 'com.android.billingclient:billing:1.2.2'
// Firebase // Firebase
implementation 'com.google.firebase:firebase-core:16.0.8' implementation 'com.google.firebase:firebase-core:16.0.8'

@ -4,6 +4,7 @@
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="com.android.vending.BILLING" />
<application <application
android:name=".PokerAnalyticsApplication" android:name=".PokerAnalyticsApplication"
@ -112,6 +113,11 @@
android:launchMode="singleTop" android:launchMode="singleTop"
android:screenOrientation="portrait" /> android:screenOrientation="portrait" />
<activity
android:name="net.pokeranalytics.android.ui.activity.BillingActivity"
android:launchMode="singleTop"
android:screenOrientation="portrait" />
<meta-data <meta-data
android:name="preloaded_fonts" android:name="preloaded_fonts"
android:resource="@array/preloaded_fonts" /> android:resource="@array/preloaded_fonts" />

@ -26,6 +26,9 @@ class PokerAnalyticsApplication : Application() {
super.onCreate() super.onCreate()
UserDefaults.init(this) UserDefaults.init(this)
// AppGuard / Billing services
// AppGuard.load(this.applicationContext)
// Realm // Realm
Realm.init(this) Realm.init(this)
val realmConfiguration = RealmConfiguration.Builder() val realmConfiguration = RealmConfiguration.Builder()
@ -52,7 +55,7 @@ class PokerAnalyticsApplication : Application() {
if (BuildConfig.DEBUG) { if (BuildConfig.DEBUG) {
Timber.d("UserPreferences.defaultCurrency: ${UserDefaults.currency.symbol}") Timber.d("UserPreferences.defaultCurrency: ${UserDefaults.currency.symbol}")
this.createFakeSessions() // this.createFakeSessions()
} }
Patcher.patchBreaks() Patcher.patchBreaks()

@ -203,8 +203,8 @@ class BankrollReport(var setup: BankrollReportSetup) : RowRepresentable {
fun lineDataSet(context: Context): LineDataSet { fun lineDataSet(context: Context): LineDataSet {
val entries = mutableListOf<Entry>() val entries = mutableListOf<Entry>()
this.evolutionPoints.forEach { this.evolutionPoints.forEachIndexed { index, point ->
val entry = Entry(it.date.time.toFloat(), it.value.toFloat(), it.data) val entry = Entry(index.toFloat(), point.value.toFloat(), point.data)
entries.add(entry) entries.add(entry)
} }
return DataSetFactory.lineDataSetInstance(entries, "", context) return DataSetFactory.lineDataSetInstance(entries, "", context)

@ -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()
}
}

@ -40,6 +40,7 @@ class SessionFragment : PokerAnalyticsFragment(), RowRepresentableDelegate {
private val oldRows: ArrayList<RowRepresentable> = ArrayList() private val oldRows: ArrayList<RowRepresentable> = ArrayList()
private var sessionHasBeenCustomized = false private var sessionHasBeenCustomized = false
private val handler: Handler = Handler() private val handler: Handler = Handler()
private val refreshTimer: Runnable = object : Runnable { private val refreshTimer: Runnable = object : Runnable {
override fun run() { override fun run() {
// Refresh header each 30 seconds // Refresh header each 30 seconds
@ -263,6 +264,7 @@ class SessionFragment : PokerAnalyticsFragment(), RowRepresentableDelegate {
when (currentSession.getState()) { when (currentSession.getState()) {
SessionState.PENDING, SessionState.PLANNED, SessionState.PAUSED -> { SessionState.PENDING, SessionState.PLANNED, SessionState.PAUSED -> {
currentSession.startOrContinue() currentSession.startOrContinue()
this.recyclerView.smoothScrollToPosition(0)
} }
SessionState.STARTED -> { SessionState.STARTED -> {
currentSession.pause() currentSession.pause()

@ -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<SkuDetails>?) {
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)
}
}

@ -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<String>()
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<Purchase>?) {
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
}
}

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="vertical">
<fragment
android:id="@+id/subscriptionFragment"
android:name="net.pokeranalytics.android.ui.fragment.SubscriptionFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:layout="@layout/fragment_subscription" />
</LinearLayout>

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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="match_parent"
android:orientation="vertical">
<com.google.android.material.button.MaterialButton
android:id="@+id/purchase"
style="@style/PokerAnalyticsTheme.Button"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:text="@string/purchase" />
</androidx.constraintlayout.widget.ConstraintLayout>

@ -11,6 +11,7 @@
<string name="initial_value">Initial Value</string> <string name="initial_value">Initial Value</string>
<string name="less_then_2_values_for_display">Can\'t show because there is less than two values to display!</string> <string name="less_then_2_values_for_display">Can\'t show because there is less than two values to display!</string>
<string name="invalid_object">The object you\'re trying to access is invalid</string> <string name="invalid_object">The object you\'re trying to access is invalid</string>
<string name="billingclient_unavailable">The billing service is unavailable at the moment. Please check your internet connection and retry later.</string>
<string name="address">Address</string> <string name="address">Address</string>
<string name="suggestions">Naming suggestions</string> <string name="suggestions">Naming suggestions</string>

Loading…
Cancel
Save