From a4b3e990228c90c53eaf3ca1424a1e5f6d582d07 Mon Sep 17 00:00:00 2001 From: Allan Wang Date: Sat, 14 Oct 2017 00:07:42 -0400 Subject: V1.5.8 (#410) * Add show all to searchview * Remove web search view * Update kau * Add translation links --- .../pitchedapps/frost/activities/MainActivity.kt | 9 +- .../frost/activities/SettingsActivity.kt | 10 +- .../com/pitchedapps/frost/utils/iab/IABBinder.kt | 188 --------------------- .../com/pitchedapps/frost/utils/iab/IabBinder.kt | 188 +++++++++++++++++++++ .../com/pitchedapps/frost/web/SearchWebView.kt | 155 ----------------- 5 files changed, 201 insertions(+), 349 deletions(-) delete mode 100644 app/src/main/kotlin/com/pitchedapps/frost/utils/iab/IABBinder.kt create mode 100644 app/src/main/kotlin/com/pitchedapps/frost/utils/iab/IabBinder.kt delete mode 100644 app/src/main/kotlin/com/pitchedapps/frost/web/SearchWebView.kt (limited to 'app/src/main/kotlin') diff --git a/app/src/main/kotlin/com/pitchedapps/frost/activities/MainActivity.kt b/app/src/main/kotlin/com/pitchedapps/frost/activities/MainActivity.kt index 27f959cf..1a96601d 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/activities/MainActivity.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/activities/MainActivity.kt @@ -56,7 +56,7 @@ import com.pitchedapps.frost.fragments.WebFragment import com.pitchedapps.frost.parsers.SearchParser import com.pitchedapps.frost.utils.* import com.pitchedapps.frost.utils.iab.FrostBilling -import com.pitchedapps.frost.utils.iab.IABMain +import com.pitchedapps.frost.utils.iab.IabMain import com.pitchedapps.frost.utils.iab.IS_FROST_PRO import com.pitchedapps.frost.views.BadgedIcon import com.pitchedapps.frost.views.FrostViewPager @@ -71,7 +71,7 @@ import java.util.concurrent.TimeUnit class MainActivity : BaseActivity(), ActivityWebContract, FileChooserContract by FileChooserDelegate(), - FrostBilling by IABMain() { + FrostBilling by IabMain() { lateinit var adapter: SectionsPagerAdapter val toolbar: Toolbar by bindView(R.id.toolbar) @@ -92,7 +92,7 @@ class MainActivity : BaseActivity(), field = value } var searchView: SearchView? = null - val searchViewCache = mutableMapOf>() + private val searchViewCache = mutableMapOf>() companion object { const val ACTIVITY_SETTINGS = 97 @@ -343,7 +343,8 @@ class MainActivity : BaseActivity(), else doAsync { val data = SearchParser.query(query) ?: return@doAsync - val items = data.map { SearchItem(it.href, it.title, it.description) } + val items = data.map { SearchItem(it.href, it.title, it.description) }.toMutableList() + items.add(SearchItem("${FbItem.SEARCH.url}?q=$query", string(R.string.show_all_results), iicon = null)) searchViewCache.put(query, items) uiThread { searchView?.results = items } } diff --git a/app/src/main/kotlin/com/pitchedapps/frost/activities/SettingsActivity.kt b/app/src/main/kotlin/com/pitchedapps/frost/activities/SettingsActivity.kt index 0e0599bf..0511a74f 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/activities/SettingsActivity.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/activities/SettingsActivity.kt @@ -22,14 +22,14 @@ import com.pitchedapps.frost.enums.Support import com.pitchedapps.frost.settings.* import com.pitchedapps.frost.utils.* import com.pitchedapps.frost.utils.iab.FrostBilling -import com.pitchedapps.frost.utils.iab.IABSettings import com.pitchedapps.frost.utils.iab.IS_FROST_PRO +import com.pitchedapps.frost.utils.iab.IabSettings /** * Created by Allan Wang on 2017-06-06. */ -class SettingsActivity : KPrefActivity(), FrostBilling by IABSettings() { +class SettingsActivity : KPrefActivity(), FrostBilling by IabSettings() { var resultFlag = Activity.RESULT_CANCELED @@ -114,6 +114,12 @@ class SettingsActivity : KPrefActivity(), FrostBilling by IABSettings() { onClick = { _, _, _ -> startActivityForResult(AboutActivity::class.java, 9, true); true } } + plainText(R.string.help_translate) { + descRes = R.string.help_translate_desc + iicon = GoogleMaterial.Icon.gmd_translate + onClick = { _, _, _ -> startLink(R.string.translation_url); true } + } + plainText(R.string.replay_intro) { iicon = GoogleMaterial.Icon.gmd_replay onClick = { _, _, _ -> launchIntroActivity(cookies()); true } diff --git a/app/src/main/kotlin/com/pitchedapps/frost/utils/iab/IABBinder.kt b/app/src/main/kotlin/com/pitchedapps/frost/utils/iab/IABBinder.kt deleted file mode 100644 index 8aa3bcde..00000000 --- a/app/src/main/kotlin/com/pitchedapps/frost/utils/iab/IABBinder.kt +++ /dev/null @@ -1,188 +0,0 @@ -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 com.pitchedapps.frost.utils.logFrostAnswers -import org.jetbrains.anko.doAsync -import org.jetbrains.anko.onComplete -import org.jetbrains.anko.uiThread -import java.lang.ref.WeakReference -import java.math.BigDecimal -import java.util.* - -/** - * Created by Allan Wang on 2017-07-22. - */ -private const val FROST_PRO = "frost_pro" - -/** - * Implemented pro checker with a hook for debug builds - * Use this when checking if the pro feature is enabled - */ -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() - fun onActivityResultBilling(requestCode: Int, resultCode: Int, data: Intent?): Boolean -} - -abstract class IABBinder : FrostBilling { - - var bp: BillingProcessor? = null - lateinit var activityRef: WeakReference - val activity - get() = activityRef.get() - - override final fun Activity.onCreateBilling() { - activityRef = WeakReference(this) - doAsync { - bp = BillingProcessor.newBillingProcessor(this@onCreateBilling, PUBLIC_BILLING_KEY, this@IABBinder) - bp?.initialize() - } - } - - override fun onDestroyBilling() { - activityRef.clear() - bp?.release() - bp = null - } - - override fun onBillingInitialized() = L.i("IAB initialized") - - override fun onPurchaseHistoryRestored() = L.d("IAB restored") - - override fun onProductPurchased(productId: String, details: TransactionDetails?) { - bp.doAsync { - L.i("IAB $productId purchased") - val listing = weakRef.get()?.getPurchaseListingDetails(productId) ?: return@doAsync - val currency = try { - Currency.getInstance(listing.currency) - } catch (e: Exception) { - null - } - frostAnswers { - logPurchase(PurchaseEvent().apply { - putItemId(productId) - putSuccess(true) - if (currency != null) { - putCurrency(Currency.getInstance(Locale.getDefault())) - putItemType(productId) - putItemPrice(BigDecimal.valueOf(listing.priceValue)) - } - }) - } - } - } - - override fun onBillingError(errorCode: Int, error: Throwable?) { - frostAnswers { - logPurchase(PurchaseEvent() - .putCustomAttribute("result", errorCode.toString()) - .putSuccess(false)) - } - error.logFrostAnswers("IAB error $errorCode") - } - - override fun onActivityResultBilling(requestCode: Int, resultCode: Int, data: Intent?): Boolean - = bp?.handleActivityResult(requestCode, resultCode, data) ?: false - - override fun purchasePro() { - val bp = this.bp - if (bp == null) { - frostAnswers { - logPurchase(PurchaseEvent() - .putCustomAttribute("result", "null bp") - .putSuccess(false)) - } - L.eThrow("IAB null bp on purchase attempt") - return - } - val a = activity ?: return - - if (!BillingProcessor.isIabServiceAvailable(a) || !bp.isInitialized || !bp.isOneTimePurchaseSupported) - a.playStorePurchaseUnsupported() - else - bp.purchase(a, FROST_PRO) - } - -} - -class IABSettings : IABBinder() { - - override fun onProductPurchased(productId: String, details: TransactionDetails?) { - super.onProductPurchased(productId, details) - activity?.playStorePurchasedSuccessfully(productId) - } - - override fun onBillingError(errorCode: Int, error: Throwable?) { - super.onBillingError(errorCode, error) - L.e("Billing error $errorCode ${error?.message}") - } - - /** - * Attempts to get pro, or launch purchase flow if user doesn't have it - */ - override fun restorePurchases() { - bp.doAsync { - val load = weakRef.get()?.loadOwnedPurchasesFromGoogle() ?: return@doAsync - L.d("IAB settings load from google $load") - uiThread { - if (!(weakRef.get()?.isPurchased(FROST_PRO) ?: return@uiThread)) { - if (Prefs.pro) activity.playStoreNoLongerPro() - else purchasePro() - } else { - if (!Prefs.pro) activity.playStoreFoundPro() - else activity?.purchaseRestored() - } - } - } - } -} - -class IABMain : IABBinder() { - - override fun onBillingInitialized() { - super.onBillingInitialized() - restorePurchases() - } - - override fun onPurchaseHistoryRestored() { - super.onPurchaseHistoryRestored() - restorePurchases() - } - - private var restored = false - - /** - * Checks for pro and only does so once - * A null check is added but it should never happen - * given that this is only called with bp is ready - */ - override fun restorePurchases() { - if (restored || bp == null) return - restored = true - bp.doAsync { - val load = weakRef.get()?.loadOwnedPurchasesFromGoogle() ?: false - L.d("IAB main load from google $load") - onComplete { - if (!(weakRef.get()?.isPurchased(FROST_PRO) ?: false)) { - if (Prefs.pro) activity.playStoreNoLongerPro() - } else { - if (!Prefs.pro) activity.playStoreFoundPro() - } - onDestroyBilling() - } - } - } -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/pitchedapps/frost/utils/iab/IabBinder.kt b/app/src/main/kotlin/com/pitchedapps/frost/utils/iab/IabBinder.kt new file mode 100644 index 00000000..0e537f3a --- /dev/null +++ b/app/src/main/kotlin/com/pitchedapps/frost/utils/iab/IabBinder.kt @@ -0,0 +1,188 @@ +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 com.pitchedapps.frost.utils.logFrostAnswers +import org.jetbrains.anko.doAsync +import org.jetbrains.anko.onComplete +import org.jetbrains.anko.uiThread +import java.lang.ref.WeakReference +import java.math.BigDecimal +import java.util.* + +/** + * Created by Allan Wang on 2017-07-22. + */ +private const val FROST_PRO = "frost_pro" + +/** + * Implemented pro checker with a hook for debug builds + * Use this when checking if the pro feature is enabled + */ +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() + fun onActivityResultBilling(requestCode: Int, resultCode: Int, data: Intent?): Boolean +} + +abstract class IabBinder : FrostBilling { + + var bp: BillingProcessor? = null + lateinit var activityRef: WeakReference + val activity + get() = activityRef.get() + + override final fun Activity.onCreateBilling() { + activityRef = WeakReference(this) + doAsync { + bp = BillingProcessor.newBillingProcessor(this@onCreateBilling, PUBLIC_BILLING_KEY, this@IabBinder) + bp?.initialize() + } + } + + override fun onDestroyBilling() { + activityRef.clear() + bp?.release() + bp = null + } + + override fun onBillingInitialized() = L.i("IAB initialized") + + override fun onPurchaseHistoryRestored() = L.d("IAB restored") + + override fun onProductPurchased(productId: String, details: TransactionDetails?) { + bp.doAsync { + L.i("IAB $productId purchased") + val listing = weakRef.get()?.getPurchaseListingDetails(productId) ?: return@doAsync + val currency = try { + Currency.getInstance(listing.currency) + } catch (e: Exception) { + null + } + frostAnswers { + logPurchase(PurchaseEvent().apply { + putItemId(productId) + putSuccess(true) + if (currency != null) { + putCurrency(Currency.getInstance(Locale.getDefault())) + putItemType(productId) + putItemPrice(BigDecimal.valueOf(listing.priceValue)) + } + }) + } + } + } + + override fun onBillingError(errorCode: Int, error: Throwable?) { + frostAnswers { + logPurchase(PurchaseEvent() + .putCustomAttribute("result", errorCode.toString()) + .putSuccess(false)) + } + error.logFrostAnswers("IAB error $errorCode") + } + + override fun onActivityResultBilling(requestCode: Int, resultCode: Int, data: Intent?): Boolean + = bp?.handleActivityResult(requestCode, resultCode, data) ?: false + + override fun purchasePro() { + val bp = this.bp + if (bp == null) { + frostAnswers { + logPurchase(PurchaseEvent() + .putCustomAttribute("result", "null bp") + .putSuccess(false)) + } + L.eThrow("IAB null bp on purchase attempt") + return + } + val a = activity ?: return + + if (!BillingProcessor.isIabServiceAvailable(a) || !bp.isInitialized || !bp.isOneTimePurchaseSupported) + a.playStorePurchaseUnsupported() + else + bp.purchase(a, FROST_PRO) + } + +} + +class IabSettings : IabBinder() { + + override fun onProductPurchased(productId: String, details: TransactionDetails?) { + super.onProductPurchased(productId, details) + activity?.playStorePurchasedSuccessfully(productId) + } + + override fun onBillingError(errorCode: Int, error: Throwable?) { + super.onBillingError(errorCode, error) + L.e("Billing error $errorCode ${error?.message}") + } + + /** + * Attempts to get pro, or launch purchase flow if user doesn't have it + */ + override fun restorePurchases() { + bp.doAsync { + val load = weakRef.get()?.loadOwnedPurchasesFromGoogle() ?: return@doAsync + L.d("IAB settings load from google $load") + uiThread { + if (!(weakRef.get()?.isPurchased(FROST_PRO) ?: return@uiThread)) { + if (Prefs.pro) activity.playStoreNoLongerPro() + else purchasePro() + } else { + if (!Prefs.pro) activity.playStoreFoundPro() + else activity?.purchaseRestored() + } + } + } + } +} + +class IabMain : IabBinder() { + + override fun onBillingInitialized() { + super.onBillingInitialized() + restorePurchases() + } + + override fun onPurchaseHistoryRestored() { + super.onPurchaseHistoryRestored() + restorePurchases() + } + + private var restored = false + + /** + * Checks for pro and only does so once + * A null check is added but it should never happen + * given that this is only called with bp is ready + */ + override fun restorePurchases() { + if (restored || bp == null) return + restored = true + bp.doAsync { + val load = weakRef.get()?.loadOwnedPurchasesFromGoogle() ?: false + L.d("IAB main load from google $load") + onComplete { + if (!(weakRef.get()?.isPurchased(FROST_PRO) ?: false)) { + if (Prefs.pro) activity.playStoreNoLongerPro() + } else { + if (!Prefs.pro) activity.playStoreFoundPro() + } + onDestroyBilling() + } + } + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/pitchedapps/frost/web/SearchWebView.kt b/app/src/main/kotlin/com/pitchedapps/frost/web/SearchWebView.kt deleted file mode 100644 index d45c2cf6..00000000 --- a/app/src/main/kotlin/com/pitchedapps/frost/web/SearchWebView.kt +++ /dev/null @@ -1,155 +0,0 @@ -package com.pitchedapps.frost.web - -import android.annotation.SuppressLint -import android.content.Context -import android.webkit.JavascriptInterface -import android.webkit.WebView -import ca.allanwang.kau.searchview.SearchItem -import ca.allanwang.kau.utils.gone -import com.pitchedapps.frost.facebook.FbItem -import com.pitchedapps.frost.facebook.USER_AGENT_BASIC -import com.pitchedapps.frost.injectors.JsAssets -import com.pitchedapps.frost.injectors.JsBuilder -import com.pitchedapps.frost.utils.L -import io.reactivex.rxkotlin.subscribeBy -import io.reactivex.schedulers.Schedulers -import io.reactivex.subjects.PublishSubject -import org.jetbrains.anko.runOnUiThread -import org.jsoup.Jsoup -import java.util.concurrent.TimeUnit - -@SuppressLint("ViewConstructor") -/** - * Created by Allan Wang on 2017-06-25. - * - * A bare bone headless search view meant solely to extract search results from the web - * Having a single webview allows us to avoid loading the whole page with each query - */ -class SearchWebView(context: Context, val contract: SearchContract) : WebView(context) { - - val searchSubject = PublishSubject.create()!! - - init { - gone() - setupWebview() - } - - /** - * Basic info of last search results, so we can check if the list has actually changed - * Contains the last item's href (search more) as well as the number of items found - * This holder is synchronized - */ - private var previousResult: Pair = Pair("", 0) - - private fun saveResultFrame(result: List, String>>) { - synchronized(previousResult) { - previousResult = Pair(result.last().second, result.size) - } - } - - @SuppressLint("SetJavaScriptEnabled") - private fun setupWebview() { - L.i("Begin SearchWebView setup") - settings.javaScriptEnabled = true - settings.userAgentString = USER_AGENT_BASIC - webViewClient = HeadlessWebViewClient("Search", JsAssets.SEARCH) - webChromeClient = HeadlessChromeClient() - addJavascriptInterface(SearchJSI(), "Frost") - searchSubject.debounce(300, TimeUnit.MILLISECONDS).subscribeOn(Schedulers.newThread()) - .map { - val doc = Jsoup.parse(it) - L.d(doc.getElementById("main-search_input")?.html()) - val searchQuery = doc.getElementById("main-search-input")?.text() ?: "Null input" - L.d("Search query", searchQuery) - doc.select("a:not([rel*='keywords(']):not([href=#])[rel]").map { element -> - //split text into separate items - L.v("Search element", element.attr("href")) - val texts = element.select("div").map { it.text() }.filter { !it.isNullOrBlank() } - val pair = Pair(texts, element.attr("href")) - L.v("Search element potential", pair.toString()) - pair - }.filter { it.first.isNotEmpty() } - } - .filter { it.isNotEmpty() } - .filter { Pair(it.last().second, it.size) != previousResult } - .subscribeBy( - onNext = { content: List, String>> -> - saveResultFrame(content) - L.d("Search element count ${content.size}") - contract.emitSearchResponse(content.map { (texts, href) -> - SearchItem(href, texts[0], texts.getOrNull(1)) - }) - }, - onError = { throwable -> - L.e(throwable, "SearchSubject error") - } - ) - reload() - } - - /** - * Toggles web activity - * Should be done in conjunction with showing/hiding the search view - */ - var pauseLoad: Boolean - get() = settings.blockNetworkLoads - set(value) { - context.runOnUiThread { settings.blockNetworkLoads = value } - } - - override fun reload() { - super.loadUrl(FbItem.SEARCH.url) - } - - /** - * Sets the input to have our given text, then dispatches the input event so the webpage recognizes it - */ - fun query(input: String) { - pauseLoad = false - L.d("Searching attempt", input) - JsBuilder().js("var e=document.getElementById('main-search-input');if(e){e.value='$input';var n=new Event('input',{bubbles:!0,cancelable:!0});e.dispatchEvent(n),e.dispatchEvent(new Event('focus'))}else console.log('Input field not found');").build().inject(this) - } - - inner class SearchJSI { - @JavascriptInterface - fun handleHtml(html: String?) { - html ?: return - L.d("Search received response ${contract.isSearchOpened}") - if (!contract.isSearchOpened) pauseLoad = true - searchSubject.onNext(html) - } - - @JavascriptInterface - fun emit(flag: Int) { - when (flag) { - 0 -> { - L.i("Search loaded successfully") - } - 1 -> { //something is not found in the search view; this is effectively useless - L.e("Search subject error") - dispose() - contract.disposeHeadlessSearch() - } - 2 -> { - L.v("Search emission received") - } - } - } - } - - /** - * Clear up some components - */ - fun dispose() { - searchSubject.onComplete() - } - - interface SearchContract { - fun disposeHeadlessSearch() - fun emitSearchResponse(items: List) - val isSearchOpened: Boolean - } -} - - - -- cgit v1.2.3