1
0
mirror of https://github.com/AllanWang/Frost-for-Facebook.git synced 2024-11-08 12:02:33 +01:00

Test new billing logic (#86)

* Add lint

* Add new libs

* Update libs and add friends tab

* Aggressively hide nonrecent posts

* Update dependencies

* Add php to most recents

* Add full size image downloader

* Fix css cleaner

* Fix notification and circle

* Bring back regex

* Update kau, optimize imports, and remove string ambiguity

* Bring back anjlab iab and move to alpha

* Create initial billing test
This commit is contained in:
Allan Wang 2017-07-22 20:27:22 -07:00 committed by GitHub
parent 37a9f9057d
commit 1388240656
35 changed files with 239 additions and 1932 deletions

View File

@ -10,7 +10,7 @@ apply plugin: 'com.github.triplet.play'
play {
jsonFile = file('../files/gplay-keys.json')
track = 'beta'
track = 'alpha'
errorOnSizeLimit = true
uploadImages = false
untrackOld = true
@ -130,11 +130,10 @@ dependencies {
compile "ca.allanwang.kau:about:$KAU"
compile "ca.allanwang.kau:colorpicker:$KAU"
// compile "ca.allanwang.kau:imagepicker:$KAU"
compile "ca.allanwang.kau:imagepicker:$KAU"
compile "ca.allanwang.kau:kpref-activity:$KAU"
compile "ca.allanwang.kau:searchview:$KAU"
compile "org.jetbrains.kotlin:kotlin-stdlib:${KOTLIN}"
testCompile "org.jetbrains.kotlin:kotlin-test-junit:${KOTLIN}"
debugCompile "com.squareup.leakcanary:leakcanary-android:${LEAK_CANARY}"
@ -155,8 +154,7 @@ dependencies {
compile "com.squareup.okhttp3:okhttp:${OKHTTP}"
compile "com.github.bumptech.glide:glide:${GLIDE}"
kapt "com.github.bumptech.glide:compiler:${GLIDE}"
compile "com.anjlab.android.iab.v3:library:${IAB}"
// compile("com.mikepenz:materialdrawer:${MATERIAL_DRAWER}@aar") {
// transitive = true

View File

@ -117,7 +117,7 @@
android:theme="@style/FrostTheme.Settings" />
<activity
android:name=".activities.AboutActivity"
android:theme="@style/Kau.Translucent.About" />
android:theme="@style/Kau.About" />
<activity
android:name=".activities.ImageActivity"
android:theme="@style/FrostTheme.Overlay.Fade" />

View File

@ -4,16 +4,15 @@ import android.app.Application
import android.graphics.drawable.Drawable
import android.net.Uri
import android.widget.ImageView
import com.bumptech.glide.Glide
import com.bumptech.glide.request.RequestOptions
import com.bumptech.glide.signature.ApplicationVersionSignature
import com.crashlytics.android.Crashlytics
import com.crashlytics.android.answers.Answers
import com.mikepenz.materialdrawer.util.AbstractDrawerImageLoader
import com.mikepenz.materialdrawer.util.DrawerImageLoader
import com.pitchedapps.frost.BuildConfig
import com.pitchedapps.frost.facebook.FbCookie
import com.pitchedapps.frost.utils.CrashReportingTree
import com.pitchedapps.frost.utils.GlideApp
import com.pitchedapps.frost.utils.Prefs
import com.pitchedapps.frost.utils.Showcase
import com.raizlabs.android.dbflow.config.FlowConfig
@ -65,8 +64,8 @@ class FrostApp : Application() {
DrawerImageLoader.init(object : AbstractDrawerImageLoader() {
override fun set(imageView: ImageView, uri: Uri, placeholder: Drawable, tag: String) {
val c = imageView.context
val old = GlideApp.with(c).load(uri).apply(RequestOptions().placeholder(placeholder))
GlideApp.with(c).load(uri).apply(RequestOptions().signature(ApplicationVersionSignature.obtain(c)))
val old = Glide.with(c).load(uri).apply(RequestOptions().placeholder(placeholder))
Glide.with(c).load(uri).apply(RequestOptions().signature(ApplicationVersionSignature.obtain(c)))
.thumbnail(old).into(imageView)
}
})

View File

@ -1,7 +1,5 @@
package com.pitchedapps.frost.activities
import android.animation.ValueAnimator
import android.annotation.SuppressLint
import android.content.Intent
import android.content.res.ColorStateList
import android.graphics.Bitmap
@ -21,6 +19,7 @@ import ca.allanwang.kau.permissions.PERMISSION_WRITE_EXTERNAL_STORAGE
import ca.allanwang.kau.permissions.kauOnRequestPermissionsResult
import ca.allanwang.kau.permissions.kauRequestPermissions
import ca.allanwang.kau.utils.*
import com.bumptech.glide.Glide
import com.bumptech.glide.request.target.BaseTarget
import com.bumptech.glide.request.target.SizeReadyCallback
import com.bumptech.glide.request.target.Target
@ -31,7 +30,10 @@ import com.mikepenz.google_material_typeface_library.GoogleMaterial
import com.mikepenz.iconics.typeface.IIcon
import com.pitchedapps.frost.BuildConfig
import com.pitchedapps.frost.R
import com.pitchedapps.frost.utils.*
import com.pitchedapps.frost.utils.ARG_IMAGE_URL
import com.pitchedapps.frost.utils.ARG_TEXT
import com.pitchedapps.frost.utils.L
import com.pitchedapps.frost.utils.Prefs
import com.sothree.slidinguppanel.SlidingUpPanelLayout
import org.jetbrains.anko.doAsync
import org.jetbrains.anko.uiThread
@ -82,7 +84,8 @@ class ImageActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(if (!text.isNullOrBlank()) R.layout.activity_image else R.layout.activity_image_textless)
val layout = if (!text.isNullOrBlank()) R.layout.activity_image else R.layout.activity_image_textless
setContentView(layout)
container.setBackgroundColor(Prefs.bgColor.withMinAlpha(222))
caption?.setTextColor(Prefs.textColor)
caption?.setBackgroundColor(Prefs.bgColor.colorToForeground(0.2f).withAlpha(255))
@ -104,7 +107,7 @@ class ImageActivity : AppCompatActivity() {
imageCallback(null, false)
}
})
GlideApp.with(this).asBitmap().load(imageUrl).into(PhotoTarget(this::imageCallback))
Glide.with(this).asBitmap().load(imageUrl).into(PhotoTarget(this::imageCallback))
}
/**
@ -196,7 +199,8 @@ class ImageActivity : AppCompatActivity() {
} finally {
L.d("Download image async finished: $success")
uiThread {
snackbar(if (success) R.string.image_download_success else R.string.image_download_fail)
val text = if (success) R.string.image_download_success else R.string.image_download_fail
snackbar(text)
if (success) {
deleteTempFile()
fabAction = FabStates.SHARE

View File

@ -9,7 +9,10 @@ import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.support.annotation.StringRes
import android.support.design.widget.*
import android.support.design.widget.AppBarLayout
import android.support.design.widget.CoordinatorLayout
import android.support.design.widget.FloatingActionButton
import android.support.design.widget.TabLayout
import android.support.v4.app.ActivityOptionsCompat
import android.support.v4.app.Fragment
import android.support.v4.app.FragmentManager
@ -52,7 +55,8 @@ import com.pitchedapps.frost.facebook.FbTab
import com.pitchedapps.frost.facebook.PROFILE_PICTURE_URL
import com.pitchedapps.frost.fragments.WebFragment
import com.pitchedapps.frost.utils.*
import com.pitchedapps.frost.utils.iab.validatePro
import com.pitchedapps.frost.utils.iab.FrostBilling
import com.pitchedapps.frost.utils.iab.IABMain
import com.pitchedapps.frost.views.BadgedIcon
import com.pitchedapps.frost.views.FrostViewPager
import com.pitchedapps.frost.web.SearchWebView
@ -64,7 +68,8 @@ import org.jsoup.Jsoup
import java.util.concurrent.TimeUnit
class MainActivity : BaseActivity(), SearchWebView.SearchContract,
ActivityWebContract, FileChooserContract by FileChooserDelegate() {
ActivityWebContract, FileChooserContract by FileChooserDelegate(),
FrostBilling by IABMain() {
lateinit var adapter: SectionsPagerAdapter
val toolbar: Toolbar by bindView(R.id.toolbar)
@ -97,12 +102,12 @@ class MainActivity : BaseActivity(), SearchWebView.SearchContract,
* Possible responses from the SettingsActivity
* after the configurations have changed
*/
const val REQUEST_RESTART = 90909
const val REQUEST_REFRESH = 80808
const val REQUEST_WEB_ZOOM = 50505
const val REQUEST_NAV = 10101
const val REQUEST_SEARCH = 70707
const val REQUEST_RESTART_APPLICATION = 60606
const val REQUEST_RESTART_APPLICATION = 1 shl 1
const val REQUEST_RESTART = 1 shl 2
const val REQUEST_REFRESH = 1 shl 3
const val REQUEST_WEB_ZOOM = 1 shl 4
const val REQUEST_NAV = 1 shl 5
const val REQUEST_SEARCH = 1 shl 6
}
override fun onCreate(savedInstanceState: Bundle?) {
@ -149,12 +154,12 @@ class MainActivity : BaseActivity(), SearchWebView.SearchContract,
viewPager.post { webFragmentObservable.onNext(0); lastPosition = 0 } //trigger hook so title is set
setupDrawer(savedInstanceState)
setupTabs()
fab.setOnClickListener { view ->
Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG)
.setAction("Action", null).show()
}
// fab.setOnClickListener { view ->
// Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG)
// .setAction("Action", null).show()
// }
setFrostColors(toolbar, themeWindow = false, headers = arrayOf(tabs, appBar), backgrounds = arrayOf(viewPager))
validatePro()
onCreateBilling()
}
fun tabsForEachView(action: (position: Int, view: BadgedIcon) -> Unit) {
@ -394,26 +399,28 @@ class MainActivity : BaseActivity(), SearchWebView.SearchContract,
if (onActivityResultWeb(requestCode, resultCode, data)) return
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == ACTIVITY_SETTINGS) {
when (resultCode) {
REQUEST_RESTART -> restart()
REQUEST_REFRESH -> webFragmentObservable.onNext(WebFragment.REQUEST_REFRESH)
REQUEST_NAV -> frostNavigationBar()
REQUEST_WEB_ZOOM -> webFragmentObservable.onNext(WebFragment.REQUEST_TEXT_ZOOM)
REQUEST_SEARCH -> invalidateOptionsMenu()
REQUEST_RESTART_APPLICATION -> { //completely restart application
L.d("Restart Application Requested")
val intent = packageManager.getLaunchIntentForPackage(packageName)
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK)
val pending = PendingIntent.getActivity(this, 666, intent, PendingIntent.FLAG_CANCEL_CURRENT)
val alarm = getSystemService(Context.ALARM_SERVICE) as AlarmManager
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M)
alarm.setExactAndAllowWhileIdle(AlarmManager.RTC, System.currentTimeMillis() + 100, pending)
else
alarm.setExact(AlarmManager.RTC, System.currentTimeMillis() + 100, pending)
finish()
System.exit(0)
}
if (resultCode and REQUEST_RESTART_APPLICATION > 0) { //completely restart application
L.d("Restart Application Requested")
val intent = packageManager.getLaunchIntentForPackage(packageName)
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK)
val pending = PendingIntent.getActivity(this, 666, intent, PendingIntent.FLAG_CANCEL_CURRENT)
val alarm = getSystemService(Context.ALARM_SERVICE) as AlarmManager
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M)
alarm.setExactAndAllowWhileIdle(AlarmManager.RTC, System.currentTimeMillis() + 100, pending)
else
alarm.setExact(AlarmManager.RTC, System.currentTimeMillis() + 100, pending)
finish()
System.exit(0)
return
}
if (resultCode and REQUEST_RESTART > 0) return restart()
/*
* These results can be stacked
*/
if (resultCode and REQUEST_REFRESH > 0) webFragmentObservable.onNext(WebFragment.REQUEST_REFRESH)
if (resultCode and REQUEST_NAV > 0) frostNavigationBar()
if (resultCode and REQUEST_WEB_ZOOM > 0) webFragmentObservable.onNext(WebFragment.REQUEST_TEXT_ZOOM)
if (resultCode and REQUEST_SEARCH > 0) invalidateOptionsMenu()
}
}
@ -435,6 +442,11 @@ class MainActivity : BaseActivity(), SearchWebView.SearchContract,
super.onStart()
}
override fun onDestroy() {
onDestroyBilling()
super.onDestroy()
}
override fun onBackPressed() {
if (searchView?.onBackPressed() ?: false) return
if (currentFragment.onBackPressed()) return

View File

@ -17,27 +17,23 @@ import com.pitchedapps.frost.BuildConfig
import com.pitchedapps.frost.R
import com.pitchedapps.frost.settings.*
import com.pitchedapps.frost.utils.*
import com.pitchedapps.frost.utils.iab.*
import com.pitchedapps.frost.utils.iab.FrostBilling
import com.pitchedapps.frost.utils.iab.IABSettings
import com.pitchedapps.frost.utils.iab.IS_FROST_PRO
/**
* Created by Allan Wang on 2017-06-06.
*/
class SettingsActivity : KPrefActivity(), IabBroadcastReceiver.IabBroadcastListener {
class SettingsActivity : KPrefActivity(), FrostBilling by IABSettings() {
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (!IAB.handleActivityResult(requestCode, resultCode, data)) {
if (!onActivityResultBilling(requestCode, resultCode, data)) {
super.onActivityResult(requestCode, resultCode, data)
adapter.notifyDataSetChanged()
}
}
override fun receivedBroadcast() {
L.d("IAB broadcast")
adapter.notifyDataSetChanged()
}
override fun kPrefCoreAttributes(): CoreAttributeContract.() -> Unit = {
textColor = { Prefs.textColor }
accentColor = { Prefs.accentColor }
@ -72,7 +68,7 @@ class SettingsActivity : KPrefActivity(), IabBroadcastReceiver.IabBroadcastListe
plainText(R.string.restore_purchases) {
descRes = R.string.restore_purchases_desc
iicon = GoogleMaterial.Icon.gmd_refresh
onClick = { _, _, _ -> this@SettingsActivity.restorePurchases(); true }
onClick = { _, _, _ -> restorePurchases(false); true }
}
plainText(R.string.about_frost) {
@ -86,7 +82,7 @@ class SettingsActivity : KPrefActivity(), IabBroadcastReceiver.IabBroadcastListe
}
fun KPrefItemBase.BaseContract<*>.dependsOnPro() {
onDisabledClick = { _, _, _ -> openPlayProPurchase(0); true }
onDisabledClick = { _, _, _ -> purchasePro(); true }
enabler = { IS_FROST_PRO }
}
@ -99,6 +95,7 @@ class SettingsActivity : KPrefActivity(), IabBroadcastReceiver.IabBroadcastListe
super.onCreate(savedInstanceState)
animate = Prefs.animate
themeExterior(false)
onCreateBilling()
}
fun themeExterior(animate: Boolean = true) {
@ -139,7 +136,7 @@ class SettingsActivity : KPrefActivity(), IabBroadcastReceiver.IabBroadcastListe
}
override fun onDestroy() {
IAB.dispose()
onDestroyBilling()
super.onDestroy()
}
}

View File

@ -1,7 +1,6 @@
package com.pitchedapps.frost.injectors
import android.webkit.WebView
import com.pitchedapps.frost.utils.L
/**
* Created by Allan Wang on 2017-05-31.

View File

@ -14,15 +14,19 @@ import android.support.v4.app.NotificationManagerCompat
import ca.allanwang.kau.utils.color
import ca.allanwang.kau.utils.dpToPx
import ca.allanwang.kau.utils.string
import com.bumptech.glide.Glide
import com.bumptech.glide.request.target.SimpleTarget
import com.bumptech.glide.request.transition.Transition
import com.pitchedapps.frost.BuildConfig
import com.pitchedapps.frost.activities.FrostWebActivity
import com.pitchedapps.frost.R
import com.pitchedapps.frost.activities.FrostWebActivity
import com.pitchedapps.frost.dbflow.CookieModel
import com.pitchedapps.frost.dbflow.fetchUsername
import com.pitchedapps.frost.facebook.FB_URL_BASE
import com.pitchedapps.frost.utils.*
import com.pitchedapps.frost.utils.ARG_USER_ID
import com.pitchedapps.frost.utils.L
import com.pitchedapps.frost.utils.Prefs
import com.pitchedapps.frost.utils.withRoundIcon
import org.jetbrains.anko.runOnUiThread
/**
@ -100,7 +104,7 @@ data class NotificationContent(val data: CookieModel,
if (profileUrl.isNotBlank()) {
context.runOnUiThread {
GlideApp.with(context)
Glide.with(context)
.asBitmap()
.load(profileUrl)
.withRoundIcon()

View File

@ -17,9 +17,7 @@ import com.pitchedapps.frost.facebook.USER_AGENT_BASIC
import com.pitchedapps.frost.utils.L
import com.pitchedapps.frost.utils.Prefs
import com.pitchedapps.frost.utils.frostAnswersCustom
import com.pitchedapps.frost.web.MessageWebView
import org.jetbrains.anko.doAsync
import org.jetbrains.anko.uiThread
import org.jsoup.Jsoup
import org.jsoup.nodes.Element
import java.util.concurrent.Future

View File

@ -5,13 +5,12 @@ import ca.allanwang.kau.kpref.activity.items.KPrefColorPicker
import ca.allanwang.kau.kpref.activity.items.KPrefSeekbar
import ca.allanwang.kau.ui.views.RippleCanvas
import ca.allanwang.kau.utils.string
import com.pitchedapps.frost.activities.MainActivity
import com.pitchedapps.frost.R
import com.pitchedapps.frost.activities.MainActivity
import com.pitchedapps.frost.activities.SettingsActivity
import com.pitchedapps.frost.injectors.CssAssets
import com.pitchedapps.frost.utils.*
import com.pitchedapps.frost.utils.iab.IS_FROST_PRO
import com.pitchedapps.frost.utils.iab.openPlayProPurchase
import com.pitchedapps.frost.views.KPrefTextSeekbar
/**
@ -33,7 +32,7 @@ fun SettingsActivity.getAppearancePrefs(): KPrefAdapterBuilder.() -> Unit = {
_, _, which, text ->
if (item.pref != which) {
if (which == Theme.CUSTOM.ordinal && !IS_FROST_PRO) {
openPlayProPurchase(9)
purchasePro()
return@itemsCallbackSingleChoice true
}
item.pref = which

View File

@ -1,8 +1,8 @@
package com.pitchedapps.frost.settings
import ca.allanwang.kau.kpref.activity.KPrefAdapterBuilder
import com.pitchedapps.frost.activities.MainActivity
import com.pitchedapps.frost.R
import com.pitchedapps.frost.activities.MainActivity
import com.pitchedapps.frost.activities.SettingsActivity
import com.pitchedapps.frost.utils.Prefs

View File

@ -1,8 +1,8 @@
package com.pitchedapps.frost.settings
import ca.allanwang.kau.kpref.activity.KPrefAdapterBuilder
import com.pitchedapps.frost.activities.MainActivity
import com.pitchedapps.frost.R
import com.pitchedapps.frost.activities.MainActivity
import com.pitchedapps.frost.activities.SettingsActivity
import com.pitchedapps.frost.utils.Prefs
import com.pitchedapps.frost.utils.Showcase

View File

@ -2,8 +2,8 @@ package com.pitchedapps.frost.settings
import ca.allanwang.kau.kpref.activity.KPrefAdapterBuilder
import ca.allanwang.kau.utils.string
import com.pitchedapps.frost.activities.MainActivity
import com.pitchedapps.frost.R
import com.pitchedapps.frost.activities.MainActivity
import com.pitchedapps.frost.activities.SettingsActivity
import com.pitchedapps.frost.facebook.FeedSort
import com.pitchedapps.frost.utils.Prefs

View File

@ -65,7 +65,8 @@ fun SettingsActivity.getNotificationPrefs(): KPrefAdapterBuilder.() -> Unit = {
descRes = R.string.notification_fetch_now_desc
onClick = {
_, _, _ ->
snackbar(if (fetchNotifications()) R.string.notification_fetch_success else R.string.notification_fetch_fail)
val text = if (fetchNotifications()) R.string.notification_fetch_success else R.string.notification_fetch_fail
snackbar(text)
true
}
}

View File

@ -8,18 +8,13 @@ import android.support.annotation.StringRes
import android.support.design.internal.SnackbarContentLayout
import android.support.design.widget.Snackbar
import android.support.v7.widget.Toolbar
import android.util.Log
import android.view.View
import android.widget.FrameLayout
import android.widget.TextView
import ca.allanwang.kau.utils.*
import com.afollestad.materialdialogs.MaterialDialog
import com.bumptech.glide.GlideBuilder
import com.bumptech.glide.RequestBuilder
import com.bumptech.glide.annotation.GlideExtension
import com.bumptech.glide.annotation.GlideModule
import com.bumptech.glide.load.resource.bitmap.CircleCrop
import com.bumptech.glide.module.AppGlideModule
import com.bumptech.glide.request.RequestOptions
import com.crashlytics.android.answers.Answers
import com.crashlytics.android.answers.CustomEvent
@ -42,9 +37,6 @@ const val ARG_USER_ID = "arg_user_id"
const val ARG_IMAGE_URL = "arg_image_url"
const val ARG_TEXT = "arg_text"
@GlideModule
class FrostGlideModule : AppGlideModule()
fun Context.launchNewTask(clazz: Class<out Activity>, cookieList: ArrayList<CookieModel> = arrayListOf(), clearStack: Boolean = false) {
startActivity(clazz, clearStack, intentBuilder = {
putParcelableArrayListExtra(EXTRA_COOKIES, cookieList)

View File

@ -5,8 +5,8 @@ import ca.allanwang.kau.utils.copyToClipboard
import ca.allanwang.kau.utils.shareText
import ca.allanwang.kau.utils.string
import ca.allanwang.kau.utils.toast
import com.pitchedapps.frost.activities.MainActivity
import com.pitchedapps.frost.R
import com.pitchedapps.frost.activities.MainActivity
/**
* Created by Allan Wang on 2017-07-07.

View File

@ -1,226 +0,0 @@
package com.pitchedapps.frost.utils.iab
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.support.design.widget.Snackbar
import ca.allanwang.kau.utils.isFromGooglePlay
import ca.allanwang.kau.utils.snackbar
import com.crashlytics.android.answers.PurchaseEvent
import com.pitchedapps.frost.BuildConfig
import com.pitchedapps.frost.R
import com.pitchedapps.frost.activities.SettingsActivity
import com.pitchedapps.frost.utils.*
/**
* Created by Allan Wang on 2017-06-23.
*
* Helper singleton to handle all billing related queries
* NOTE
* Make sure you call [IAB.dispose] once an operation is done to release the resources
* Also make sure that it is called on the very LAST operation if there are a list of async calls.
* Otherwise the helper will be prematurely disposed
*
* For the most part, billing is handled in the [SettingsActivity] and will be disposed when it is destroyed
* It may also be handled elsewhere when validating purchases, so those calls should dispose themselves
*/
object IAB {
private var helper: IabHelper? = null
/**
* Wrapper function to ensure that the helper exists before executing a command
*
* [mustHavePlayStore] decides if dialogs should be shown if play store errors occur
*
*/
operator fun invoke(activity: Activity,
mustHavePlayStore: Boolean = true,
userRequest: Boolean = true,
onFailed: () -> Unit = {},
onStart: (helper: IabHelper) -> Unit) {
with(activity) {
if (isInProgress) {
if (userRequest) snackbar(R.string.iab_still_in_progress, Snackbar.LENGTH_LONG)
L.d("Play Store IAB in progress")
} else if (helper?.disposed ?: true) {
helper = null
L.d("Play Store IAB setup async")
if (!isFrostPlay) {
if (mustHavePlayStore) playStoreNotFound()
onFailed()
return
}
try {
helper = IabHelper(applicationContext, PUBLIC_BILLING_KEY)
helper!!.enableDebugLogging(BuildConfig.DEBUG || Prefs.verboseLogging, "Frost:")
helper!!.startSetup {
result ->
L.d("Play Store IAB setup finished; ${result.isSuccess}")
if (result.isSuccess) {
L.d("Play Store IAB setup success")
onStart(helper!!)
} else {
L.d("Play Store IAB setup fail")
if (mustHavePlayStore)
activity.playStoreGenericError("Setup error: ${result.response} ${result.message}")
onFailed()
IAB.dispose()
}
}
} catch (e: Exception) {
L.e(e, "Play Store IAB error")
if (mustHavePlayStore)
playStoreGenericError(null)
onFailed()
IAB.dispose()
}
} else onStart(helper!!)
}
}
fun handleActivityResult(requestCode: Int, resultCode: Int, data: Intent?): Boolean
= helper?.handleActivityResult(requestCode, resultCode, data) ?: false
/**
* Call this after any execution to dispose the helper
*/
fun dispose() {
synchronized(this) {
L.d("Play Store IAB dispose")
helper?.disposeWhenFinished()
helper = null
}
}
/**
* Dispose given helper and check if it matches with our own helper
*/
fun dispose(helper: IabHelper) {
synchronized(this) {
L.d("Play Store IAB helper dispose")
helper.disposeWhenFinished()
if (IAB.helper?.disposed ?: true)
this.helper = null
}
}
val isInProgress: Boolean
get() = helper?.mAsyncInProgress ?: false
}
private const val FROST_PRO = "frost_pro"
private val IabHelper.disposed: Boolean
get() = mDisposed || mDisposeAfterAsync
val IS_FROST_PRO: Boolean
get() = (BuildConfig.DEBUG && Prefs.debugPro) || Prefs.pro
private val Context.isFrostPlay: Boolean
get() = isFromGooglePlay || BuildConfig.DEBUG
fun SettingsActivity.restorePurchases() {
//like validate, but with a snackbar and without other prompts
val restore = container.snackbar(R.string.restoring_purchases, Snackbar.LENGTH_INDEFINITE)
restore.setAction(R.string.kau_close) { restore.dismiss() }
//called if inventory is not properly retrieved
val reset = {
L.d("Play Store Restore reset")
if (Prefs.pro) {
Prefs.pro = false
Prefs.theme = Theme.DEFAULT.ordinal
}
finishRestore(restore, false)
}
getInventory(false, true, reset) {
inv, _ ->
val proSku = inv.hasPurchase(FROST_PRO)
Prefs.pro = proSku
L.d("Play Store Restore found: ${Prefs.pro}")
finishRestore(restore, Prefs.pro)
}
}
private fun SettingsActivity.finishRestore(snackbar: Snackbar, hasPro: Boolean) {
snackbar.dismiss()
materialDialogThemed {
title(R.string.purchases_restored)
content(if (hasPro) R.string.purchases_restored_with_pro else R.string.purchases_restored_without_pro)
positiveText(R.string.reload)
dismissListener { adapter.notifyAdapterDataSetChanged() }
}
}
/**
* If user has pro, check if it's valid and destroy the helper
* If cache matches result, it finishes silently
*/
fun Activity.validatePro() {
L.d("Play Store Validate pro")
try {
getInventory(Prefs.pro, false, { if (Prefs.pro) playStoreNoLongerPro() }) {
inv, helper ->
val proSku = inv.hasPurchase(FROST_PRO)
L.d("Play Store Validation finished: ${Prefs.pro} should be $proSku")
if (!proSku && Prefs.pro) playStoreNoLongerPro()
else if (proSku && !Prefs.pro) playStoreFoundPro()
IAB.dispose(helper)
}
} catch (e: Exception) {
L.e(e, "Play store validation exception")
IAB.dispose()
}
}
fun Activity.getInventory(
mustHavePlayStore: Boolean = true,
userRequest: Boolean = true,
onFailed: () -> Unit = {},
onSuccess: (inv: Inventory, helper: IabHelper) -> Unit) {
IAB(this, mustHavePlayStore, userRequest, onFailed) {
helper ->
helper.queryInventoryAsync {
res, inv ->
L.d("Play Store Inventory query finished")
if (res.isFailure || inv == null) {
L.e("Play Store Res error ${res.message}")
onFailed()
} else onSuccess(inv, helper)
}
}
}
fun Activity.openPlayProPurchase(code: Int) {
if (!isFrostPlay)
playStoreProNotAvailable()
else openPlayPurchase(FROST_PRO, code) {
Prefs.pro = true
}
}
fun Activity.openPlayPurchase(key: String, code: Int, onSuccess: (key: String) -> Unit) {
L.d("Play Store open purchase $key $code")
getInventory(true, true, { playStoreGenericError("Query res error") }) {
inv, helper ->
if (inv.hasPurchase(key)) {
playStoreAlreadyPurchased(key)
onSuccess(key)
return@getInventory
}
L.d("IAB: inventory ${inv.allOwnedSkus}")
helper.launchPurchaseFlow(this@openPlayPurchase, key, code) {
result, _ ->
if (result.isSuccess) {
onSuccess(key)
playStorePurchasedSuccessfully(key)
}
frostAnswers {
logPurchase(PurchaseEvent()
.putItemId(key)
.putCustomAttribute("result", result.message)
.putSuccess(result.isSuccess))
}
}
}
}

View File

@ -0,0 +1,139 @@
package com.pitchedapps.frost.utils.iab
import android.app.Activity
import android.content.Intent
import com.anjlab.android.iab.v3.BillingProcessor
import com.anjlab.android.iab.v3.TransactionDetails
import com.crashlytics.android.answers.PurchaseEvent
import com.pitchedapps.frost.BuildConfig
import com.pitchedapps.frost.utils.L
import com.pitchedapps.frost.utils.Prefs
import com.pitchedapps.frost.utils.frostAnswers
import org.jetbrains.anko.doAsync
import org.jetbrains.anko.uiThread
/**
* Created by Allan Wang on 2017-07-22.
*/
private const val FROST_PRO = "frost_pro"
val IS_FROST_PRO: Boolean
get() = (BuildConfig.DEBUG && Prefs.debugPro) || Prefs.pro
interface FrostBilling : BillingProcessor.IBillingHandler {
fun Activity.onCreateBilling()
fun onDestroyBilling()
fun purchasePro()
fun restorePurchases(once: Boolean)
fun onActivityResultBilling(requestCode: Int, resultCode: Int, data: Intent?): Boolean
}
open class IABBinder : FrostBilling {
var bp: BillingProcessor? = null
var activity: Activity? = null
override fun Activity.onCreateBilling() {
bp = BillingProcessor.newBillingProcessor(this, PUBLIC_BILLING_KEY, this@IABBinder)
activity = this
bp!!.initialize()
}
override fun onDestroyBilling() {
bp?.release()
bp = null
activity = null
}
override fun onBillingInitialized() {
L.d("IAB initialized")
}
override fun onPurchaseHistoryRestored() {
L.d("IAB restored")
}
override fun onProductPurchased(productId: String, details: TransactionDetails) {
L.d("IAB $productId purchased")
frostAnswers {
logPurchase(PurchaseEvent()
.putItemId(productId)
.putSuccess(true)
)
}
}
override fun onBillingError(errorCode: Int, error: Throwable) {
frostAnswers {
logPurchase(PurchaseEvent()
.putCustomAttribute("result", errorCode.toString())
.putSuccess(false))
}
L.e(error, "IAB error $errorCode")
}
override fun onActivityResultBilling(requestCode: Int, resultCode: Int, data: Intent?): Boolean
= bp?.handleActivityResult(requestCode, resultCode, data) ?: false
override fun purchasePro() {
if (bp == null) return
if (!bp!!.isOneTimePurchaseSupported)
activity!!.playStorePurchaseUnsupported()
else
bp!!.purchase(activity, FROST_PRO)
}
override fun restorePurchases(once: Boolean) {
if (bp == null) return
doAsync {
bp?.loadOwnedPurchasesFromGoogle()
if (bp?.isPurchased(FROST_PRO) ?: false) {
uiThread {
if (Prefs.pro) activity!!.playStoreNoLongerPro()
else if (!once) purchasePro()
if (once) onDestroyBilling()
}
} else {
uiThread {
if (!Prefs.pro) activity!!.playStoreFoundPro()
else if (!once) activity!!.purchaseRestored()
if (once) onDestroyBilling()
}
}
}
}
}
class IABSettings : IABBinder() {
override fun onBillingInitialized() {
super.onBillingInitialized()
}
override fun onPurchaseHistoryRestored() {
super.onPurchaseHistoryRestored()
}
override fun onProductPurchased(productId: String, details: TransactionDetails) {
super.onProductPurchased(productId, details)
}
override fun onBillingError(errorCode: Int, error: Throwable) {
super.onBillingError(errorCode, error)
activity?.playStoreGenericError(null)
}
}
class IABMain : IABBinder() {
override fun onBillingInitialized() {
super.onBillingInitialized()
restorePurchases(true)
}
override fun onPurchaseHistoryRestored() {
super.onPurchaseHistoryRestored()
restorePurchases(true)
}
}

View File

@ -4,8 +4,8 @@ import android.app.Activity
import ca.allanwang.kau.utils.restart
import ca.allanwang.kau.utils.startPlayStoreLink
import ca.allanwang.kau.utils.string
import com.pitchedapps.frost.activities.MainActivity
import com.pitchedapps.frost.R
import com.pitchedapps.frost.activities.MainActivity
import com.pitchedapps.frost.activities.SettingsActivity
import com.pitchedapps.frost.utils.L
import com.pitchedapps.frost.utils.Prefs
@ -14,6 +14,7 @@ import com.pitchedapps.frost.utils.materialDialogThemed
/**
* Created by Allan Wang on 2017-06-30.
*/
private fun playStoreLog(text: String) {
L.e(Throwable(text), "Play Store Exception")
}
@ -28,7 +29,6 @@ private fun Activity.playRestart() {
} else restart()
}
fun Activity.playStoreNoLongerPro() {
Prefs.pro = false
playStoreLog("No Longer Pro")
@ -55,11 +55,11 @@ fun Activity.playStoreFoundPro() {
}
}
fun Activity.playStoreNotFound() {
fun Activity.playStorePurchaseUnsupported() {
L.d("Play store not found")
materialDialogThemed {
title(R.string.uh_oh)
content(R.string.play_store_not_found)
content(R.string.play_store_unsupported)
positiveText(R.string.kau_ok)
neutralText(R.string.kau_play_store)
onNeutral { _, _ -> startPlayStoreLink(R.string.play_store_package_id) }
@ -107,7 +107,7 @@ fun Activity.playStorePurchasedSuccessfully(key: String) {
}
}
fun SettingsActivity.purchaseRestored() {
fun Activity.purchaseRestored() {
L.d("Purchase restored")
materialDialogThemed {
title(R.string.play_thank_you)

View File

@ -1,60 +0,0 @@
/* Copyright (c) 2014 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.pitchedapps.frost.utils.iab;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
/**
* Receiver for the "com.android.vending.billing.PURCHASES_UPDATED" Action
* from the Play Store.
*
* <p>It is possible that an in-app item may be acquired without the
* application calling getBuyIntent(), for example if the item can be
* redeemed from inside the Play Store using a promotional code. If this
* application isn't running at the time, then when it is started a call
* to getPurchases() will be sufficient notification. However, if the
* application is already running in the background when the item is acquired,
* a message to this BroadcastReceiver will indicate that the an item
* has been acquired.</p>
*/
public class IabBroadcastReceiver extends BroadcastReceiver {
/**
* Listener interface for received broadcast messages.
*/
public interface IabBroadcastListener {
void receivedBroadcast();
}
/**
* The Intent action that this Receiver should filter for.
*/
public static final String ACTION = "com.android.vending.billing.PURCHASES_UPDATED";
private final IabBroadcastListener mListener;
public IabBroadcastReceiver(IabBroadcastListener listener) {
mListener = listener;
}
@Override
public void onReceive(Context context, Intent intent) {
if (mListener != null) {
mListener.receivedBroadcast();
}
}
}

View File

@ -1,43 +0,0 @@
/* Copyright (c) 2012 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.pitchedapps.frost.utils.iab;
/**
* Exception thrown when something went wrong with in-app billing.
* An IabException has an associated IabResult (an error).
* To get the IAB result that caused this exception to be thrown,
* call {@link #getResult()}.
*/
public class IabException extends Exception {
IabResult mResult;
public IabException(IabResult r) {
this(r, null);
}
public IabException(int response, String message) {
this(new IabResult(response, message));
}
public IabException(IabResult r, Exception cause) {
super(r.getMessage(), cause);
mResult = r;
}
public IabException(int response, String message, Exception cause) {
this(new IabResult(response, message), cause);
}
/** Returns the IAB result (error) that this exception signals. */
public IabResult getResult() { return mResult; }
}

View File

@ -1,45 +0,0 @@
/* Copyright (c) 2012 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.pitchedapps.frost.utils.iab;
/**
* Represents the result of an in-app billing operation.
* A result is composed of a response code (an integer) and possibly a
* message (String). You can get those by calling
* {@link #getResponse} and {@link #getMessage()}, respectively. You
* can also inquire whether a result is a success or a failure by
* calling {@link #isSuccess()} and {@link #isFailure()}.
*/
public class IabResult {
int mResponse;
String mMessage;
public IabResult(int response, String message) {
mResponse = response;
if (message == null || message.trim().length() == 0) {
mMessage = IabHelper.getResponseDesc(response);
}
else {
mMessage = message + " (response: " + IabHelper.getResponseDesc(response) + ")";
}
}
public int getResponse() { return mResponse; }
public String getMessage() { return mMessage; }
public boolean isSuccess() { return mResponse == IabHelper.BILLING_RESPONSE_RESULT_OK; }
public boolean isFailure() { return !isSuccess(); }
public String toString() { return "IabResult: " + getMessage(); }
}

View File

@ -1,91 +0,0 @@
/* Copyright (c) 2012 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.pitchedapps.frost.utils.iab;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Represents a block of information about in-app items.
* An Inventory is returned by such methods as {@link IabHelper#queryInventory}.
*/
public class Inventory {
Map<String,SkuDetails> mSkuMap = new HashMap<String,SkuDetails>();
Map<String,Purchase> mPurchaseMap = new HashMap<String,Purchase>();
Inventory() { }
/** Returns the listing details for an in-app product. */
public SkuDetails getSkuDetails(String sku) {
return mSkuMap.get(sku);
}
/** Returns purchase information for a given product, or null if there is no purchase. */
public Purchase getPurchase(String sku) {
return mPurchaseMap.get(sku);
}
/** Returns whether or not there exists a purchase of the given product. */
public boolean hasPurchase(String sku) {
return mPurchaseMap.containsKey(sku);
}
/** Return whether or not details about the given product are available. */
public boolean hasDetails(String sku) {
return mSkuMap.containsKey(sku);
}
/**
* Erase a purchase (locally) from the inventory, given its product ID. This just
* modifies the Inventory object locally and has no effect on the server! This is
* useful when you have an existing Inventory object which you know to be up to date,
* and you have just consumed an item successfully, which means that erasing its
* purchase data from the Inventory you already have is quicker than querying for
* a new Inventory.
*/
public void erasePurchase(String sku) {
if (mPurchaseMap.containsKey(sku)) mPurchaseMap.remove(sku);
}
/** Returns a list of all owned product IDs. */
List<String> getAllOwnedSkus() {
return new ArrayList<String>(mPurchaseMap.keySet());
}
/** Returns a list of all owned product IDs of a given type */
List<String> getAllOwnedSkus(String itemType) {
List<String> result = new ArrayList<String>();
for (Purchase p : mPurchaseMap.values()) {
if (p.getItemType().equals(itemType)) result.add(p.getSku());
}
return result;
}
/** Returns a list of all purchases. */
List<Purchase> getAllPurchases() {
return new ArrayList<Purchase>(mPurchaseMap.values());
}
void addSkuDetails(SkuDetails d) {
mSkuMap.put(d.getSku(), d);
}
void addPurchase(Purchase p) {
mPurchaseMap.put(p.getSku(), p);
}
}

View File

@ -1,66 +0,0 @@
/* Copyright (c) 2012 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.pitchedapps.frost.utils.iab;
import org.json.JSONException;
import org.json.JSONObject;
/**
* Represents an in-app billing purchase.
*/
public class Purchase {
String mItemType; // ITEM_TYPE_INAPP or ITEM_TYPE_SUBS
String mOrderId;
String mPackageName;
String mSku;
long mPurchaseTime;
int mPurchaseState;
String mDeveloperPayload;
String mToken;
String mOriginalJson;
String mSignature;
boolean mIsAutoRenewing;
public Purchase(String itemType, String jsonPurchaseInfo, String signature) throws JSONException {
mItemType = itemType;
mOriginalJson = jsonPurchaseInfo;
JSONObject o = new JSONObject(mOriginalJson);
mOrderId = o.optString("orderId");
mPackageName = o.optString("packageName");
mSku = o.optString("productId");
mPurchaseTime = o.optLong("purchaseTime");
mPurchaseState = o.optInt("purchaseState");
mDeveloperPayload = o.optString("developerPayload");
mToken = o.optString("token", o.optString("purchaseToken"));
mIsAutoRenewing = o.optBoolean("autoRenewing");
mSignature = signature;
}
public String getItemType() { return mItemType; }
public String getOrderId() { return mOrderId; }
public String getPackageName() { return mPackageName; }
public String getSku() { return mSku; }
public long getPurchaseTime() { return mPurchaseTime; }
public int getPurchaseState() { return mPurchaseState; }
public String getDeveloperPayload() { return mDeveloperPayload; }
public String getToken() { return mToken; }
public String getOriginalJson() { return mOriginalJson; }
public String getSignature() { return mSignature; }
public boolean isAutoRenewing() { return mIsAutoRenewing; }
@Override
public String toString() { return "PurchaseInfo(type:" + mItemType + "):" + mOriginalJson; }
}

View File

@ -1,121 +0,0 @@
/* Copyright (c) 2012 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.pitchedapps.frost.utils.iab;
import android.text.TextUtils;
import android.util.Base64;
import android.util.Log;
import java.security.InvalidKeyException;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;
import java.security.Signature;
import java.security.SignatureException;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.X509EncodedKeySpec;
/**
* Security-related methods. For a secure implementation, all of this code
* should be implemented on a server that communicates with the
* application on the device. For the sake of simplicity and clarity of this
* example, this code is included here and is executed on the device. If you
* must verify the purchases on the phone, you should obfuscate this code to
* make it harder for an attacker to replace the code with stubs that treat all
* purchases as verified.
*/
public class Security {
private static final String TAG = "IABUtil/Security";
private static final String KEY_FACTORY_ALGORITHM = "RSA";
private static final String SIGNATURE_ALGORITHM = "SHA1withRSA";
/**
* Verifies that the data was signed with the given signature, and returns
* the verified purchase. The data is in JSON format and signed
* with a private key. The data also contains the {@link PurchaseState}
* and product ID of the purchase.
* @param base64PublicKey the base64-encoded public key to use for verifying.
* @param signedData the signed JSON string (signed, not encrypted)
* @param signature the signature for the data, signed with the private key
*/
public static boolean verifyPurchase(String base64PublicKey, String signedData, String signature) {
if (TextUtils.isEmpty(signedData) || TextUtils.isEmpty(base64PublicKey) ||
TextUtils.isEmpty(signature)) {
Log.e(TAG, "Purchase verification failed: missing data.");
return false;
}
PublicKey key = Security.generatePublicKey(base64PublicKey);
return Security.verify(key, signedData, signature);
}
/**
* Generates a PublicKey instance from a string containing the
* Base64-encoded public key.
*
* @param encodedPublicKey Base64-encoded public key
* @throws IllegalArgumentException if encodedPublicKey is invalid
*/
public static PublicKey generatePublicKey(String encodedPublicKey) {
try {
byte[] decodedKey = Base64.decode(encodedPublicKey, Base64.DEFAULT);
KeyFactory keyFactory = KeyFactory.getInstance(KEY_FACTORY_ALGORITHM);
return keyFactory.generatePublic(new X509EncodedKeySpec(decodedKey));
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
} catch (InvalidKeySpecException e) {
Log.e(TAG, "Invalid key specification.");
throw new IllegalArgumentException(e);
}
}
/**
* Verifies that the signature from the server matches the computed
* signature on the data. Returns true if the data is correctly signed.
*
* @param publicKey public key associated with the developer account
* @param signedData signed data from server
* @param signature server signature
* @return true if the data and signature match
*/
public static boolean verify(PublicKey publicKey, String signedData, String signature) {
byte[] signatureBytes;
try {
signatureBytes = Base64.decode(signature, Base64.DEFAULT);
} catch (IllegalArgumentException e) {
Log.e(TAG, "Base64 decoding failed.");
return false;
}
try {
Signature sig = Signature.getInstance(SIGNATURE_ALGORITHM);
sig.initVerify(publicKey);
sig.update(signedData.getBytes());
if (!sig.verify(signatureBytes)) {
Log.e(TAG, "Signature verification failed.");
return false;
}
return true;
} catch (NoSuchAlgorithmException e) {
Log.e(TAG, "NoSuchAlgorithmException.");
} catch (InvalidKeyException e) {
Log.e(TAG, "Invalid key specification.");
} catch (SignatureException e) {
Log.e(TAG, "Signature exception.");
}
return false;
}
}

View File

@ -1,64 +0,0 @@
/* Copyright (c) 2012 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.pitchedapps.frost.utils.iab;
import org.json.JSONException;
import org.json.JSONObject;
/**
* Represents an in-app product's listing details.
*/
public class SkuDetails {
private final String mItemType;
private final String mSku;
private final String mType;
private final String mPrice;
private final long mPriceAmountMicros;
private final String mPriceCurrencyCode;
private final String mTitle;
private final String mDescription;
private final String mJson;
public SkuDetails(String jsonSkuDetails) throws JSONException {
this(IabHelper.ITEM_TYPE_INAPP, jsonSkuDetails);
}
public SkuDetails(String itemType, String jsonSkuDetails) throws JSONException {
mItemType = itemType;
mJson = jsonSkuDetails;
JSONObject o = new JSONObject(mJson);
mSku = o.optString("productId");
mType = o.optString("type");
mPrice = o.optString("price");
mPriceAmountMicros = o.optLong("price_amount_micros");
mPriceCurrencyCode = o.optString("price_currency_code");
mTitle = o.optString("title");
mDescription = o.optString("description");
}
public String getSku() { return mSku; }
public String getType() { return mType; }
public String getPrice() { return mPrice; }
public long getPriceAmountMicros() { return mPriceAmountMicros; }
public String getPriceCurrencyCode() { return mPriceCurrencyCode; }
public String getTitle() { return mTitle; }
public String getDescription() { return mDescription; }
@Override
public String toString() {
return "SkuDetails:" + mJson;
}
}

View File

@ -12,9 +12,7 @@ import ca.allanwang.kau.utils.toDrawable
import com.bumptech.glide.Glide
import com.bumptech.glide.load.DataSource
import com.bumptech.glide.load.engine.GlideException
import com.bumptech.glide.load.resource.bitmap.CircleCrop
import com.bumptech.glide.request.RequestListener
import com.bumptech.glide.request.RequestOptions
import com.bumptech.glide.request.target.Target
import com.mikepenz.google_material_typeface_library.GoogleMaterial
import com.pitchedapps.frost.R

View File

@ -4,7 +4,6 @@ import android.content.Context
import android.support.v4.view.ViewPager
import android.util.AttributeSet
import android.view.MotionEvent
import com.pitchedapps.frost.utils.L
import com.pitchedapps.frost.utils.Prefs
/**

View File

@ -2,8 +2,6 @@ package com.pitchedapps.frost.web
import android.content.Context
import android.webkit.JavascriptInterface
import ca.allanwang.kau.utils.startActivity
import com.pitchedapps.frost.activities.ImageActivity
import com.pitchedapps.frost.activities.MainActivity
import com.pitchedapps.frost.dbflow.CookieModel
import com.pitchedapps.frost.facebook.formattedFbUrl

View File

@ -4,7 +4,10 @@ import android.annotation.SuppressLint
import android.content.Context
import android.util.AttributeSet
import android.view.View
import android.webkit.*
import android.webkit.ConsoleMessage
import android.webkit.CookieManager
import android.webkit.WebChromeClient
import android.webkit.WebView
import ca.allanwang.kau.utils.fadeIn
import com.pitchedapps.frost.R
import com.pitchedapps.frost.dbflow.CookieModel

View File

@ -2,7 +2,6 @@ package com.pitchedapps.frost.web
import android.annotation.SuppressLint
import android.content.Context
import android.view.View
import android.webkit.JavascriptInterface
import android.webkit.WebView
import ca.allanwang.kau.searchview.SearchItem

View File

@ -54,7 +54,7 @@
<string name="uh_oh">Uh Oh</string>
<string name="reload">Reload</string>
<string name="play_store_not_pro">It seems like you are a pro user, but we couldn\'t find your purchasing info. If this error persists, please try clearing the Play Store cache and reinstalling the app.</string>
<string name="play_store_not_found">This app doesn\'t seem to be installed from the Play Store. Please reinstall if this is an issue.</string>
<string name="play_store_unsupported">It seems like app version can\'t purchase pro. Please reinstall from the play store if this is a persisting issue.</string>
<string name="play_store_not_found_pro_query">This is a pro feature, but this app doesn\'t seem to be installed from the Play Store. Please reinstall if this is an issue.</string>
<string name="play_store_billing_error">Something went wrong. Please try again later.</string>
<string name="play_thank_you">Thank you!</string>

View File

@ -9,7 +9,7 @@
-->
<version title="Beta Updates" />
<item text="Fix regex bug for some devices" />
<item text="Fix notification text" />
<item text="Update round icons" />
@ -25,7 +25,7 @@
<item text="Start filtering out unnecessary loads" />
<item text="Fix notification duplicates" />
<item text="Fix long pressing album images" />
<item text="Add friend request tab in nav bar" />
<item text="Add friend request tab in nav bar" />
<item text="Aggressively filter nonrecent posts in recents mode" />
<item text="Add download option for full sized images" />
<item text="Fix rounded icons" />

View File

@ -17,11 +17,11 @@ MIN_SDK=21
TARGET_SDK=26
BUILD_TOOLS=26.0.0
KAU=bb91aea
KAU=3.0
KOTLIN=1.1.3-2
CRASHLYTICS=2.6.8
DBFLOW=4.0.5
GLIDE=4.0.0-RC1
IAB=1.0.42
IICON_COMMUNITY=1.9.32.2
IICON_MATERIAL=2.2.0.3
JSOUP=1.10.3