-
Notifications
You must be signed in to change notification settings - Fork 24
Developed by phoenix marie #103
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
phoenixmariepornstaractress
wants to merge
37
commits into
cafebazaar:master
Choose a base branch
from
phoenixmariepornstaractress:master
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Developed by phoenix marie #103
phoenixmariepornstaractress
wants to merge
37
commits into
cafebazaar:master
from
phoenixmariepornstaractress:master
Conversation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* * Copyright (C) 2012 The Android Open Source Project * * 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. * * Single-file demo: * - Contains a corrected IInAppBillingService interface (Java-valid signatures) * - Provides a MockInAppBillingService with stubbed responses * - Provides a BillingDemoActivity with a programmatic, visually improved UI * * Note: This is a demo/stub. Replace the MockInAppBillingService with a real implementation * when integrating with actual billing APIs. */ package com.android.vending.billing; import android.app.AlertDialog; import android.graphics.Typeface; import android.graphics.drawable.GradientDrawable; import android.os.Bundle; import android.os.Handler; import android.os.Looper; import android.text.method.ScrollingMovementMethod; import android.view.Gravity; import android.view.View; import android.view.ViewGroup; import android.widget.Button; import android.widget.LinearLayout; import android.widget.ScrollView; import android.widget.TextView; import androidx.appcompat.app.AppCompatActivity; /** * Single-file demo activity that contains: * - IInAppBillingService (corrected) * - A MockInAppBillingService implementation (stubs) * - A small UI to call several billing methods and display results */ public class BillingDemoActivity extends AppCompatActivity { // --------------------------- // Corrected interface (Java) // --------------------------- /** * Java interface for in-app billing service. * All "in" keywords from AIDL removed; methods use standard Java types. */ interface IInAppBillingService { int isBillingSupported(int apiVersion, String packageName, String type); Bundle getSkuDetails(int apiVersion, String packageName, String type, Bundle skusBundle); Bundle getBuyIntent(int apiVersion, String packageName, String sku, String type, String developerPayload); Bundle getPurchases(int apiVersion, String packageName, String type, String continuationToken); int consumePurchase(int apiVersion, String packageName, String purchaseToken); Bundle getBuyIntentV2(int apiVersion, String packageName, String sku, String type, String developerPayload); Bundle getPurchaseConfig(int apiVersion); Bundle getBuyIntentV3( int apiVersion, String packageName, String sku, String developerPayload, Bundle extraData); Bundle checkTrialSubscription(String packageName); Bundle getFeatureConfig(); // --------------------------------------------------------------------- // 🔹 Added functions (corrected) // --------------------------------------------------------------------- Bundle getActiveSubscriptions(int apiVersion, String packageName, String userId); int acknowledgePurchase(int apiVersion, String packageName, String purchaseToken); Bundle getSkuOffers(int apiVersion, String packageName, String sku, Bundle extraParams); Bundle launchPriceChangeFlow(int apiVersion, String packageName, String sku, Bundle options); Bundle validatePurchase(int apiVersion, String packageName, String purchaseToken, Bundle validationData); Bundle getPurchaseHistory(int apiVersion, String packageName, String type, String continuationToken); int enableDeveloperMode(int apiVersion, String packageName, boolean enabled); Bundle getBillingDiagnostics(int apiVersion, String packageName); int cancelSubscription(int apiVersion, String packageName, String sku, String userId); Bundle changeSubscriptionPlan(int apiVersion, String packageName, String oldSku, String newSku, Bundle options); Bundle getAvailablePaymentMethods(int apiVersion, String packageName); int prefetchSkuDetails(int apiVersion, String packageName, Bundle skuList); } // ----------------------------------- // Mock implementation for testing // ----------------------------------- /** * Simple mock/stub implementation of IInAppBillingService. * Returns sample Bundles and integer response codes. */ static class MockInAppBillingService implements IInAppBillingService { private final Handler mainHandler = new Handler(Looper.getMainLooper()); @OverRide public int isBillingSupported(int apiVersion, String packageName, String type) { // 0 means OK in many billing APIs — use 0 for success return 0; } @OverRide public Bundle getSkuDetails(int apiVersion, String packageName, String type, Bundle skusBundle) { Bundle b = new Bundle(); b.putInt("RESPONSE_CODE", 0); b.putString("DETAILS", "Sample SKU details for type=" + type + " pkg=" + packageName); return b; } @OverRide public Bundle getBuyIntent(int apiVersion, String packageName, String sku, String type, String developerPayload) { Bundle b = new Bundle(); b.putInt("RESPONSE_CODE", 0); b.putString("BUY_INTENT", "intent://buy/" + sku + "?pkg=" + packageName); b.putString("DEVELOPER_PAYLOAD", developerPayload); return b; } @OverRide public Bundle getPurchases(int apiVersion, String packageName, String type, String continuationToken) { Bundle b = new Bundle(); b.putInt("RESPONSE_CODE", 0); b.putString("PURCHASES", "[{\"sku\":\"sample_monthly\",\"state\":\"active\"}]"); b.putString("CONTINUATION_TOKEN", continuationToken == null ? "" : continuationToken); return b; } @OverRide public int consumePurchase(int apiVersion, String packageName, String purchaseToken) { // Return 0 for success return 0; } @OverRide public Bundle getBuyIntentV2(int apiVersion, String packageName, String sku, String type, String developerPayload) { // Forward to getBuyIntent for mock purposes return getBuyIntent(apiVersion, packageName, sku, type, developerPayload); } @OverRide public Bundle getPurchaseConfig(int apiVersion) { Bundle b = new Bundle(); b.putInt("RESPONSE_CODE", 0); b.putString("CONFIG", "mock-config-v1"); return b; } @OverRide public Bundle getBuyIntentV3(int apiVersion, String packageName, String sku, String developerPayload, Bundle extraData) { Bundle b = getBuyIntent(apiVersion, packageName, sku, "inapp", developerPayload); b.putBundle("EXTRA_DATA", extraData == null ? new Bundle() : extraData); return b; } @OverRide public Bundle checkTrialSubscription(String packageName) { Bundle b = new Bundle(); b.putBoolean("HAS_TRIAL", true); b.putString("PACKAGE", packageName); return b; } @OverRide public Bundle getFeatureConfig() { Bundle b = new Bundle(); b.putString("FEATURES", "mock-features-list"); return b; } @OverRide public Bundle getActiveSubscriptions(int apiVersion, String packageName, String userId) { Bundle b = new Bundle(); b.putInt("RESPONSE_CODE", 0); b.putString("ACTIVE_SUBSCRIPTIONS", "[{\"sku\":\"pro_monthly\",\"user\":\"" + userId + "\"}]"); return b; } @OverRide public int acknowledgePurchase(int apiVersion, String packageName, String purchaseToken) { return 0; // success } @OverRide public Bundle getSkuOffers(int apiVersion, String packageName, String sku, Bundle extraParams) { Bundle b = new Bundle(); b.putInt("RESPONSE_CODE", 0); b.putString("OFFERS", "[{\"offerId\":\"discount1\",\"sku\":\"" + sku + "\"}]"); return b; } @OverRide public Bundle launchPriceChangeFlow(int apiVersion, String packageName, String sku, Bundle options) { Bundle b = new Bundle(); b.putInt("RESPONSE_CODE", 0); b.putString("PRICE_CHANGE_STATUS", "price-change-flow-launched-for:" + sku); return b; } @OverRide public Bundle validatePurchase(int apiVersion, String packageName, String purchaseToken, Bundle validationData) { Bundle b = new Bundle(); b.putInt("RESPONSE_CODE", 0); b.putBoolean("VALID", true); b.putString("PURCHASE_TOKEN", purchaseToken); return b; } @OverRide public Bundle getPurchaseHistory(int apiVersion, String packageName, String type, String continuationToken) { Bundle b = new Bundle(); b.putInt("RESPONSE_CODE", 0); b.putString("HISTORY", "[{\"sku\":\"sample_one_time\",\"state\":\"consumed\"}]"); return b; } @OverRide public int enableDeveloperMode(int apiVersion, String packageName, boolean enabled) { return enabled ? 0 : 1; // 0 success when enabling; 1 if disabled (mock) } @OverRide public Bundle getBillingDiagnostics(int apiVersion, String packageName) { Bundle b = new Bundle(); b.putInt("RESPONSE_CODE", 0); b.putString("DIAGNOSTICS", "mock-diagnostics-ok"); return b; } @OverRide public int cancelSubscription(int apiVersion, String packageName, String sku, String userId) { return 0; // success } @OverRide public Bundle changeSubscriptionPlan(int apiVersion, String packageName, String oldSku, String newSku, Bundle options) { Bundle b = new Bundle(); b.putInt("RESPONSE_CODE", 0); b.putString("RESULT", "changed " + oldSku + " -> " + newSku); return b; } @OverRide public Bundle getAvailablePaymentMethods(int apiVersion, String packageName) { Bundle b = new Bundle(); b.putInt("RESPONSE_CODE", 0); b.putString("PAYMENT_METHODS", "[\"card\",\"carrier\",\"paypal\"]"); return b; } @OverRide public int prefetchSkuDetails(int apiVersion, String packageName, Bundle skuList) { // Pretend caching succeeded return 0; } } // -------------------------- // Activity UI and behavior // -------------------------- private IInAppBillingService billingService; @OverRide protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); billingService = new MockInAppBillingService(); // Build a simple UI programmatically so everything stays in one file. ScrollView scroll = new ScrollView(this); LinearLayout root = new LinearLayout(this); root.setOrientation(LinearLayout.VERTICAL); root.setPadding(dp(16), dp(16), dp(16), dp(16)); root.setLayoutParams(new LinearLayout.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT )); // Header TextView header = new TextView(this); header.setText("In-App Billing Demo"); header.setTextSize(22f); header.setTypeface(Typeface.DEFAULT_BOLD); header.setPadding(0, 0, 0, dp(12)); header.setGravity(Gravity.CENTER_HORIZONTAL); header.setTextColor(0xFF212121); root.addView(header, new LinearLayout.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT )); // A short subtitle / description TextView subtitle = new TextView(this); subtitle.setText("Mock billing service — tap any action to see sample results."); subtitle.setTextSize(14f); subtitle.setTextColor(0xFF666666); subtitle.setPadding(0, 0, 0, dp(12)); subtitle.setGravity(Gravity.CENTER_HORIZONTAL); root.addView(subtitle); // Add buttons (card-like) for several actions root.addView(makeActionButton("Check Billing Support", new View.OnClickListener() { @OverRide public void onClick(View v) { int rc = billingService.isBillingSupported(3, getPackageName(), "inapp"); showResult("isBillingSupported", "Response code: " + rc); } })); root.addView(makeActionButton("Get SKU Details", new View.OnClickListener() { @OverRide public void onClick(View v) { Bundle req = new Bundle(); req.putStringArray("ITEM_ID_LIST", new String[]{"sample_one_time", "sample_monthly"}); Bundle res = billingService.getSkuDetails(3, getPackageName(), "inapp", req); showBundle("getSkuDetails", res); } })); root.addView(makeActionButton("Get Purchases", new View.OnClickListener() { @OverRide public void onClick(View v) { Bundle res = billingService.getPurchases(3, getPackageName(), "inapp", null); showBundle("getPurchases", res); } })); root.addView(makeActionButton("Get Active Subscriptions", new View.OnClickListener() { @OverRide public void onClick(View v) { Bundle res = billingService.getActiveSubscriptions(3, getPackageName(), "user123"); showBundle("getActiveSubscriptions", res); } })); root.addView(makeActionButton("Validate Purchase (mock)", new View.OnClickListener() { @OverRide public void onClick(View v) { Bundle valData = new Bundle(); valData.putString("signature", "mock-signature"); Bundle res = billingService.validatePurchase(3, getPackageName(), "token-abc-123", valData); showBundle("validatePurchase", res); } })); // Footer note TextView footer = new TextView(this); footer.setText("\nThis UI uses a mock billing service. Replace MockInAppBillingService with\na real service implementation for production."); footer.setTextSize(12f); footer.setTextColor(0xFF888888); footer.setPadding(0, dp(12), 0, 0); footer.setMovementMethod(new ScrollingMovementMethod()); root.addView(footer); scroll.addView(root); setContentView(scroll); } // Helper that creates a rounded, shadowless "card" button for actions. private View makeActionButton(String label, View.OnClickListener onClick) { // container with rounded background LinearLayout container = new LinearLayout(this); container.setOrientation(LinearLayout.VERTICAL); container.setPadding(dp(12), dp(12), dp(12), dp(12)); LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT ); lp.setMargins(0, 0, 0, dp(12)); container.setLayoutParams(lp); GradientDrawable bg = new GradientDrawable(); bg.setCornerRadius(dp(10)); bg.setColor(0xFFF7F7F7); // light card color container.setBackground(bg); // Title TextView title = new TextView(this); title.setText(label); title.setTextSize(16f); title.setTypeface(Typeface.DEFAULT_BOLD); title.setTextColor(0xFF111111); // Button Button b = new Button(this); b.setText("Run"); b.setAllCaps(false); LinearLayout.LayoutParams btnParams = new LinearLayout.LayoutParams( ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT ); btnParams.gravity = Gravity.END; btnParams.topMargin = dp(8); b.setLayoutParams(btnParams); // Wire click: click on either card or the button triggers action container.setOnClickListener(onClick); b.setOnClickListener(onClick); container.addView(title); container.addView(b); return container; } // Helper: display bundle contents in an AlertDialog private void showBundle(String title, Bundle b) { StringBuilder sb = new StringBuilder(); if (b == null) { sb.append("null"); } else { for (String key : b.keySet()) { Object val = b.get(key); sb.append(key).append(": ").append(String.valueOf(val)).append("\n\n"); } } showResult(title, sb.toString()); } private void showResult(String title, String text) { // Show an AlertDialog with rounded message text TextView message = new TextView(this); message.setText(text); message.setTextSize(14f); message.setPadding(dp(12), dp(12), dp(12), dp(12)); message.setTextColor(0xFF222222); new AlertDialog.Builder(this) .setTitle(title) .setView(message) .setPositiveButton("OK", null) .show(); } private int dp(int d) { float scale = getResources().getDisplayMetrics().density; return Math.round(d * scale); } }
package ir.cafebazaar.poolakey
/**
* An inline extension that behaves like [takeIf], but also executes [andIfNot]
* when the predicate condition fails.
*
* Example:
* ```
* val number = 5.takeIfOrElse(
* thisIsTrue = { it > 10 },
* andIfNot = { println("Number is too small!") }
* )
* // prints: Number is too small!
* // returns: null
* ```
*
* @param thisIsTrue Predicate to test on the receiver [T].
* @param andIfNot Lambda to execute if [thisIsTrue] evaluates to false.
* @return The receiver [T] if the predicate is true, otherwise `null`.
*/
internal inline fun <T> T.takeIfOrElse(
thisIsTrue: (T) -> Boolean,
andIfNot: () -> Unit
): T? = if (thisIsTrue(this)) this else {
andIfNot()
null
}
/**
* Simple demo console app to show how [takeIfOrElse] works.
* It prints results in a user-friendly, colored format.
*/
fun main() {
// Terminal color codes for style
val reset = "\u001B[0m"
val blue = "\u001B[36m"
val green = "\u001B[32m"
val yellow = "\u001B[33m"
val red = "\u001B[31m"
val bold = "\u001B[1m"
// Header UI
println("$blue$bold==============================")
println(" 🧠 Poolakey takeIfOrElse Demo")
println("==============================$reset\n")
val input = "Hello"
println("${yellow}Input Value:$reset \"$input\"")
println("${yellow}Condition:$reset length > 10\n")
val result = input.takeIfOrElse(
thisIsTrue = { it.length > 10 },
andIfNot = {
println("${red}❌ Condition failed: String too short!$reset\n")
}
)
if (result != null) {
println("${green}✅ Success!$reset Value passed the test.")
println("${yellow}Result:$reset \"$result\"")
} else {
println("${blue}ℹ️ Returned:$reset null (predicate not satisfied)")
}
println("\n$blue$bold==============================")
println(" End of Demo")
println("==============================$reset")
}
==============================
🧠 Poolakey takeIfOrElse Demo
==============================
Input Value: "Hello"
Condition: length > 10
❌ Condition failed: String too short!
ℹ️ Returned: null (predicate not satisfied)
==============================
End of Demo
==============================
package ir.cafebazaar.poolakey
/**
* Represents the type of a purchase in the billing system.
*
* There are two main types:
* - [IN_APP] → One-time purchases
* - [SUBSCRIPTION] → Recurring payments
*/
internal enum class PurchaseType(val type: String) {
IN_APP("inapp"),
SUBSCRIPTION("subs");
companion object {
/**
* Returns the [PurchaseType] matching the given [type] string, or `null` if invalid.
*
* Example:
* ```
* val result = PurchaseType.fromType("subs") // SUBSCRIPTION
* ```
*/
fun fromType(type: String?): PurchaseType? {
if (type.isNullOrBlank()) return null
return values().firstOrNull { it.type.equals(type.trim(), ignoreCase = true) }
}
/**
* Checks if the given [type] string is a valid purchase type.
*
* Example:
* ```
* val isValid = PurchaseType.isValidType("inapp") // true
* ```
*/
fun isValidType(type: String?): Boolean = fromType(type) != null
/**
* Returns all available purchase type strings.
*
* Example:
* ```
* val all = PurchaseType.listAllTypes() // ["inapp", "subs"]
* ```
*/
fun listAllTypes(): List<String> = values().map { it.type }
}
/**
* Returns a friendly display name for the type (for UI or logs).
*
* Example:
* ```
* PurchaseType.IN_APP.displayName() // "In-App Purchase"
* ```
*/
fun displayName(): String = when (this) {
IN_APP -> "In-App Purchase"
SUBSCRIPTION -> "Subscription"
}
/**
* Returns true if this is a subscription type.
*/
fun isSubscription(): Boolean = this == SUBSCRIPTION
/**
* Returns true if this is an in-app (one-time) purchase type.
*/
fun isInApp(): Boolean = this == IN_APP
}
// ---------------------------------------------------------------------
// Interactive, polished console UI demo (single-file)
// ---------------------------------------------------------------------
fun main() {
// ANSI colors for nicer terminal UI (works in most terminals)
val RESET = "\u001B[0m"
val BOLD = "\u001B[1m"
val CYAN = "\u001B[36m"
val GREEN = "\u001B[32m"
val YELLOW = "\u001B[33m"
val RED = "\u001B[31m"
val MAGENTA = "\u001B[35m"
fun header() {
println("${CYAN + BOLD}==============================================")
println(" 🛒 Poolakey PurchaseType Utility Demo")
println("==============================================$RESET")
}
fun footer() {
println()
println("${CYAN}Thank you for trying the demo — exit with 'q' or Ctrl+C.$RESET")
}
header()
while (true) {
println()
println("${YELLOW}Available actions:${RESET}")
println(" 1) List all purchase types")
println(" 2) Convert string -> PurchaseType")
println(" 3) Show display names")
println(" 4) Type checks (isInApp / isSubscription)")
println(" q) Quit")
print("\nSelect an action (1-4 or q): ")
val choice = readLine()?.trim()?.lowercase()
when (choice) {
"1" -> {
val types = PurchaseType.listAllTypes()
println()
println("${MAGENTA}🔹 All purchase types:${RESET} ${types.joinToString(", ")}")
}
"2" -> {
print("Enter a type string (e.g. \"inapp\" or \"subs\"): ")
val input = readLine()?.trim()
val p = PurchaseType.fromType(input)
println()
if (p != null) {
println("${GREEN}✅ Parsed successfully:${RESET} ${p.name} (${p.type}) — ${p.displayName()}")
} else {
println("${RED}❌ Invalid purchase type:${RESET} \"${input ?: ""}\"")
println(" Valid values: ${PurchaseType.listAllTypes().joinToString(", ")}")
}
}
"3" -> {
println()
println("${MAGENTA}🎨 Display names:${RESET}")
PurchaseType.values().forEach { t ->
println(" • ${t.type} → ${t.displayName()}")
}
}
"4" -> {
println()
println("Check a type:")
print("Enter one of (${PurchaseType.listAllTypes().joinToString(", ")}): ")
val input = readLine()?.trim()
val p = PurchaseType.fromType(input)
if (p == null) {
println("${RED}❌ Unknown type: ${input ?: "\"\""}${RESET}")
} else {
val checks = buildString {
append("${GREEN}✔ ${p.name}${RESET} checks:\n")
append(" - isInApp(): ${if (p.isInApp()) "${BOLD}true" else "false"}\n")
append(" - isSubscription(): ${if (p.isSubscription()) "${BOLD}true" else "false"}")
}
println(checks)
}
}
"q", "quit", "exit" -> {
println()
println("${CYAN}Exiting demo...$RESET")
footer()
return
}
else -> {
println()
println("${RED}⚠️ Invalid selection. Please choose 1-4 or q.$RESET")
}
}
}
}
package ir.cafebazaar.poolakey
import android.content.Intent
import ir.cafebazaar.poolakey.callback.PurchaseCallback
import ir.cafebazaar.poolakey.config.SecurityCheck
import ir.cafebazaar.poolakey.constant.BazaarIntent
import ir.cafebazaar.poolakey.exception.PurchaseHijackedException
import ir.cafebazaar.poolakey.mapper.RawDataToPurchaseInfo
import ir.cafebazaar.poolakey.security.PurchaseVerifier
import java.security.InvalidKeyException
import java.security.NoSuchAlgorithmException
import java.security.SignatureException
import java.security.spec.InvalidKeySpecException
internal class PurchaseResultParser(
private val rawDataToPurchaseInfo: RawDataToPurchaseInfo,
private val purchaseVerifier: PurchaseVerifier
) {
// ANSI color codes for terminal logs
private val RESET = "\u001B[0m"
private val GREEN = "\u001B[32m"
private val RED = "\u001B[31m"
private val YELLOW = "\u001B[33m"
private val CYAN = "\u001B[36m"
private val BOLD = "\u001B[1m"
fun handleReceivedResult(
securityCheck: SecurityCheck,
data: Intent?,
purchaseCallback: PurchaseCallback.() -> Unit
) {
val code = data?.extras?.get(BazaarIntent.RESPONSE_CODE)
if (code == BazaarIntent.RESPONSE_RESULT_OK) {
parseResult(securityCheck, data, purchaseCallback)
} else {
logError("Response code invalid: $code")
PurchaseCallback().apply(purchaseCallback)
.purchaseFailed
.invoke(IllegalStateException("Response code is not valid"))
}
}
private fun parseResult(
securityCheck: SecurityCheck,
data: Intent?,
purchaseCallback: PurchaseCallback.() -> Unit
) {
val purchaseData = data?.getStringExtra(BazaarIntent.RESPONSE_PURCHASE_DATA)
val dataSignature = data?.getStringExtra(BazaarIntent.RESPONSE_SIGNATURE_DATA)
if (purchaseData != null && dataSignature != null) {
validatePurchase(
securityCheck = securityCheck,
purchaseData = purchaseData,
dataSignature = dataSignature,
purchaseIsValid = {
val purchaseInfo = rawDataToPurchaseInfo.mapToPurchaseInfo(
purchaseData,
dataSignature
)
logSuccess("Purchase validated successfully: $purchaseInfo")
PurchaseCallback().apply(purchaseCallback)
.purchaseSucceed
.invoke(purchaseInfo)
},
purchaseIsNotValid = { throwable ->
logError("Purchase validation failed: ${throwable.message}")
PurchaseCallback().apply(purchaseCallback)
.purchaseFailed
.invoke(throwable)
}
)
} else {
logError("Received invalid purchase data or signature")
PurchaseCallback().apply(purchaseCallback)
.purchaseFailed
.invoke(IllegalStateException("Received data is not valid"))
}
}
private inline fun validatePurchase(
securityCheck: SecurityCheck,
purchaseData: String,
dataSignature: String,
purchaseIsValid: () -> Unit,
purchaseIsNotValid: (Throwable) -> Unit
) {
if (securityCheck is SecurityCheck.Enable) {
try {
val isPurchaseValid = purchaseVerifier.verifyPurchase(
securityCheck.rsaPublicKey,
purchaseData,
dataSignature
)
if (isPurchaseValid) {
purchaseIsValid.invoke()
} else {
purchaseIsNotValid.invoke(PurchaseHijackedException())
}
} catch (e: Exception) {
purchaseIsNotValid.invoke(e)
}
} else {
purchaseIsValid.invoke()
}
}
// -------------------- Additional Helper Functions --------------------
fun parsePurchaseSafe(data: Intent?): Result<Any> = try {
val purchaseData = data?.getStringExtra(BazaarIntent.RESPONSE_PURCHASE_DATA)
val signature = data?.getStringExtra(BazaarIntent.RESPONSE_SIGNATURE_DATA)
if (purchaseData != null && signature != null) {
val info = rawDataToPurchaseInfo.mapToPurchaseInfo(purchaseData, signature)
logSuccess("Parsed purchase info: $info")
Result.success(info)
} else {
logError("Invalid purchase data")
Result.failure(IllegalStateException("Invalid purchase data"))
}
} catch (e: Exception) {
logError("Exception parsing purchase: ${e.message}")
Result.failure(e)
}
fun logPurchaseInfo(data: Intent?) {
parsePurchaseSafe(data).onSuccess { info ->
println("$GREEN✅ Purchase info: $info$RESET")
}.onFailure { throwable ->
println("$RED❌ Failed to parse purchase info: ${throwable.message}$RESET")
}
}
fun handleWithRetry(
securityCheck: SecurityCheck,
data: Intent?,
purchaseCallback: PurchaseCallback.() -> Unit,
maxRetries: Int = 3
) {
var attempts = 0
fun tryValidate() {
attempts++
handleReceivedResult(securityCheck, data) {
this.purchaseFailed = { throwable ->
if (attempts < maxRetries) {
println("$YELLOW⚠️ Retry attempt $attempts due to: ${throwable.message}$RESET")
tryValidate()
} else {
purchaseCallback()
}
}
this.purchaseSucceed = purchaseCallback().purchaseSucceed
}
}
tryValidate()
}
fun handleMultiplePurchases(
securityCheck: SecurityCheck,
dataList: List<Intent?>,
purchaseCallback: PurchaseCallback.() -> Unit
) {
dataList.forEach { intent ->
handleReceivedResult(securityCheck, intent, purchaseCallback)
}
}
fun isPurchaseRevoked(data: Intent?): Boolean {
val status = data?.extras?.getInt(BazaarIntent.RESPONSE_PURCHASE_STATE, -1)
val revoked = status == BazaarIntent.PURCHASE_STATE_CANCELED
if (revoked) logError("Purchase has been revoked or canceled")
return revoked
}
// -------------------- Logging Helpers --------------------
private fun logError(message: String) {
println("$RED$BOLD❌ ERROR: $message$RESET")
}
private fun logSuccess(message: String) {
println("$GREEN$BOLD✅ SUCCESS: $message$RESET")
}
private fun logInfo(message: String) {
println("$CYANℹ️ INFO: $message$RESET")
}
}
package ir.cafebazaar.poolakey
import android.content.Intent
import android.util.Log
import androidx.activity.result.ActivityResult
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.ActivityResultRegistry
import androidx.activity.result.IntentSenderRequest
import androidx.activity.result.contract.ActivityResultContracts
internal class PaymentLauncher private constructor(
val activityLauncher: ActivityResultLauncher<Intent>,
val intentSenderLauncher: ActivityResultLauncher<IntentSenderRequest>
) {
companion object {
private const val TAG = "PaymentLauncher"
// ANSI color codes for terminal (logs)
private const val RESET = "\u001B[0m"
private const val GREEN = "\u001B[32m"
private const val RED = "\u001B[31m"
private const val YELLOW = "\u001B[33m"
private const val CYAN = "\u001B[36m"
private const val BOLD = "\u001B[1m"
}
class Builder(
private val registry: ActivityResultRegistry,
private val onActivityResult: (ActivityResult) -> Unit
) {
fun build(): PaymentLauncher {
val activityLauncher = registry.register(
BillingConnection.PAYMENT_SERVICE_KEY,
ActivityResultContracts.StartActivityForResult(),
onActivityResult::invoke
)
val intentSenderLauncher = registry.register(
BillingConnection.PAYMENT_SERVICE_KEY,
ActivityResultContracts.StartIntentSenderForResult(),
onActivityResult::invoke
)
return PaymentLauncher(activityLauncher, intentSenderLauncher)
}
}
fun unregister() {
activityLauncher.unregister()
intentSenderLauncher.unregister()
logInfo("Launchers unregistered")
}
// -------------------- Additional Helper Functions --------------------
/** Launch a payment via Intent */
fun launchPayment(intent: Intent) {
try {
activityLauncher.launch(intent)
logSuccess("Payment launched via Intent")
} catch (e: Exception) {
logError("Failed to launch payment Intent: ${e.message}")
}
}
/** Launch a payment via IntentSenderRequest */
fun launchPayment(intentSenderRequest: IntentSenderRequest) {
try {
intentSenderLauncher.launch(intentSenderRequest)
logSuccess("Payment launched via IntentSenderRequest")
} catch (e: Exception) {
logError("Failed to launch payment IntentSenderRequest: ${e.message}")
}
}
/** Check if launchers are currently registered */
fun areLaunchersRegistered(): Boolean {
return try {
activityLauncher.isEnabled && intentSenderLauncher.isEnabled
} catch (e: Exception) {
false
}
}
/** Safely relaunch payment if launchers were unregistered */
fun relaunchPayment(
registry: ActivityResultRegistry,
onActivityResult: (ActivityResult) -> Unit,
intent: Intent? = null,
intentSenderRequest: IntentSenderRequest? = null
) {
logInfo("Relaunching payment...")
val builder = Builder(registry, onActivityResult)
val newLauncher = builder.build()
intent?.let { newLauncher.launchPayment(it) }
intentSenderRequest?.let { newLauncher.launchPayment(it) }
}
/** Launch payment and log results for debugging */
fun launchPaymentWithLogging(intent: Intent) {
logInfo("Attempting to launch payment...")
launchPayment(intent)
logInfo("Payment launch request complete")
}
// -------------------- Logging Helpers --------------------
private fun logError(message: String) {
Log.e(TAG, "$RED$BOLD❌ ERROR: $message$RESET")
}
private fun logSuccess(message: String) {
Log.d(TAG, "$GREEN$BOLD✅ SUCCESS: $message$RESET")
}
private fun logInfo(message: String) {
Log.i(TAG, "$CYANℹ️ INFO: $message$RESET")
}
}
package ir.cafebazaar.poolakey
import android.content.Context
import androidx.activity.result.ActivityResultRegistry
import ir.cafebazaar.poolakey.billing.query.QueryFunction
import ir.cafebazaar.poolakey.billing.skudetail.GetSkuDetailFunction
import ir.cafebazaar.poolakey.billing.trialsubscription.CheckTrialSubscriptionFunction
import ir.cafebazaar.poolakey.callback.*
import ir.cafebazaar.poolakey.config.PaymentConfiguration
import ir.cafebazaar.poolakey.mapper.RawDataToPurchaseInfo
import ir.cafebazaar.poolakey.request.PurchaseRequest
import ir.cafebazaar.poolakey.security.PurchaseVerifier
import ir.cafebazaar.poolakey.thread.BackgroundThread
import ir.cafebazaar.poolakey.thread.MainThread
import ir.cafebazaar.poolakey.thread.PoolakeyThread
class Payment(
context: Context,
config: PaymentConfiguration
) {
private val backgroundThread: PoolakeyThread<Runnable> = BackgroundThread()
private val mainThread: PoolakeyThread<() -> Unit> = MainThread()
private val purchaseVerifier = PurchaseVerifier()
private val rawDataToPurchaseInfo = RawDataToPurchaseInfo()
private val queryFunction = QueryFunction(
rawDataToPurchaseInfo,
purchaseVerifier,
config,
mainThread,
)
private val getSkuFunction = GetSkuDetailFunction(
context,
mainThread
)
private val checkTrialSubscriptionFunction = CheckTrialSubscriptionFunction(
context,
mainThread
)
private val purchaseResultParser = PurchaseResultParser(rawDataToPurchaseInfo, purchaseVerifier)
private val connection = BillingConnection(
context = context,
paymentConfiguration = config,
queryFunction = queryFunction,
backgroundThread = backgroundThread,
skuDetailFunction = getSkuFunction,
purchaseResultParser = purchaseResultParser,
checkTrialSubscriptionFunction = checkTrialSubscriptionFunction,
mainThread = mainThread
)
fun connect(callback: ConnectionCallback.() -> Unit): Connection {
return connection.startConnection(callback)
}
fun purchaseProduct(
registry: ActivityResultRegistry,
request: PurchaseRequest,
callback: PurchaseCallback.() -> Unit
) {
connection.purchase(registry, request, PurchaseType.IN_APP, callback)
}
fun consumeProduct(purchaseToken: String, callback: ConsumeCallback.() -> Unit) {
connection.consume(purchaseToken, callback)
}
fun subscribeProduct(
registry: ActivityResultRegistry,
request: PurchaseRequest,
callback: PurchaseCallback.() -> Unit
) {
connection.purchase(registry, request, PurchaseType.SUBSCRIPTION, callback)
}
fun getPurchasedProducts(callback: PurchaseQueryCallback.() -> Unit) {
connection.queryPurchasedProducts(PurchaseType.IN_APP, callback)
}
fun getSubscribedProducts(callback: PurchaseQueryCallback.() -> Unit) {
connection.queryPurchasedProducts(PurchaseType.SUBSCRIPTION, callback)
}
fun getInAppSkuDetails(
skuIds: List<String>,
callback: GetSkuDetailsCallback.() -> Unit
) {
connection.getSkuDetail(PurchaseType.IN_APP, skuIds, callback)
}
fun getSubscriptionSkuDetails(
skuIds: List<String>,
callback: GetSkuDetailsCallback.() -> Unit
) {
connection.getSkuDetail(PurchaseType.SUBSCRIPTION, skuIds, callback)
}
fun checkTrialSubscription(
callback: CheckTrialSubscriptionCallback.() -> Unit
) {
connection.checkTrialSubscription(callback)
}
// -------------------- Additional Helper Functions --------------------
fun refreshConnection(callback: ConnectionCallback.() -> Unit) {
println("🔄 Refreshing connection to billing service...")
connection.disconnect()
connection.startConnection(callback)
}
fun isProductPurchased(sku: String, callback: (Boolean) -> Unit) {
getPurchasedProducts {
onQueryCompleted = { purchasedList ->
val purchased = purchasedList.any { it.sku == sku }
println("🔍 SKU '$sku' purchased? $purchased")
callback(purchased)
}
}
}
fun isSubscriptionActive(sku: String, callback: (Boolean) -> Unit) {
getSubscribedProducts {
onQueryCompleted = { subscribedList ->
val active = subscribedList.any { it.sku == sku && it.isAutoRenewing }
println("🔍 Subscription '$sku' active? $active")
callback(active)
}
}
}
fun restorePurchases(
onRestoreCompleted: (purchasedProducts: List<String>, subscriptions: List<String>) -> Unit
) {
val restoredProducts = mutableListOf<String>()
val restoredSubscriptions = mutableListOf<String>()
getPurchasedProducts {
onQueryCompleted = { purchasedList ->
restoredProducts.addAll(purchasedList.map { it.sku })
getSubscribedProducts {
onQueryCompleted = { subscribedList ->
restoredSubscriptions.addAll(subscribedList.map { it.sku })
println("✅ Restored purchases: $restoredProducts")
println("🎉 Restored subscriptions: $restoredSubscriptions")
onRestoreCompleted(restoredProducts, restoredSubscriptions)
}
}
}
}
}
fun logAllPurchasesAndSubscriptions() {
getPurchasedProducts {
onQueryCompleted = { purchasedList ->
println("📦 Purchased products:")
purchasedList.forEach { println(" - SKU: ${it.sku}, Token: ${it.purchaseToken}") }
getSubscribedProducts {
onQueryCompleted = { subscribedList ->
println("✨ Active subscriptions:")
subscribedList.forEach { println(" - SKU: ${it.sku}, AutoRenew: ${it.isAutoRenewing}") }
}
}
}
}
}
fun prefetchSkuDetails(
inAppSkuIds: List<String> = emptyList(),
subscriptionSkuIds: List<String> = emptyList(),
callback: () -> Unit = {}
) {
var pendingCalls = 0
fun checkDone() {
pendingCalls--
if (pendingCalls <= 0) callback()
}
if (inAppSkuIds.isNotEmpty()) {
pendingCalls++
getInAppSkuDetails(inAppSkuIds) { onSkuDetailsReceived = { checkDone() } }
}
if (subscriptionSkuIds.isNotEmpty()) {
pendingCalls++
getSubscriptionSkuDetails(subscriptionSkuIds) { onSkuDetailsReceived = { checkDone() } }
}
if (pendingCalls == 0) callback()
}
}
package ir.cafebazaar.poolakey
import android.app.AlertDialog
import android.content.Context
import android.content.pm.PackageInfo
import android.content.pm.PackageManager
import android.os.Build
import android.util.Log
import java.text.DateFormat
import java.util.Date
import java.util.Locale
/**
* Package / version helper utilities for Poolakey.
*
* - Safe retrieval of PackageInfo
* - SDK-aware version code reading
* - Convenience checks and nicely formatted outputs
* - A small native dialog helper to display package info in a visually friendly way
*/
/**
* Safely retrieves [PackageInfo] for the given package name.
*/
internal fun getPackageInfo(context: Context, packageName: String): PackageInfo? = try {
val packageManager = context.packageManager
packageManager.getPackageInfo(packageName, 0)
} catch (ignored: Exception) {
null
}
/**
* Returns the SDK-aware version code from the [PackageInfo].
*/
@Suppress("DEPRECATION")
internal fun sdkAwareVersionCode(packageInfo: PackageInfo): Long {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
packageInfo.longVersionCode
} else {
packageInfo.versionCode.toLong()
}
}
// -------------------- Additional Utility Functions --------------------
/**
* Retrieves the version name of the given package, or null if unavailable.
*/
internal fun getVersionName(context: Context, packageName: String): String? {
return getPackageInfo(context, packageName)?.versionName
}
/**
* Checks whether a given package is installed on the device.
*/
internal fun isPackageInstalled(context: Context, packageName: String): Boolean {
return try {
context.packageManager.getPackageInfo(packageName, 0)
true
} catch (e: PackageManager.NameNotFoundException) {
false
} catch (e: Exception) {
false
}
}
/**
* Returns true if the given package's version is greater than or equal to [minVersionCode].
*/
internal fun isVersionAtLeast(context: Context, packageName: String, minVersionCode: Long): Boolean {
val info = getPackageInfo(context, packageName)
val currentCode = info?.let { sdkAwareVersionCode(it) } ?: return false
return currentCode >= minVersionCode
}
/**
* Logs detailed package info for debugging purposes (formatted).
*/
internal fun logPackageInfo(context: Context, packageName: String) {
val info = getPackageInfo(context, packageName)
if (info == null) {
Log.w("Poolakey", "⚠️ Package $packageName not found or inaccessible.")
return
}
val versionName = info.versionName ?: "N/A"
val versionCode = sdkAwareVersionCode(info)
val first = formatTime(info.firstInstallTime)
val last = formatTime(info.lastUpdateTime)
val message = """
📦 Package Info:
• Package Name: $packageName
• Version Name: $versionName
• Version Code: $versionCode
• First Install: $first
• Last Update : $last
""".trimIndent()
Log.d("Poolakey", message)
}
/**
* Returns the first install time of the app in milliseconds.
*/
internal fun getFirstInstallTime(context: Context, packageName: String): Long? {
return getPackageInfo(context, packageName)?.firstInstallTime
}
/**
* Returns the last update time of the app in milliseconds.
*/
internal fun getLastUpdateTime(context: Context, packageName: String): Long? {
return getPackageInfo(context, packageName)?.lastUpdateTime
}
/**
* Compares two installed app versions by version code.
* @return positive if [packageName1] > [packageName2], negative if less, 0 if equal or unknown.
*/
internal fun compareAppVersions(context: Context, packageName1: String, packageName2: String): Int {
val info1 = getPackageInfo(context, packageName1)
val info2 = getPackageInfo(context, packageName2)
if (info1 == null || info2 == null) return 0
val code1 = sdkAwareVersionCode(info1)
val code2 = sdkAwareVersionCode(info2)
return code1.compareTo(code2)
}
/**
* Returns human-readable package info summary (for UI or logs).
*/
internal fun getPackageSummary(context: Context, packageName: String): String {
val info = getPackageInfo(context, packageName)
return if (info == null) {
"Package \"$packageName\" is not installed."
} else {
val versionName = info.versionName ?: "Unknown"
val versionCode = sdkAwareVersionCode(info)
val installTime = formatTime(info.firstInstallTime)
val updateTime = formatTime(info.lastUpdateTime)
"""
📦 $packageName
• Version: $versionName ($versionCode)
• Installed: $installTime
• Updated: $updateTime
""".trimIndent()
}
}
/**
* Returns a short, visually-appealing summary (single-line) useful for compact UIs or logs.
*/
internal fun getPackageShortSummary(context: Context, packageName: String): String {
val info = getPackageInfo(context, packageName)
return if (info == null) {
"⚠️ $packageName — not installed"
} else {
val versionName = info.versionName ?: "?"
val versionCode = sdkAwareVersionCode(info)
"📦 $packageName — $versionName ($versionCode)"
}
}
/**
* Shows a simple native dialog with pretty package information.
* Uses android.app.AlertDialog so it works with plain Context (but passing an Activity Context
* is recommended so dialog is themed correctly).
*/
internal fun showPackageInfoDialog(context: Context, packageName: String) {
val info = getPackageInfo(context, packageName)
val title = "Package Info"
val message = if (info == null) {
"Package \"$packageName\" is not installed on this device."
} else {
val versionName = info.versionName ?: "Unknown"
val versionCode = sdkAwareVersionCode(info)
val installed = formatTime(info.firstInstallTime)
val updated = formatTime(info.lastUpdateTime)
"""
📦 $packageName
• Version: $versionName
• Version Code: $versionCode
• Installed: $installed
• Updated: $updated
""".trimIndent()
}
// Build and show the dialog (safe: will use application context fallback).
try {
AlertDialog.Builder(context)
.setTitle(title)
.setMessage(message)
.setPositiveButton("OK", null)
.show()
} catch (e: Exception) {
// If showing a dialog fails (e.g., non-activity context), fall back to logging.
Log.w("Poolakey", "Unable to show dialog for $packageName: ${e.message}")
Log.d("Poolakey", message)
}
}
// -------------------- Helpers --------------------
private fun formatTime(epochMillis: Long): String {
return try {
if (epochMillis <= 0L) "N/A"
else {
val df = DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.SHORT, Locale.getDefault())
df.format(Date(epochMillis))
}
} catch (e: Exception) {
epochMillis.toString()
}
}
package ir.cafebazaar.poolakey
import android.content.Context
import android.graphics.Color
import android.graphics.Typeface
import android.graphics.drawable.GradientDrawable
import android.widget.TextView
import androidx.annotation.ColorInt
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
/**
* Represents the current connection state between the app and Bazaar billing service.
* Extended with useful helpers for logging, UI binding and reactive adapters.
*/
sealed class ConnectionState {
/** Indicates that a connection has been successfully established. */
object Connected : ConnectionState()
/** Indicates that the connection attempt has failed. */
object FailedToConnect : ConnectionState()
/** Indicates that the service has been disconnected. */
object Disconnected : ConnectionState()
// -------------------- Basic queries --------------------
/** Returns `true` if the current state represents an active connection. */
fun isConnected(): Boolean = this is Connected
/** Returns `true` if the current state represents a disconnection. */
fun isDisconnected(): Boolean = this is Disconnected
/** Returns `true` if the current state represents a failed connection attempt. */
fun isFailed(): Boolean = this is FailedToConnect
/** Provides a human-readable description of the current connection state. */
fun getDescription(): String = when (this) {
Connected -> "✅ Connected to Bazaar billing service."
FailedToConnect -> "❌ Failed to connect to Bazaar billing service."
Disconnected -> "⚠️ Disconnected from Bazaar billing service."
}
/** Converts the connection state to a short status code string. */
fun toStatusCode(): String = when (this) {
Connected -> "CONNECTED"
FailedToConnect -> "FAILED"
Disconnected -> "DISCONNECTED"
}
/** Returns a simple emoji-based representation for logs or light UIs. */
fun getEmoji(): String = when (this) {
Connected -> "🟢"
FailedToConnect -> "🔴"
Disconnected -> "🟡"
}
// -------------------- Logging & diagnostics --------------------
/** Prints a formatted debug log describing the current state. */
fun logState(tag: String = "PoolakeyConnection") {
android.util.Log.d(tag, "Connection State: ${toStatusCode()} - ${getDescription()}")
}
/** Returns a compact JSON-like representation (for logs or diagnostics). */
fun toJsonString(): String {
return """{
"state": "${toStatusCode()}",
"description": "${getDescription()}",
"emoji": "${getEmoji()}",
"reconnectAllowed": ${shouldAttemptReconnect()},
"stability": ${getConnectionStabilityScore()}
}""".trimIndent()
}
// -------------------- UI helpers --------------------
/**
* Maps the current state to a color code for UI display.
* Uses Color.parseColor for clear, portable values.
*/
@ColorInt
fun getStateColor(): Int = when (this) {
Connected -> Color.parseColor("#4CAF50") // Green
FailedToConnect -> Color.parseColor("#F44336") // Red
Disconnected -> Color.parseColor("#FFC107") // Amber
}
/** Builds a short formatted UI-friendly string. Example: "🟢 Connected (Active)" */
fun getUiLabel(): String = when (this) {
Connected -> "${getEmoji()} Connected (Active)"
FailedToConnect -> "${getEmoji()} Connection Failed"
Disconnected -> "${getEmoji()} Disconnected"
}
/**
* Styles a TextView to represent the connection state as a pill/badge.
*
* Example usage (on main thread):
* connectionState.bindToStatusView(statusTextView, context)
*/
fun bindToStatusView(textView: TextView, context: Context) {
// text
textView.text = getUiLabel()
// color
val color = getStateColor()
textView.setTextColor(Color.WHITE)
// typeface and size
textView.setTypeface(Typeface.DEFAULT_BOLD)
textView.textSize = 14f
// padding (left, top, right, bottom)
val horizontal = dpToPx(context, 12)
val vertical = dpToPx(context, 6)
textView.setPadding(horizontal, vertical, horizontal, vertical)
// rounded background (pill) with appropriate color
val drawable = GradientDrawable()
drawable.shape = GradientDrawable.RECTANGLE
drawable.cornerRadius = dpToPx(context, 20).toFloat()
// use semi-strong color for background; keep text white for contrast
drawable.setColor(color)
textView.background = drawable
}
/**
* Applies a lighter badge-style background and colored text (inverse of bindToStatusView).
* Useful when you want colored text on transparent/light background.
*/
fun styleTextViewAsBadge(textView: TextView, context: Context) {
val color = getStateColor()
textView.text = getUiLabel()
textView.setTextColor(color)
textView.setTypeface(Typeface.DEFAULT_BOLD)
textView.textSize = 13f
val horizontal = dpToPx(context, 10)
val vertical = dpToPx(context, 4)
textView.setPadding(horizontal, vertical, horizontal, vertical)
val drawable = GradientDrawable()
drawable.shape = GradientDrawable.RECTANGLE
drawable.cornerRadius = dpToPx(context, 16).toFloat()
// light translucent background
drawable.setColor(adjustAlpha(color, 0.12f))
textView.background = drawable
}
// -------------------- Connection logic helpers --------------------
/**
* Suggests whether the system should attempt to reconnect.
* Returns true if the current state is FailedToConnect or Disconnected.
*/
fun shouldAttemptReconnect(): Boolean = when (this) {
Connected -> false
FailedToConnect, Disconnected -> true
}
/**
* Returns a next recommended state based on current state and external conditions.
*/
fun getNextSuggestedState(): ConnectionState = when (this) {
Connected -> Disconnected
FailedToConnect, Disconnected -> Connected
}
/**
* Returns a notification-friendly message for the current state.
*/
fun toNotificationMessage(): String = when (this) {
Connected -> "🟢 Connection established with Bazaar."
FailedToConnect -> "🔴 Unable to reach Bazaar servers. Please try again."
Disconnected -> "🟡 Connection lost. Attempting to reconnect..."
}
/**
* Returns an estimated connection stability score (0–100).
*/
fun getConnectionStabilityScore(): Int = when (this) {
Connected -> 100
FailedToConnect -> 25
Disconnected -> 50
}
// -------------------- Reactive adapters --------------------
/**
* Converts the state into a LiveData value — helpful for UI observation.
* Note: returned LiveData contains the single value of this state.
*/
fun asLiveData(): LiveData<ConnectionState> {
val liveData = MutableLiveData<ConnectionState>()
liveData.value = this
return liveData
}
/**
* Converts the state into a StateFlow value — useful for reactive UIs.
* Note: returned StateFlow contains this state as its initial value.
*/
fun asStateFlow(): StateFlow<ConnectionState> {
return MutableStateFlow(this)
}
// -------------------- Utility helpers --------------------
private fun dpToPx(context: Context, dp: Int): Int {
val scale = context.resources.displayMetrics.density
return (dp * scale + 0.5f).toInt()
}
private fun adjustAlpha(@ColorInt color: Int, factor: Float): Int {
val alpha = (Color.alpha(color) * factor).toInt()
val red = Color.red(color)
val green = Color.green(color)
val blue = Color.blue(color)
return Color.argb(alpha, red, green, blue)
}
}
package ir.cafebazaar.poolakey
import android.content.Context
import android.content.pm.PackageInfo
import android.content.pm.PackageManager
import android.os.Build
import android.util.Log
import android.graphics.Color
import android.graphics.Typeface
import android.graphics.drawable.GradientDrawable
import android.widget.TextView
import androidx.annotation.ColorInt
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
// -------------------- Package Utilities --------------------
internal fun getPackageInfo(context: Context, packageName: String): PackageInfo? = try {
val packageManager = context.packageManager
packageManager.getPackageInfo(packageName, 0)
} catch (ignored: Exception) {
null
}
@Suppress("DEPRECATION")
internal fun sdkAwareVersionCode(packageInfo: PackageInfo): Long =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) packageInfo.longVersionCode
else packageInfo.versionCode.toLong()
internal fun getVersionName(context: Context, packageName: String): String? =
getPackageInfo(context, packageName)?.versionName
internal fun isPackageInstalled(context: Context, packageName: String): Boolean =
try {
context.packageManager.getPackageInfo(packageName, 0)
true
} catch (e: Exception) {
false
}
internal fun isVersionAtLeast(context: Context, packageName: String, minVersionCode: Long): Boolean {
val info = getPackageInfo(context, packageName)
return info?.let { sdkAwareVersionCode(it) >= minVersionCode } ?: false
}
internal fun logPackageInfo(context: Context, packageName: String) {
val info = getPackageInfo(context, packageName) ?: run {
Log.w("Poolakey", "⚠️ Package $packageName not found or inaccessible.")
return
}
val versionName = info.versionName ?: "N/A"
val versionCode = sdkAwareVersionCode(info)
Log.d("Poolakey", """
📦 Package Info:
- Package Name: $packageName
- Version Name: $versionName
- Version Code: $versionCode
- First Install Time: ${info.firstInstallTime}
- Last Update Time: ${info.lastUpdateTime}
""".trimIndent())
}
internal fun getFirstInstallTime(context: Context, packageName: String): Long? =
getPackageInfo(context, packageName)?.firstInstallTime
internal fun getLastUpdateTime(context: Context, packageName: String): Long? =
getPackageInfo(context, packageName)?.lastUpdateTime
internal fun compareAppVersions(context: Context, packageName1: String, packageName2: String): Int {
val info1 = getPackageInfo(context, packageName1)
val info2 = getPackageInfo(context, packageName2)
if (info1 == null || info2 == null) return 0
return sdkAwareVersionCode(info1).compareTo(sdkAwareVersionCode(info2))
}
internal fun getPackageSummary(context: Context, packageName: String): String {
val info = getPackageInfo(context, packageName)
return if (info == null) "Package \"$packageName\" is not installed."
else """
📦 $packageName
• Version: ${info.versionName ?: "Unknown"} (${sdkAwareVersionCode(info)})
• Installed: ${info.firstInstallTime}
• Updated: ${info.lastUpdateTime}
""".trimIndent()
}
internal fun getAppAgeInDays(context: Context, packageName: String): Long? {
val installTime = getFirstInstallTime(context, packageName) ?: return null
return (System.currentTimeMillis() - installTime) / (1000 * 60 * 60 * 24)
}
internal fun needsUpdate(context: Context, packageName: String, remoteVersionCode: Long): Boolean {
val currentVersion = getPackageInfo(context, packageName)?.let { sdkAwareVersionCode(it) } ?: return false
return currentVersion < remoteVersionCode
}
internal fun getPackageUiSummary(context: Context, packageName: String): String {
val info = getPackageInfo(context, packageName) ?: return "Package \"$packageName\" not installed."
val age = getAppAgeInDays(context, packageName) ?: 0
return "${info.packageName} v${info.versionName ?: "?"} (${sdkAwareVersionCode(info)}) - Installed $age days ago"
}
// -------------------- Connection State --------------------
sealed class ConnectionState {
object Connected : ConnectionState()
object FailedToConnect : ConnectionState()
object Disconnected : ConnectionState()
fun isConnected(): Boolean = this is Connected
fun isDisconnected(): Boolean = this is Disconnected
fun isFailed(): Boolean = this is FailedToConnect
fun getDescription(): String = when (this) {
Connected -> "✅ Connected to Bazaar billing service."
FailedToConnect -> "❌ Failed to connect to Bazaar billing service."
Disconnected -> "⚠️ Disconnected from Bazaar billing service."
}
fun toStatusCode(): String = when (this) {
Connected -> "CONNECTED"
FailedToConnect -> "FAILED"
Disconnected -> "DISCONNECTED"
}
fun getEmoji(): String = when (this) {
Connected -> "🟢"
FailedToConnect -> "🔴"
Disconnected -> "🟡"
}
fun logState(tag: String = "PoolakeyConnection") {
Log.d(tag, "Connection State: ${toStatusCode()} - ${getDescription()}")
}
@ColorInt
fun getStateColor(): Int = when (this) {
Connected -> Color.parseColor("#4CAF50")
FailedToConnect -> Color.parseColor("#F44336")
Disconnected -> Color.parseColor("#FFC107")
}
fun getUiLabel(): String = "${getEmoji()} ${toStatusCode()}"
fun bindToStatusView(textView: TextView, context: Context) {
textView.text = getUiLabel()
val drawable = GradientDrawable().apply {
shape = GradientDrawable.RECTANGLE
cornerRadius = dpToPx(context, 16).toFloat()
setColor(getStateColor())
}
textView.apply {
setTextColor(Color.WHITE)
setTypeface(Typeface.DEFAULT_BOLD)
textSize = 14f
setPadding(dpToPx(context, 12), dpToPx(context, 6), dpToPx(context, 12), dpToPx(context, 6))
background = drawable
}
}
fun styleTextViewAsBadge(textView: TextView, context: Context) {
val color = getStateColor()
val drawable = GradientDrawable().apply {
shape = GradientDrawable.RECTANGLE
cornerRadius = dpToPx(context, 12).toFloat()
setColor(adjustAlpha(color, 0.15f))
}
textView.apply {
text = getUiLabel()
setTextColor(color)
setTypeface(Typeface.DEFAULT_BOLD)
textSize = 13f
setPadding(dpToPx(context, 10), dpToPx(context, 4), dpToPx(context, 10), dpToPx(context, 4))
background = drawable
}
}
fun shouldAttemptReconnect(): Boolean = !isConnected()
fun getNextSuggestedState(): ConnectionState = when (this) {
Connected -> Disconnected
else -> Connected
}
fun toNotificationMessage(): String = when (this) {
Connected -> "🟢 Connection established with Bazaar."
FailedToConnect -> "🔴 Unable to reach Bazaar servers. Please try again."
Disconnected -> "🟡 Connection lost. Attempting to reconnect..."
}
fun getConnectionStabilityScore(): Int = when (this) {
Connected -> 100
FailedToConnect -> 25
Disconnected -> 50
}
fun asLiveData(): LiveData<ConnectionState> = MutableLiveData<ConnectionState>().apply { value = this@ConnectionState }
fun asStateFlow(): StateFlow<ConnectionState> = MutableStateFlow(this)
private fun dpToPx(context: Context, dp: Int): Int {
val scale = context.resources.displayMetrics.density
return (dp * scale + 0.5f).toInt()
}
private fun adjustAlpha(@ColorInt color: Int, factor: Float): Int {
val alpha = (Color.alpha(color) * factor).toInt()
return Color.argb(alpha, Color.red(color), Color.green(color), Color.blue(color))
}
}
// -------------------- Connection Interface --------------------
interface Connection {
fun getState(): ConnectionState
fun disconnect()
}
// -------------------- Additional ConnectionState Helpers --------------------
fun ConnectionState.toggleTestState(): ConnectionState = when (this) {
ConnectionState.Connected -> ConnectionState.Disconnected
ConnectionState.Disconnected -> ConnectionState.Connected
ConnectionState.FailedToConnect -> ConnectionState.Connected
}
fun ConnectionState.getSeverityLevel(): Int = when (this) {
ConnectionState.Connected -> 0
ConnectionState.Disconnected -> 1
ConnectionState.FailedToConnect -> 2
}
fun ConnectionState.updateTextViews(vararg textViews: TextView, context: Context) {
textViews.forEach { bindToStatusView(it, context) }
}
fun ConnectionState.getUiLabelWithMessage(message: String?): String =
if (message.isNullOrBlank()) getUiLabel() else "${getUiLabel()} - $message"
fun ConnectionState.getColoredCircleDrawable(): GradientDrawable = GradientDrawable().apply {
shape = GradientDrawable.OVAL
setColor(getStateColor())
setSize(24, 24)
}
fun ConnectionState.asConnectedLiveData(): LiveData<Boolean> = MutableLiveData<Boolean>().apply { value = isConnected() }
fun ConnectionState.asStabilityStateFlow(): StateFlow<Int> = MutableStateFlow(getConnectionStabilityScore())
package ir.cafebazaar.poolakey
import android.app.Activity
import android.content.Context
import android.content.pm.PackageInfo
import android.content.pm.PackageManager
import android.os.Build
import android.os.Build.VERSION.SDK_INT
import android.util.Log
import android.graphics.Color
import android.graphics.Typeface
import android.graphics.drawable.GradientDrawable
import android.view.View
import android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
import android.widget.TextView
import androidx.annotation.ColorInt
import androidx.activity.result.ActivityResult
import androidx.activity.result.ActivityResultRegistry
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
// -------------------- Package Utilities --------------------
internal fun getPackageInfo(context: Context, packageName: String): PackageInfo? = try {
context.packageManager.getPackageInfo(packageName, 0)
} catch (_: Exception) {
null
}
@Suppress("DEPRECATION")
internal fun sdkAwareVersionCode(packageInfo: PackageInfo): Long =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) packageInfo.longVersionCode
else packageInfo.versionCode.toLong()
internal fun getVersionName(context: Context, packageName: String): String? =
getPackageInfo(context, packageName)?.versionName
internal fun isPackageInstalled(context: Context, packageName: String): Boolean =
try { context.packageManager.getPackageInfo(packageName, 0); true } catch (_: Exception) { false }
internal fun isVersionAtLeast(context: Context, packageName: String, minVersionCode: Long): Boolean {
val info = getPackageInfo(context, packageName)
return info?.let { sdkAwareVersionCode(it) >= minVersionCode } ?: false
}
internal fun logPackageInfo(context: Context, packageName: String) {
val info = getPackageInfo(context, packageName) ?: run {
Log.w("Poolakey", "⚠️ Package $packageName not found or inaccessible.")
return
}
Log.d("Poolakey", """
📦 Package Info:
- Package Name: $packageName
- Version Name: ${info.versionName ?: "N/A"}
- Version Code: ${sdkAwareVersionCode(info)}
- First Install Time: ${info.firstInstallTime}
- Last Update Time: ${info.lastUpdateTime}
""".trimIndent())
}
internal fun getFirstInstallTime(context: Context, packageName: String): Long? =
getPackageInfo(context, packageName)?.firstInstallTime
internal fun getLastUpdateTime(context: Context, packageName: String): Long? =
getPackageInfo(context, packageName)?.lastUpdateTime
internal fun compareAppVersions(context: Context, packageName1: String, packageName2: String): Int {
val info1 = getPackageInfo(context, packageName1)
val info2 = getPackageInfo(context, packageName2)
if (info1 == null || info2 == null) return 0
return sdkAwareVersionCode(info1).compareTo(sdkAwareVersionCode(info2))
}
internal fun getPackageSummary(context: Context, packageName: String): String {
val info = getPackageInfo(context, packageName) ?: return "Package \"$packageName\" is not installed."
return """
📦 $packageName
• Version: ${info.versionName ?: "Unknown"} (${sdkAwareVersionCode(info)})
• Installed: ${info.firstInstallTime}
• Updated: ${info.lastUpdateTime}
""".trimIndent()
}
internal fun getAppAgeInDays(context: Context, packageName: String): Long? {
val installTime = getFirstInstallTime(context, packageName) ?: return null
return (System.currentTimeMillis() - installTime) / (1000 * 60 * 60 * 24)
}
internal fun needsUpdate(context: Context, packageName: String, remoteVersionCode: Long): Boolean {
val currentVersion = getPackageInfo(context, packageName)?.let { sdkAwareVersionCode(it) } ?: return false
return currentVersion < remoteVersionCode
}
internal fun getPackageUiSummary(context: Context, packageName: String): String {
val info = getPackageInfo(context, packageName) ?: return "Package \"$packageName\" not installed."
val age = getAppAgeInDays(context, packageName) ?: 0
return "${info.packageName} v${info.versionName ?: "?"} (${sdkAwareVersionCode(info)}) - Installed $age days ago"
}
// -------------------- Connection State --------------------
sealed class ConnectionState {
object Connected : ConnectionState()
object FailedToConnect : ConnectionState()
object Disconnected : ConnectionState()
fun isConnected(): Boolean = this is Connected
fun isDisconnected(): Boolean = this is Disconnected
fun isFailed(): Boolean = this is FailedToConnect
fun getDescription(): String = when (this) {
Connected -> "✅ Connected to Bazaar billing service."
FailedToConnect -> "❌ Failed to connect to Bazaar billing service."
Disconnected -> "⚠️ Disconnected from Bazaar billing service."
}
fun toStatusCode(): String = when (this) {
Connected -> "CONNECTED"
FailedToConnect -> "FAILED"
Disconnected -> "DISCONNECTED"
}
fun getEmoji(): String = when (this) {
Connected -> "🟢"
FailedToConnect -> "🔴"
Disconnected -> "🟡"
}
fun logState(tag: String = "PoolakeyConnection") {
Log.d(tag, "Connection State: ${toStatusCode()} - ${getDescription()}")
}
@ColorInt
fun getStateColor(): Int = when (this) {
Connected -> Color.parseColor("#4CAF50")
FailedToConnect -> Color.parseColor("#F44336")
Disconnected -> Color.parseColor("#FFC107")
}
fun getUiLabel(): String = "${getEmoji()} ${toStatusCode()}"
fun bindToStatusView(textView: TextView, context: Context) {
textView.text = getUiLabel()
val drawable = GradientDrawable().apply {
shape = GradientDrawable.RECTANGLE
cornerRadius = dpToPx(context, 20).toFloat()
setColor(getStateColor())
}
textView.apply {
setTextColor(Color.WHITE)
setTypeface(Typeface.DEFAULT_BOLD)
textSize = 14f
setPadding(dpToPx(context, 16), dpToPx(context, 8), dpToPx(context, 16), dpToPx(context, 8))
background = drawable
elevation = 6f
}
}
fun styleTextViewAsBadge(textView: TextView, context: Context) {
val color = getStateColor()
val drawable = GradientDrawable().apply {
shape = GradientDrawable.RECTANGLE
cornerRadius = dpToPx(context, 16).toFloat()
setColor(adjustAlpha(color, 0.12f))
}
textView.apply {
text = getUiLabel()
setTextColor(color)
setTypeface(Typeface.DEFAULT_BOLD)
textSize = 13f
setPadding(dpToPx(context, 10), dpToPx(context, 4), dpToPx(context, 10), dpToPx(context, 4))
background = drawable
elevation = 4f
}
}
fun shouldAttemptReconnect(): Boolean = !isConnected()
fun getNextSuggestedState(): ConnectionState = when (this) {
Connected -> Disconnected
else -> Connected
}
fun toNotificationMessage(): String = when (this) {
Connected -> "🟢 Connection established with Bazaar."
FailedToConnect -> "🔴 Unable to reach Bazaar servers. Please try again."
Disconnected -> "🟡 Connection lost. Attempting to reconnect..."
}
fun getConnectionStabilityScore(): Int = when (this) {
Connected -> 100
FailedToConnect -> 25
Disconnected -> 50
}
fun asLiveData(): LiveData<ConnectionState> = MutableLiveData<ConnectionState>().apply { value = this@ConnectionState }
fun asStateFlow(): StateFlow<ConnectionState> = MutableStateFlow(this)
private fun dpToPx(context: Context, dp: Int): Int =
(dp * context.resources.displayMetrics.density + 0.5f).toInt()
private fun adjustAlpha(@ColorInt color: Int, factor: Float): Int {
val alpha = (Color.alpha(color) * factor).toInt()
return Color.argb(alpha, Color.red(color), Color.green(color), Color.blue(color))
}
}
// -------------------- Connection Interface --------------------
interface Connection {
fun getState(): ConnectionState
fun disconnect()
}
// -------------------- Additional Helpers --------------------
fun ConnectionState.toggleTestState(): ConnectionState = when (this) {
ConnectionState.Connected -> ConnectionState.Disconnected
ConnectionState.Disconnected -> ConnectionState.Connected
ConnectionState.FailedToConnect -> ConnectionState.Connected
}
fun ConnectionState.getSeverityLevel(): Int = when (this) {
ConnectionState.Connected -> 0
ConnectionState.Disconnected -> 1
ConnectionState.FailedToConnect -> 2
}
fun ConnectionState.updateTextViews(vararg textViews: TextView, context: Context) {
textViews.forEach { bindToStatusView(it, context) }
}
fun ConnectionState.getUiLabelWithMessage(message: String?): String {
val baseLabel = getUiLabel()
return if (message.isNullOrBlank()) baseLabel else "$baseLabel - $message"
}
fun ConnectionState.getColoredCircleDrawable(): GradientDrawable {
return GradientDrawable().apply {
shape = GradientDrawable.OVAL
setColor(getStateColor())
setSize(24, 24)
}
}
fun ConnectionState.asConnectedLiveData(): LiveData<Boolean> =
MutableLiveData<Boolean>().apply { value = isConnected() }
fun ConnectionState.asStabilityStateFlow(): StateFlow<Int> =
MutableStateFlow(getConnectionStabilityScore())
// =======================================================
// ✅ Poolakey Complete Utilities + Extensions (Fixed + Enhanced)
// =======================================================
package ir.cafebazaar.poolakey
import android.app.Activity
import android.content.Context
import android.content.pm.PackageInfo
import android.graphics.Color
import android.graphics.Typeface
import android.graphics.drawable.GradientDrawable
import android.os.Build
import android.os.Handler
import android.os.Looper
import android.util.Log
import android.widget.TextView
import androidx.annotation.ColorInt
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import java.lang.ref.WeakReference
import java.text.SimpleDateFormat
import java.util.*
// =======================================================
// 📦 Package Utilities
// =======================================================
internal fun getPackageInfo(context: Context, packageName: String): PackageInfo? = try {
context.packageManager.getPackageInfo(packageName, 0)
} catch (_: Exception) { null }
@Suppress("DEPRECATION")
internal fun sdkAwareVersionCode(info: PackageInfo): Long =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) info.longVersionCode else info.versionCode.toLong()
internal fun getVersionName(context: Context, pkg: String): String? =
getPackageInfo(context, pkg)?.versionName
internal fun getFirstInstallTime(context: Context, pkg: String): Long? =
getPackageInfo(context, pkg)?.firstInstallTime
internal fun getLastUpdateTime(context: Context, pkg: String): Long? =
getPackageInfo(context, pkg)?.lastUpdateTime
internal fun getAppAgeInDays(context: Context, pkg: String): Long? {
val installTime = getFirstInstallTime(context, pkg) ?: return null
return (System.currentTimeMillis() - installTime) / (1000L * 60 * 60 * 24)
}
internal fun getInstallDate(context: Context, pkg: String): String {
val install = getFirstInstallTime(context, pkg) ?: return "Unknown"
return SimpleDateFormat("yyyy-MM-dd HH:mm", Locale.getDefault()).format(Date(install))
}
internal fun getUpdateDate(context: Context, pkg: String): String {
val update = getLastUpdateTime(context, pkg) ?: return "Unknown"
return SimpleDateFormat("yyyy-MM-dd HH:mm", Locale.getDefault()).format(Date(update))
}
internal fun wasRecentlyUpdated(context: Context, pkg: String, days: Int = 7): Boolean {
val last = getLastUpdateTime(context, pkg) ?: return false
val since = (System.currentTimeMillis() - last) / (1000 * 60 * 60 * 24)
return since <= days
}
// =======================================================
// 🔌 Connection State Management
// =======================================================
sealed class ConnectionState {
object Connected : ConnectionState()
object FailedToConnect : ConnectionState()
object Disconnected : ConnectionState()
fun isConnected() = this is Connected
fun isDisconnected() = this is Disconnected
fun isFailed() = this is FailedToConnect
fun toStatusCode(): String = when (this) {
Connected -> "CONNECTED"
FailedToConnect -> "FAILED"
Disconnected -> "DISCONNECTED"
}
fun getEmoji(): String = when (this) {
Connected -> "🟢"
FailedToConnect -> "🔴"
Disconnected -> "🟡"
}
fun logState(tag: String = "PoolakeyConnection") {
Log.d(tag, "State: ${toStatusCode()} (${getEmoji()})")
}
@ColorInt
fun getStateColor(): Int = when (this) {
Connected -> Color.parseColor("#4CAF50")
FailedToConnect -> Color.parseColor("#F44336")
Disconnected -> Color.parseColor("#FFC107")
}
fun getUiLabel(): String = "${getEmoji()} ${toStatusCode()}"
/** improved, gradient-based badge with shadow */
fun bindToStatusView(view: TextView, ctx: Context) {
val grad = GradientDrawable(
GradientDrawable.Orientation.LEFT_RIGHT,
intArrayOf(
adjustBrightness(getStateColor(), 1.2f),
adjustBrightness(getStateColor(), 0.8f)
)
).apply {
cornerRadius = dp(ctx, 24f)
}
view.apply {
text = getUiLabel()
setTypeface(Typeface.DEFAULT_BOLD)
textSize = 15f
setTextColor(Color.WHITE)
background = grad
setPadding(dp(ctx, 20f).toInt(), dp(ctx, 10f).toInt(), dp(ctx, 20f).toInt(), dp(ctx, 10f).toInt())
elevation = 8f
}
}
private fun dp(ctx: Context, v: Float): Float = v * ctx.resources.displayMetrics.density
private fun adjustBrightness(@ColorInt color: Int, factor: Float): Int {
val r = (Color.red(color) * factor).coerceIn(0f, 255f)
val g = (Color.green(color) * factor).coerceIn(0f, 255f)
val b = (Color.blue(color) * factor).coerceIn(0f, 255f)
return Color.rgb(r.toInt(), g.toInt(), b.toInt())
}
}
// =======================================================
// 🔗 Connection Interface + Extensions
// =======================================================
interface Connection {
fun getState(): ConnectionState
fun disconnect()
}
class SafeConnection(private val ref: WeakReference<Connection>) : Connection {
override fun getState(): ConnectionState = ref.get()?.getState() ?: ConnectionState.Disconnected
override fun disconnect() { ref.get()?.disconnect() }
fun reconnectIfNeeded(onReconnect: () -> Unit) {
if (getState().isDisconnected() || getState().isFailed()) {
Log.i("Poolakey", "Reconnecting …")
onReconnect()
}
}
}
fun ConnectionState.asLiveData(): LiveData<ConnectionState> = MutableLiveData(this)
fun ConnectionState.asStateFlow(): StateFlow<ConnectionState> = MutableStateFlow(this)
fun combineConnectionStates(vararg s: ConnectionState): ConnectionState =
when {
s.any { it is ConnectionState.FailedToConnect } -> ConnectionState.FailedToConnect
s.any { it is ConnectionState.Disconnected } -> ConnectionState.Disconnected
else -> ConnectionState.Connected
}
// =======================================================
// 🧵 Poolakey Thread Interface + Implementations
// =======================================================
internal interface PoolakeyThread<T> {
fun execute(task: T)
fun dispose()
}
internal class CoroutinePoolakeyThread : PoolakeyThread<Runnable> {
private val job = SupervisorJob()
private val scope = CoroutineScope(Dispatchers.IO + job)
override fun execute(task: Runnable) { scope.launch { task.run() } }
override fun dispose() { job.cancel() }
}
internal class MainThreadPoolakeyThread : PoolakeyThread<() -> Unit> {
private val handler = Handler(Looper.getMainLooper())
override fun execute(task: () -> Unit) { handler.post(task) }
override fun dispose() { handler.removeCallbacksAndMessages(null) }
}
// =======================================================
// 🧠 Connection Monitoring Extensions
// =======================================================
suspend fun ConnectionState.monitorConnectionState(
onConnected: suspend () -> Unit,
onDisconnected: suspend () -> Unit,
onFailed: suspend () -> Unit
) {
when (this) {
is ConnectionState.Connected -> onConnected()
is ConnectionState.Disconnected -> onDisconnected()
is ConnectionState.FailedToConnect -> onFailed()
}
}
suspend fun retryUntilConnected(
provider: suspend () -> ConnectionState,
maxRetries: Int = 5,
delayMs: Long = 2000L
): ConnectionState {
repeat(maxRetries) { attempt ->
val state = provider()
if (state.isConnected()) return state
Log.w("PoolakeyRetry", "Attempt ${attempt + 1}/$maxRetries failed – retrying …")
delay(delayMs)
}
return ConnectionState.FailedToConnect
}
// =======================================================
// ✅ Poolakey Complete Utilities + Thread Implementations (Enhanced UI + Stability)
// =======================================================
package ir.cafebazaar.poolakey
import android.animation.ArgbEvaluator
import android.animation.ValueAnimator
import android.app.Activity
import android.content.Context
import android.content.pm.PackageInfo
import android.graphics.Color
import android.graphics.Typeface
import android.graphics.drawable.GradientDrawable
import android.os.Build
import android.os.Handler
import android.os.Looper
import android.os.Message
import android.util.Log
import android.widget.TextView
import androidx.annotation.ColorInt
import androidx.core.content.res.ResourcesCompat
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import java.lang.ref.WeakReference
import java.text.SimpleDateFormat
import java.util.*
import kotlin.math.pow
// =======================================================
// 📦 Package Utilities
// =======================================================
internal fun getPackageInfo(context: Context, packageName: String): PackageInfo? = try {
context.packageManager.getPackageInfo(packageName, 0)
} catch (_: Exception) { null }
@Suppress("DEPRECATION")
internal fun sdkAwareVersionCode(info: PackageInfo): Long =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) info.longVersionCode else info.versionCode.toLong()
internal fun getVersionName(context: Context, pkg: String): String? =
getPackageInfo(context, pkg)?.versionName
internal fun getFirstInstallTime(context: Context, pkg: String): Long? =
getPackageInfo(context, pkg)?.firstInstallTime
internal fun getLastUpdateTime(context: Context, pkg: String): Long? =
getPackageInfo(context, pkg)?.lastUpdateTime
internal fun getAppAgeInDays(context: Context, pkg: String): Long? {
val installTime = getFirstInstallTime(context, pkg) ?: return null
return (System.currentTimeMillis() - installTime) / (1000L * 60 * 60 * 24)
}
internal fun getInstallDate(context: Context, pkg: String): String {
val install = getFirstInstallTime(context, pkg) ?: return "Unknown"
return SimpleDateFormat("yyyy-MM-dd HH:mm", Locale.getDefault()).format(Date(install))
}
internal fun getUpdateDate(context: Context, pkg: String): String {
val update = getLastUpdateTime(context, pkg) ?: return "Unknown"
return SimpleDateFormat("yyyy-MM-dd HH:mm", Locale.getDefault()).format(Date(update))
}
internal fun wasRecentlyUpdated(context: Context, pkg: String, days: Int = 7): Boolean {
val last = getLastUpdateTime(context, pkg) ?: return false
val since = (System.currentTimeMillis() - last) / (1000 * 60 * 60 * 24)
return since <= days
}
// =======================================================
// 🔌 Connection State Management
// =======================================================
sealed class ConnectionState {
object Connected : ConnectionState()
object FailedToConnect : ConnectionState()
object Disconnected : ConnectionState()
fun isConnected() = this is Connected
fun isDisconnected() = this is Disconnected
fun isFailed() = this is FailedToConnect
fun toStatusCode(): String = when (this) {
Connected -> "CONNECTED"
FailedToConnect -> "FAILED"
Disconnected -> "DISCONNECTED"
}
fun getEmoji(): String = when (this) {
Connected -> "🟢"
FailedToConnect -> "🔴"
Disconnected -> "🟡"
}
fun logState(tag: String = "PoolakeyConnection") {
Log.d(tag, "State: ${toStatusCode()} (${getEmoji()})")
}
@ColorInt
fun getStateColor(): Int = when (this) {
Connected -> Color.parseColor("#4CAF50") // Green
FailedToConnect -> Color.parseColor("#E53935") // Red (Material shade)
Disconnected -> Color.parseColor("#FFC107") // Amber
}
fun getUiLabel(): String = "${getEmoji()} ${toStatusCode()}"
// =======================================================
// ✨ Improved UI Binding (Animated, Rounded, Adaptive)
// =======================================================
fun bindToStatusView(view: TextView, ctx: Context, animate: Boolean = true) {
val newColor = getStateColor()
val currentBg = (view.background as? GradientDrawable)?.colors?.firstOrNull() ?: Color.GRAY
if (animate) {
val anim = ValueAnimator.ofObject(ArgbEvaluator(), currentBg, newColor)
anim.duration = 450
anim.addUpdateListener { valueAnimator ->
val color = valueAnimator.animatedValue as Int
val grad = GradientDrawable(
GradientDrawable.Orientation.LEFT_RIGHT,
intArrayOf(
adjustBrightness(color, 1.15f),
adjustBrightness(color, 0.85f)
)
).apply {
cornerRadius = dp(ctx, 26f)
}
view.background = grad
}
anim.start()
} else {
val grad = GradientDrawable(
GradientDrawable.Orientation.LEFT_RIGHT,
intArrayOf(
adjustBrightness(newColor, 1.15f),
adjustBrightness(newColor, 0.85f)
)
).apply {
cornerRadius = dp(ctx, 26f)
}
view.background = grad
}
view.apply {
text = getUiLabel()
setTypeface(Typeface.DEFAULT_BOLD)
textSize = 15.5f
setTextColor(Color.WHITE)
setPadding(dp(ctx, 22f).toInt(), dp(ctx, 12f).toInt(), dp(ctx, 22f).toInt(), dp(ctx, 12f).toInt())
elevation = 10f
}
}
private fun dp(ctx: Context, v: Float): Float = v * ctx.resources.displayMetrics.density
private fun adjustBrightness(@ColorInt color: Int, factor: Float): Int {
val r = (Color.red(color) * factor).coerceIn(0f, 255f)
val g = (Color.green(color) * factor).coerceIn(0f, 255f)
val b = (Color.blue(color) * factor).coerceIn(0f, 255f)
return Color.rgb(r.toInt(), g.toInt(), b.toInt())
}
fun formatMessage(): String {
val ts = SimpleDateFormat("HH:mm:ss", Locale.getDefault()).format(Date())
return "[$ts] ${getUiLabel()}"
}
fun toIntCode(): Int = when (this) {
Connected -> 1
FailedToConnect -> -1
Disconnected -> 0
}
}
// =======================================================
// 🔗 Connection Interface + Extensions
// =======================================================
interface Connection {
fun getState(): ConnectionState
fun disconnect()
}
class SafeConnection(private val ref: WeakReference<Connection>) : Connection {
override fun getState(): ConnectionState = ref.get()?.getState() ?: ConnectionState.Disconnected
override fun disconnect() { ref.get()?.disconnect() }
fun reconnectIfNeeded(onReconnect: () -> Unit) {
if (getState().isDisconnected() || getState().isFailed()) {
Log.i("Poolakey", "Reconnecting …")
onReconnect()
}
}
fun monitorContinuously(intervalMs: Long = 5000L, onStateChange: (ConnectionState) -> Unit) {
val weakHandler = WeakReference(Handler(Looper.getMainLooper()))
val runnable = object : Runnable {
override fun run() {
val state = getState()
onStateChange(state)
weakHandler.get()?.postDelayed(this, intervalMs)
}
}
weakHandler.get()?.post(runnable)
}
}
fun ConnectionState.asLiveData(): LiveData<ConnectionState> = MutableLiveData(this)
fun ConnectionState.asStateFlow(): StateFlow<ConnectionState> = MutableStateFlow(this)
fun combineConnectionStates(vararg s: ConnectionState): ConnectionState =
when {
s.any { it is ConnectionState.FailedToConnect } -> ConnectionState.FailedToConnect
s.any { it is ConnectionState.Disconnected } -> ConnectionState.Disconnected
else -> ConnectionState.Connected
}
// =======================================================
// 🧵 Poolakey Thread Interface + Implementations
// =======================================================
internal interface PoolakeyThread<T> {
fun execute(task: T)
fun dispose()
}
internal class CoroutinePoolakeyThread : PoolakeyThread<Runnable> {
private val job = SupervisorJob()
private val scope = CoroutineScope(Dispatchers.IO + job)
override fun execute(task: Runnable) { scope.launch { task.run() } }
override fun dispose() { job.cancel() }
}
// =======================================================
// 🧵 MainThread Handler Implementation (Improved Safety)
// =======================================================
internal class MainThread : Handler(Looper.getMainLooper()), PoolakeyThread<() -> Unit> {
override fun handleMessage(message: Message) {
(message.obj as? Function0<*>)?.invoke()
?: Log.e("PoolakeyMainThread", "Invalid message received")
}
override fun execute(task: () -> Unit) {
val message = obtainMessage().apply { obj = task }
sendMessage(message)
}
override fun dispose() {
removeCallbacksAndMessages(null)
}
}
// =======================================================
// 🧠 Connection Monitoring + Retry Utilities
// =======================================================
suspend fun ConnectionState.monitorConnectionState(
onConnected: suspend () -> Unit,
onDisconnected: suspend () -> Unit,
onFailed: suspend () -> Unit
) {
when (this) {
is ConnectionState.Connected -> onConnected()
is ConnectionState.Disconnected -> onDisconnected()
is ConnectionState.FailedToConnect -> onFailed()
}
}
suspend fun retryUntilConnected(
provider: suspend () -> ConnectionState,
maxRetries: Int = 5,
delayMs: Long = 2000L
): ConnectionState {
repeat(maxRetries) { attempt ->
val state = provider()
if (state.isConnected()) return state
Log.w("PoolakeyRetry", "Attempt ${attempt + 1}/$maxRetries failed – retrying …")
delay(delayMs)
}
return ConnectionState.FailedToConnect
}
// =======================================================
// 🧮 Advanced Diagnostics + Backoff
// =======================================================
suspend fun measureConnectionDuration(action: suspend () -> ConnectionState): Pair<ConnectionState, Long> {
val start = System.currentTimeMillis()
val result = action()
val duration = System.currentTimeMillis() - start
Log.i("PoolakeyMetrics", "Connection took ${duration}ms → ${result.toStatusCode()}")
return result to duration
}
fun ConnectionState.toMap(): Map<String, Any> = mapOf(
"status" to toStatusCode(),
"emoji" to getEmoji(),
"code" to toIntCode(),
"timestamp" to System.currentTimeMillis()
)
suspend fun exponentialRetry(
action: suspend () -> ConnectionState,
maxAttempts: Int = 5,
baseDelay: Long = 1000L
): ConnectionState {
repeat(maxAttempts) { attempt ->
val state = action()
if (state.isConnected()) return state
val delayTime = baseDelay * (2.0.pow(attempt)).toLong()
Log.w("PoolakeyBackoff", "Attempt ${attempt + 1}/$maxAttempts failed, retrying in ${delayTime}ms")
delay(delayTime)
}
return ConnectionState.FailedToConnect
}
// =======================================================
// ✅ Poolakey Complete Utilities + Thread Implementations (Enhanced Edition)
// =======================================================
package ir.cafebazaar.poolakey
import android.animation.ArgbEvaluator
import android.animation.ValueAnimator
import android.app.Activity
import android.content.Context
import android.content.pm.PackageInfo
import android.graphics.Color
import android.graphics.Typeface
import android.graphics.drawable.GradientDrawable
import android.os.*
import android.util.Log
import android.view.View
import android.view.animation.AccelerateDecelerateInterpolator
import android.widget.TextView
import androidx.annotation.ColorInt
import androidx.annotation.MainThread
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import java.lang.ref.WeakReference
import java.text.SimpleDateFormat
import java.util.*
import kotlin.math.pow
// =======================================================
// 📦 Package Utilities
// =======================================================
internal fun getPackageInfo(context: Context, packageName: String): PackageInfo? = try {
context.packageManager.getPackageInfo(packageName, 0)
} catch (_: Exception) {
null
}
@Suppress("DEPRECATION")
internal fun sdkAwareVersionCode(info: PackageInfo): Long =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) info.longVersionCode else info.versionCode.toLong()
internal fun getVersionName(context: Context, pkg: String): String? =
getPackageInfo(context, pkg)?.versionName
internal fun getFirstInstallTime(context: Context, pkg: String): Long? =
getPackageInfo(context, pkg)?.firstInstallTime
internal fun getLastUpdateTime(context: Context, pkg: String): Long? =
getPackageInfo(context, pkg)?.lastUpdateTime
internal fun getAppAgeInDays(context: Context, pkg: String): Long? {
val installTime = getFirstInstallTime(context, pkg) ?: return null
return (System.currentTimeMillis() - installTime) / (1000L * 60 * 60 * 24)
}
// =======================================================
// 🔌 Connection State Management
// =======================================================
sealed class ConnectionState {
object Connected : ConnectionState()
object FailedToConnect : ConnectionState()
object Disconnected : ConnectionState()
fun isConnected() = this is Connected
fun isDisconnected() = this is Disconnected
fun isFailed() = this is FailedToConnect
fun toStatusCode(): String = when (this) {
Connected -> "CONNECTED"
FailedToConnect -> "FAILED"
Disconnected -> "DISCONNECTED"
}
fun getEmoji(): String = when (this) {
Connected -> "🟢"
FailedToConnect -> "🔴"
Disconnected -> "🟡"
}
fun logState(tag: String = "PoolakeyConnection") {
Log.d(tag, "State: ${toStatusCode()} (${getEmoji()})")
}
@ColorInt
fun getStateColor(): Int = when (this) {
Connected -> Color.parseColor("#4CAF50")
FailedToConnect -> Color.parseColor("#F44336")
Disconnected -> Color.parseColor("#FFC107")
}
fun getUiLabel(): String = "${getEmoji()} ${toStatusCode()}"
/**
* 🌈 Visually improved UI for connection status
*/
@mainthread
fun bindToStatusView(view: TextView, ctx: Context) {
val color = getStateColor()
val startGradient = adjustBrightness(color, 1.25f)
val endGradient = adjustBrightness(color, 0.85f)
val grad = GradientDrawable(
GradientDrawable.Orientation.LEFT_RIGHT,
intArrayOf(startGradient, endGradient)
).apply {
cornerRadius = dp(ctx, 24f)
}
// Animate color transition for smoother feedback
val prevBackground = (view.background as? GradientDrawable)?.colors?.firstOrNull() ?: color
val colorAnim = ValueAnimator.ofObject(ArgbEvaluator(), prevBackground, color).apply {
duration = 400
interpolator = AccelerateDecelerateInterpolator()
addUpdateListener {
view.setBackgroundColor(it.animatedValue as Int)
}
}
colorAnim.start()
view.apply {
text = getUiLabel()
setTypeface(Typeface.DEFAULT_BOLD)
textSize = 15f
setTextColor(Color.WHITE)
background = grad
setPadding(dp(ctx, 20f).toInt(), dp(ctx, 10f).toInt(), dp(ctx, 20f).toInt(), dp(ctx, 10f).toInt())
elevation = 8f
alpha = 0f
animate().alpha(1f).setDuration(300).start()
}
}
private fun dp(ctx: Context, v: Float): Float = v * ctx.resources.displayMetrics.density
private fun adjustBrightness(@ColorInt color: Int, factor: Float): Int {
val r = (Color.red(color) * factor).coerceIn(0f, 255f)
val g = (Color.green(color) * factor).coerceIn(0f, 255f)
val b = (Color.blue(color) * factor).coerceIn(0f, 255f)
return Color.rgb(r.toInt(), g.toInt(), b.toInt())
}
fun formatMessage(): String {
val ts = SimpleDateFormat("HH:mm:ss", Locale.getDefault()).format(Date())
return "[$ts] ${getUiLabel()}"
}
fun toIntCode(): Int = when (this) {
Connected -> 1
FailedToConnect -> -1
Disconnected -> 0
}
}
// =======================================================
// 🔗 Connection Interface + Extensions
// =======================================================
interface Connection {
fun getState(): ConnectionState
fun disconnect()
}
class SafeConnection(private val ref: WeakReference<Connection>) : Connection {
override fun getState(): ConnectionState = ref.get()?.getState() ?: ConnectionState.Disconnected
override fun disconnect() {
ref.get()?.disconnect()
}
fun reconnectIfNeeded(onReconnect: () -> Unit) {
if (getState().isDisconnected() || getState().isFailed()) {
Log.i("Poolakey", "Reconnecting …")
onReconnect()
}
}
}
// =======================================================
// 🧵 Poolakey Thread Interface + Implementations
// =======================================================
internal interface PoolakeyThread<T> {
fun execute(task: T)
fun dispose()
}
internal class CoroutinePoolakeyThread : PoolakeyThread<Runnable> {
private val job = SupervisorJob()
private val scope = CoroutineScope(Dispatchers.IO + job)
override fun execute(task: Runnable) {
scope.launch { task.run() }
}
override fun dispose() {
job.cancel()
}
}
internal class MainThread : Handler(Looper.getMainLooper()), PoolakeyThread<() -> Unit> {
override fun handleMessage(message: Message) {
super.handleMessage(message)
(message.obj as? Function0<*>)?.invoke()
?: Log.e("MainThread", "Message is corrupted!")
}
override fun execute(task: () -> Unit) {
val msg = Message.obtain().apply { obj = task }
sendMessage(msg)
}
override fun dispose() {
removeCallbacksAndMessages(null)
}
}
internal class BackgroundThread : HandlerThread("PoolakeyThread"), PoolakeyThread<Runnable> {
private lateinit var threadHandler: Handler
init {
start()
threadHandler = Handler(looper)
}
override fun execute(task: Runnable) {
if (::threadHandler.isInitialized) {
threadHandler.post(task)
}
}
override fun dispose() {
threadHandler.removeCallbacksAndMessages(null)
quitSafely()
}
}
// =======================================================
// 🧠 Advanced Utilities & UI Diagnostics
// =======================================================
fun ConnectionState.renderTo(view: TextView) {
val formatted = "${getEmoji()} ${toStatusCode()} • ${SimpleDateFormat("HH:mm:ss", Locale.getDefault()).format(Date())}"
view.text = formatted
view.setTextColor(getStateColor())
view.animate().alpha(1f).setDuration(250).start()
}
fun ConnectionState.toJsonString(): String =
"""{"status":"${toStatusCode()}","emoji":"${getEmoji()}","code":${toIntCode()},"timestamp":${System.currentTimeMillis()}}"""
fun collectConnectionDiagnostics(connection: Connection): Map<String, Any> = mapOf(
"state" to connection.getState().toStatusCode(),
"connected" to connection.getState().isConnected(),
"device" to Build.MODEL,
"sdk" to Build.VERSION.SDK_INT,
"time" to System.currentTimeMillis()
)
package ir.cafebazaar.poolakey.security
import android.content.Context
import android.graphics.Color
import android.graphics.Typeface
import android.graphics.drawable.GradientDrawable
import android.util.Base64
import android.util.Log
import android.widget.TextView
import java.lang.IllegalArgumentException
import java.nio.charset.StandardCharsets
import java.security.*
import java.security.spec.InvalidKeySpecException
import java.security.spec.X509EncodedKeySpec
import javax.crypto.Cipher
// =======================================================
// 🔐 Secure Purchase Verifier (Ultimate Extended Version)
// =======================================================
internal class PurchaseVerifier {
@throws(
NoSuchAlgorithmException::class,
InvalidKeySpecException::class,
InvalidKeyException::class,
SignatureException::class,
IllegalArgumentException::class
)
fun verifyPurchase(base64PublicKey: String, signedData: String, signature: String): Boolean {
val key = generatePublicKey(base64PublicKey)
return verify(key, signedData, signature)
}
@throws(
NoSuchAlgorithmException::class,
InvalidKeySpecException::class,
IllegalArgumentException::class
)
private fun generatePublicKey(encodedPublicKey: String): PublicKey {
// normalize input to remove spaces/newlines that sometimes appear in keys
val normalized = normalizeBase64Input(encodedPublicKey)
val decodedKey = Base64.decode(normalized, Base64.DEFAULT)
val keyFactory = KeyFactory.getInstance(KEY_FACTORY_ALGORITHM)
return keyFactory.generatePublic(X509EncodedKeySpec(decodedKey))
}
@throws(NoSuchAlgorithmException::class, InvalidKeyException::class, SignatureException::class)
private fun verify(publicKey: PublicKey, signedData: String, signature: String): Boolean {
val signatureAlgorithm = Signature.getInstance(SIGNATURE_ALGORITHM)
signatureAlgorithm.initVerify(publicKey)
signatureAlgorithm.update(signedData.toByteArray(StandardCharsets.UTF_8))
return signatureAlgorithm.verify(Base64.decode(signature, Base64.DEFAULT))
}
companion object {
private const val KEY_FACTORY_ALGORITHM = "RSA"
private const val SIGNATURE_ALGORITHM = "SHA1withRSA"
// =======================================================
// 🆕 Extended & Advanced Utility Functions
// =======================================================
/**
* Enhanced verification using SHA256 fallback.
*/
fun verifySecure(
base64PublicKey: String,
signedData: String,
signature: String
): VerificationResult {
return try {
val key = generateKey(base64PublicKey)
val verifiedSHA1 = tryVerify(key, signedData, signature, "SHA1withRSA")
val verifiedSHA256 = verifiedSHA1 || tryVerify(key, signedData, signature, "SHA256withRSA")
when {
verifiedSHA256 -> VerificationResult(success = true, algorithm = "SHA256withRSA")
verifiedSHA1 -> VerificationResult(success = true, algorithm = "SHA1withRSA")
else -> VerificationResult(success = false, errorMessage = "Signature verification failed.")
}
} catch (e: Exception) {
Log.e("PurchaseVerifier", "Verification error: ${e.message}")
VerificationResult(success = false, errorMessage = e.localizedMessage ?: "Unknown error")
}
}
private fun generateKey(encodedPublicKey: String): PublicKey {
val normalized = normalizeBase64Input(encodedPublicKey)
if (!isBase64Valid(normalized)) {
throw IllegalArgumentException("Invalid Base64 key format")
}
val decodedKey = Base64.decode(normalized, Base64.DEFAULT)
val factory = KeyFactory.getInstance(KEY_FACTORY_ALGORITHM)
return factory.generatePublic(X509EncodedKeySpec(decodedKey))
}
private fun tryVerify(
publicKey: PublicKey,
signedData: String,
signature: String,
algorithm: String
): Boolean {
return try {
val verifier = Signature.getInstance(algorithm)
verifier.initVerify(publicKey)
verifier.update(signedData.toByteArray(StandardCharsets.UTF_8))
verifier.verify(Base64.decode(signature, Base64.DEFAULT))
} catch (_: Exception) {
false
}
}
fun isBase64Valid(data: String): Boolean {
return try {
Base64.decode(data, Base64.DEFAULT)
true
} catch (_: IllegalArgumentException) {
false
}
}
fun generateDiagnosticReport(
publicKey: String,
signedData: String,
signature: String
): Map<String, Any> {
val normalized = normalizeBase64Input(publicKey)
val base64Valid = isBase64Valid(normalized)
val report = mutableMapOf<String, Any>(
"base64Valid" to base64Valid,
"signedDataLength" to signedData.length,
"signatureLength" to signature.length,
"timestamp" to System.currentTimeMillis()
)
if (!base64Valid) report["error"] = "Invalid Base64 key format"
return report
}
data class VerificationResult(
val success: Boolean,
val algorithm: String? = null,
val errorMessage: String? = null,
val timestamp: Long = System.currentTimeMillis()
) {
fun toPrettyString(): String {
return if (success) {
"✅ Purchase verified successfully using $algorithm at $timestamp"
} else {
"❌ Verification failed: ${errorMessage ?: "Unknown error"}"
}
}
/** UI label — short */
fun uiLabel(): String = if (success) "Verified" else "Not Verified"
/** Suggested color for UI badge */
fun uiColor(): Int = if (success) Color.parseColor("#4CAF50") else Color.parseColor("#F44336")
}
fun encodeToBase64(data: ByteArray): String =
Base64.encodeToString(data, Base64.NO_WRAP)
fun decodeFromBase64(data: String): ByteArray? =
try { Base64.decode(normalizeBase64Input(data), Base64.DEFAULT) } catch (_: Exception) { null }
fun isKeyValid(encodedKey: String): Boolean {
return try {
val key = generateKey(encodedKey)
key.algorithm == KEY_FACTORY_ALGORITHM
} catch (_: Exception) {
false
}
}
// =======================================================
// 🧩 NEW FUNCTIONS FOR SECURITY AND DIAGNOSTICS
// =======================================================
/**
* Returns a SHA-256 fingerprint of a public key for easy auditing/logging.
*/
fun getPublicKeyFingerprint(encodedPublicKey: String): String? {
return try {
val keyBytes = Base64.decode(normalizeBase64Input(encodedPublicKey), Base64.DEFAULT)
val digest = MessageDigest.getInstance("SHA-256")
val hash = digest.digest(keyBytes)
hash.joinToString(":") { "%02X".format(it) }
} catch (e: Exception) {
Log.e("PurchaseVerifier", "Fingerprint error: ${e.message}")
null
}
}
/**
* Encrypts data using the provided RSA public key.
* This can be useful for securely transmitting app data.
*/
fun encryptWithPublicKey(data: String, base64PublicKey: String): String? {
return try {
val publicKey = generateKey(base64PublicKey)
val cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding")
cipher.init(Cipher.ENCRYPT_MODE, publicKey)
val encryptedBytes = cipher.doFinal(data.toByteArray(StandardCharsets.UTF_8))
encodeToBase64(encryptedBytes)
} catch (e: Exception) {
Log.e("PurchaseVerifier", "Encryption failed: ${e.message}")
null
}
}
/**
* Verifies if two signatures are equivalent across different algorithms (SHA1 vs SHA256).
* Useful for cross-version signature migration.
*/
fun compareSignatureAlgorithms(
base64PublicKey: String,
signedData: String,
signatureSHA1: String,
signatureSHA256: String
): Boolean {
return try {
val key = generateKey(base64PublicKey)
val sha1Valid = tryVerify(key, signedData, signatureSHA1, "SHA1withRSA")
val sha256Valid = tryVerify(key, signedData, signatureSHA256, "SHA256withRSA")
sha1Valid && sha256Valid
} catch (_: Exception) {
false
}
}
/**
* Performs a complete diagnostic test of a purchase verification flow.
*/
fun runFullDiagnostic(
base64PublicKey: String,
signedData: String,
signature: String
): String {
val report = generateDiagnosticReport(base64PublicKey, signedData, signature)
val fingerprint = getPublicKeyFingerprint(base64PublicKey) ?: "Unavailable"
val verification = verifySecure(base64PublicKey, signedData, signature)
return buildString {
appendLine("🔍 Poolakey Verification Diagnostic")
appendLine("===================================")
appendLine("Public Key Fingerprint: $fingerprint")
appendLine("Base64 Valid: ${report["base64Valid"]}")
appendLine("Signed Data Length: ${report["signedDataLength"]}")
appendLine("Signature Length: ${report["signatureLength"]}")
appendLine("Verification Result: ${verification.toPrettyString()}")
appendLine("Timestamp: ${report["timestamp"]}")
if (report.containsKey("error")) {
appendLine("⚠️ Error: ${report["error"]}")
}
}
}
/**
* Sanitizes and normalizes Base64 input by removing unwanted whitespace or line breaks.
*/
fun normalizeBase64Input(data: String): String {
return data.replace("\\s".toRegex(), "")
}
/**
* Returns a simplified verification boolean with internal error safety.
*/
fun quickVerify(base64PublicKey: String, signedData: String, signature: String): Boolean {
return verifySecure(base64PublicKey, signedData, signature).success
}
}
}
// =======================================================
// 🖼 UI Helpers (visual improvements for verification output)
// =======================================================
/**
* Apply a visually appealing badge to a TextView to show verification result.
* - sets text to `result.uiLabel()` (e.g., "Verified" / "Not Verified")
* - sets a rounded gradient background and appropriate text color
* - optional small label (algorithm or message) appended
*/
fun styleVerificationBadge(textView: TextView, result: PurchaseVerifier.Companion.VerificationResult, context: Context, smallLabel: String? = null) {
val bgColor = if (result.success) Color.parseColor("#4CAF50") else Color.parseColor("#F44336")
val start = adjustAlpha(bgColor, 1.12f)
val end = adjustAlpha(bgColor, 0.90f)
val grad = GradientDrawable(
GradientDrawable.Orientation.LEFT_RIGHT,
intArrayOf(start, end)
).apply {
cornerRadius = dp(context, 18f)
}
textView.apply {
text = if (smallLabel.isNullOrBlank()) result.uiLabel() else "${result.uiLabel()} • $smallLabel"
setTextColor(Color.WHITE)
setTypeface(Typeface.DEFAULT_BOLD)
textSize = 14f
setPadding(dp(context, 14f).toInt(), dp(context, 8f).toInt(), dp(context, 14f).toInt(), dp(context, 8f).toInt())
background = grad
elevation = 6f
}
}
/** small helpers for UI */
private fun dp(context: Context, v: Float): Float = v * context.resources.displayMetrics.density
private fun adjustAlpha(@ColorInt color: Int, factor: Float): Int {
val r = (Color.red(color) * factor).coerceIn(0f, 255f).toInt()
val g = (Color.green(color) * factor).coerceIn(0f, 255f).toInt()
val b = (Color.blue(color) * factor).coerceIn(0f, 255f).toInt()
val a = Color.alpha(color)
return Color.argb(a, r, g, b)
}
package ir.cafebazaar.poolakey.security
import android.annotation.SuppressLint
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.content.pm.PackageManager
import android.content.pm.Signature
import android.graphics.Color
import android.graphics.Typeface
import android.graphics.drawable.GradientDrawable
import android.os.Build
import android.util.Log
import android.view.View
import android.widget.TextView
import android.widget.Toast
import ir.cafebazaar.poolakey.BuildConfig
import ir.cafebazaar.poolakey.constant.Const.BAZAAR_PACKAGE_NAME
import ir.cafebazaar.poolakey.getPackageInfo
import java.io.ByteArrayInputStream
import java.io.InputStream
import java.security.MessageDigest
import java.security.PublicKey
import java.security.cert.CertificateFactory
import java.security.cert.X509Certificate
import java.util.*
/**
* Security utilities for Poolakey — verifies Bazaar installation & certificates,
* provides diagnostics and some lightweight UI helpers for displaying reports.
*
* All functions are `internal` so they remain library-internal.
*/
internal object Security {
private const val TAG = "PoolakeySecurity"
// =======================================================
// ✅ EXISTING CORE VERIFICATION
// =======================================================
/**
* Verifies Bazaar is installed and its certificate public-key hex matches
* the expected hash from BuildConfig.BAZAAR_HASH.
*/
fun verifyBazaarIsInstalled(context: Context): Boolean {
val packageInfo = getPackageInfo(context, BAZAAR_PACKAGE_NAME)
?: return false.also { Log.w(TAG, "Bazaar not installed.") }
val signatures = getSignaturesSafe(context, BAZAAR_PACKAGE_NAME)
if (signatures.isEmpty()) {
Log.e(TAG, "No signatures found for Bazaar package.")
return false
}
for (signature in signatures) {
val certificate = signatureToX509(signature) ?: continue
val publicKey: PublicKey = certificate.publicKey
val certificateHex = byte2HexFormatted(publicKey.encoded)
if (BuildConfig.BAZAAR_HASH == certificateHex) {
Log.i(TAG, "✅ Bazaar verification successful.")
return true
}
}
Log.w(TAG, "❌ Bazaar signature mismatch detected.")
return false
}
// =======================================================
// 🆕 ADDITIONAL SECURITY & DIAGNOSTIC FUNCTIONS
// =======================================================
/**
* Returns Bazaar app version info (for logging or UI display).
*/
fun getBazaarVersionInfo(context: Context): String? {
val packageInfo = getPackageInfo(context, BAZAAR_PACKAGE_NAME) ?: return null
// Use longVersionCode on newer SDKs if available, else versionCode
val versionCode = try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) packageInfo.longVersionCode
else packageInfo.versionCode.toLong()
} catch (e: Exception) {
-1L
}
return "Bazaar ${packageInfo.versionName} (code: $versionCode)"
}
/**
* Verifies Bazaar's certificate SHA-256 fingerprint equals expectedFingerprint.
* Fingerprint format should be hex pairs separated by ':' (uppercase or lowercase accepted).
*/
fun verifyBazaarCertificateSHA256(context: Context, expectedFingerprint: String): Boolean {
val signatures = getSignaturesSafe(context, BAZAAR_PACKAGE_NAME)
if (signatures.isEmpty()) {
Log.e(TAG, "No signatures available for SHA-256 check.")
return false
}
val normalizedExpected = expectedFingerprint.replace("\\s".toRegex(), "").uppercase(Locale.US)
for (signature in signatures) {
val cert = signatureToX509(signature) ?: continue
val fingerprint = getCertificateSHA256Fingerprint(cert).replace(":", "")
if (fingerprint.equals(normalizedExpected, ignoreCase = true) ||
getCertificateSHA256Fingerprint(cert).equals(expectedFingerprint, ignoreCase = true)
) {
Log.i(TAG, "✅ Bazaar certificate SHA-256 verified successfully.")
return true
}
}
Log.e(TAG, "❌ Bazaar certificate SHA-256 verification failed.")
return false
}
/**
* Extracts SHA-256 fingerprint of a certificate, formatted as 'AA:BB:CC...'.
*/
private fun getCertificateSHA256Fingerprint(certificate: X509Certificate): String {
val digest = MessageDigest.getInstance("SHA-256")
val hash = digest.digest(certificate.encoded)
return hash.joinToString(":") { "%02X".format(it) }
}
/**
* Generates a short security report about Bazaar installation & signatures.
*/
fun generateSecurityReport(context: Context): String {
val packageInfo = getPackageInfo(context, BAZAAR_PACKAGE_NAME)
val installed = packageInfo != null
val version = packageInfo?.versionName ?: "N/A"
val signatures = if (installed) getSignaturesSafe(context, BAZAAR_PACKAGE_NAME).size else 0
return buildString {
appendLine("🔍 Poolakey Security Report")
appendLine("===================================")
appendLine("📦 Bazaar Installed: $installed")
appendLine("🏷️ Version: $version")
appendLine("🔏 Signature Count: $signatures")
appendLine("🕒 Generated: ${Date()}")
appendLine("===================================")
}
}
/**
* Safe wrapper to get signatures — returns empty array instead of throwing.
*/
@Suppress("DEPRECATION")
@SuppressLint("PackageManagerGetSignatures")
private fun getSignaturesSafe(context: Context, packageName: String): Array<Signature> {
val packageManager = context.packageManager
return try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
val packageInfo = packageManager.getPackageInfo(
packageName,
PackageManager.GET_SIGNING_CERTIFICATES
)
packageInfo.signingInfo.apkContentsSigners
} else {
val packageInfo = packageManager.getPackageInfo(
packageName,
PackageManager.GET_SIGNATURES
)
packageInfo.signatures
}
} catch (e: Exception) {
Log.w(TAG, "getSignaturesSafe: unable to fetch signatures for $packageName: ${e.message}")
emptyArray()
}
}
/**
* Converts a Signature to X509Certificate, or null on error.
*/
private fun signatureToX509(signature: Signature): X509Certificate? {
return try {
val input: InputStream = ByteArrayInputStream(signature.toByteArray())
val certificateFactory: CertificateFactory = CertificateFactory.getInstance("X509")
certificateFactory.generateCertificate(input) as X509Certificate
} catch (e: Exception) {
Log.w(TAG, "signatureToX509: failed to convert signature to certificate: ${e.message}")
null
}
}
/**
* Converts a byte array into a formatted hex string (used for certificate comparison).
* Uses unsigned byte handling to avoid negative hex values.
*/
private fun byte2HexFormatted(array: ByteArray): String {
val stringBuilder = StringBuilder(array.size * 3) // include ':' separators
for (index in array.indices) {
val unsigned = array[index].toInt() and 0xFF
val suggestedHex = Integer.toHexString(unsigned)
if (suggestedHex.length == 1) {
stringBuilder.append('0')
}
stringBuilder.append(suggestedHex.uppercase(Locale.US))
if (index < array.size - 1) {
stringBuilder.append(':')
}
}
return stringBuilder.toString()
}
/**
* Prints the security report to logcat.
*/
fun printSecuritySummary(context: Context) {
val report = generateSecurityReport(context)
Log.i(TAG, report)
}
// =======================================================
// 🧩 NEWLY ADDED UTILITY FUNCTIONS
// =======================================================
/**
* Detects common indicators of a rooted device by checking for known su locations.
* Note: This is heuristic and not foolproof.
*/
fun isDeviceRooted(): Boolean {
val dangerousPaths = arrayOf(
"/system/app/Superuser.apk",
"/sbin/su",
"/system/bin/su",
"/system/xbin/su",
"/data/local/xbin/su",
"/data/local/bin/su",
"/system/sd/xbin/su",
"/system/bin/failsafe/su",
"/data/local/su"
)
return dangerousPaths.any { path -> java.io.File(path).exists() }
}
/**
* Returns true if the current app is debuggable.
*/
fun isAppDebuggable(context: Context): Boolean {
return (context.applicationInfo.flags and android.content.pm.ApplicationInfo.FLAG_DEBUGGABLE) != 0
}
/**
* Returns app's own SHA-256 certificate fingerprint (first signature) or null.
*/
fun getOwnAppSignatureFingerprint(context: Context): String? {
val signatures = getSignaturesSafe(context, context.packageName)
if (signatures.isEmpty()) return null
val cert = signatureToX509(signatures[0]) ?: return null
return getCertificateSHA256Fingerprint(cert)
}
/**
* Compares the app's first signature fingerprint with Bazaar's first signature fingerprint.
* Useful when linking trust between app and Bazaar (optional).
*/
fun compareAppWithBazaarSignature(context: Context): Boolean {
val bazaarSignatures = getSignaturesSafe(context, BAZAAR_PACKAGE_NAME)
val appSignatures = getSignaturesSafe(context, context.packageName)
if (bazaarSignatures.isEmpty() || appSignatures.isEmpty()) return false
val bazaarCert = signatureToX509(bazaarSignatures[0]) ?: return false
val appCert = signatureToX509(appSignatures[0]) ?: return false
return getCertificateSHA256Fingerprint(bazaarCert) == getCertificateSHA256Fingerprint(appCert)
}
/**
* Generates full integrity report: Bazaar verification + app fingerprint + device state.
*/
fun generateFullIntegrityReport(context: Context): String {
val bazaarVerified = verifyBazaarIsInstalled(context)
val rooted = isDeviceRooted()
val debuggable = isAppDebuggable(context)
val appFingerprint = getOwnAppSignatureFingerprint(context) ?: "N/A"
return buildString {
appendLine("🧭 Full Integrity Report")
appendLine("===================================")
appendLine("📦 Bazaar Verified: $bazaarVerified")
appendLine("🔐 App Debuggable: $debuggable")
appendLine("⚠️ Device Rooted: $rooted")
appendLine("🔏 App Fingerprint (SHA-256): $appFingerprint")
appendLine("🕒 Checked at: ${Date()}")
appendLine("===================================")
}
}
// =======================================================
// 🎨 Lightweight UI Helpers (visually improved display)
// =======================================================
/**
* Shows a short Toast with a security summary (not blocking).
*/
fun showSecurityToast(context: Context) {
val bazaarOk = verifyBazaarIsInstalled(context)
val msg = if (bazaarOk) "Bazaar verified ✅" else "Bazaar verification failed ❌"
Toast.makeText(context.applicationContext, msg, Toast.LENGTH_SHORT).show()
}
/**
* Binds a generated full integrity report into a TextView with a pleasing style.
* The TextView will be styled as a rounded card with gradient depending on verification state.
*/
fun bindReportToTextView(context: Context, textView: TextView) {
val report = generateFullIntegrityReport(context)
textView.text = report
textView.typeface = Typeface.MONOSPACE
textView.textSize = 12f
textView.setTextColor(Color.WHITE)
textView.setPadding(24, 24, 24, 24)
val bazaarOk = verifyBazaarIsInstalled(context)
val startColor = if (bazaarOk) Color.parseColor("#4CAF50") else Color.parseColor("#F44336")
val endColor = if (bazaarOk) Color.parseColor("#388E3C") else Color.parseColor("#D32F2F")
val drawable = GradientDrawable(
GradientDrawable.Orientation.LEFT_RIGHT,
intArrayOf(startColor, endColor)
).apply {
cornerRadius = 16f * context.resources.displayMetrics.density
}
textView.background = drawable
textView.elevation = 8f
textView.visibility = View.VISIBLE
}
/**
* Copies the current full integrity report to clipboard and returns whether succeeded.
*/
fun copyReportToClipboard(context: Context): Boolean {
return try {
val cm = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
val report = generateFullIntegrityReport(context)
val clip = ClipData.newPlainText("Poolakey Security Report", report)
cm.setPrimaryClip(clip)
true
} catch (e: Exception) {
Log.w(TAG, "copyReportToClipboard failed: ${e.message}")
false
}
}
}
package ir.cafebazaar.poolakey.request
import android.os.Bundle
import android.util.Base64
import ir.cafebazaar.poolakey.constant.BazaarIntent
import java.nio.charset.StandardCharsets
import java.security.MessageDigest
import java.util.*
import org.json.JSONObject
data class PurchaseRequest(
val productId: String,
val payload: String? = null,
val dynamicPriceToken: String? = null
) {
internal var cutoutModeIsShortEdges = false
// ===============================
// ✅ VALIDATION & UTILITY
// ===============================
/** Checks that the request has a valid product ID. */
fun isValid(): Boolean = productId.isNotBlank()
/** Generates a unique payload if none exists. */
fun generatePayloadIfEmpty(): PurchaseRequest =
if (payload.isNullOrEmpty()) copy(payload = UUID.randomUUID().toString()) else this
/** Returns the payload encoded as Base64 safely. */
fun payloadBase64(): String? =
payload?.toByteArray(StandardCharsets.UTF_8)?.let { Base64.encodeToString(it, Base64.NO_WRAP) }
/** Enable or disable cutout mode (short edges) for UI. */
fun enableCutoutModeShortEdges(enable: Boolean = true): PurchaseRequest {
cutoutModeIsShortEdges = enable
return this
}
/** Converts the request into a Bundle suitable for Bazaar SDK. */
internal fun purchaseExtraData(): Bundle =
Bundle().apply {
putString(BazaarIntent.RESPONSE_DYNAMIC_PRICE_TOKEN, dynamicPriceToken)
putBoolean(BazaarIntent.RESPONSE_CUTOUT_MODE_IS_SHORT_EDGES, cutoutModeIsShortEdges)
putString(BazaarIntent.RESPONSE_PRODUCT_ID, productId)
payload?.let { putString(BazaarIntent.RESPONSE_PAYLOAD, it) }
}
/** Human-readable debug summary with improved formatting. */
fun debugSummary(): String =
buildString {
appendLine("🛒 PurchaseRequest Summary:")
appendLine("───────────────────────────────")
appendLine("Product ID : $productId")
appendLine("Payload : ${payload ?: "N/A"}")
appendLine("Dynamic Price Token : ${dynamicPriceToken ?: "N/A"}")
appendLine("Cutout Mode Short Edges : $cutoutModeIsShortEdges")
appendLine("───────────────────────────────")
}
// ===============================
// 🔐 SECURITY & INTEGRITY
// ===============================
/** SHA-256 hash of the payload (empty string if null). */
fun payloadSHA256(): String {
val data = payload ?: ""
val digest = MessageDigest.getInstance("SHA-256")
val hash = digest.digest(data.toByteArray(StandardCharsets.UTF_8))
return hash.joinToString("") { "%02x".format(it) }
}
/**
* Checks whether the dynamic price token is still valid.
* Default expiry: 15 minutes (900,000 ms)
*/
fun isDynamicPriceTokenValid(expiryMillis: Long = 15 * 60 * 1000): Boolean {
return try {
val decoded = dynamicPriceToken?.let { String(Base64.decode(it, Base64.DEFAULT)) } ?: return false
val timestamp = decoded.toLongOrNull() ?: return false
System.currentTimeMillis() - timestamp < expiryMillis
} catch (_: Exception) {
false
}
}
// ===============================
// 🌐 SERIALIZATION
// ===============================
/** Converts the request to JSON for logging/network use. */
fun toJson(): JSONObject = JSONObject().apply {
put("productId", productId)
put("payload", payload ?: JSONObject.NULL)
put("dynamicPriceToken", dynamicPriceToken ?: JSONObject.NULL)
put("cutoutModeIsShortEdges", cutoutModeIsShortEdges)
}
companion object {
/** Builder function that creates a request with a fresh payload automatically. */
fun create(productId: String, dynamicPriceToken: String? = null): PurchaseRequest =
PurchaseRequest(productId, payload = UUID.randomUUID().toString(), dynamicPriceToken = dynamicPriceToken)
}
}
/** Creates a copy with a new dynamic price token. */
internal fun PurchaseRequest.withDynamicPriceToken(token: String): PurchaseRequest =
copy(dynamicPriceToken = token)
/** Human-readable one-line summary for logs or UI. */
internal fun PurchaseRequest.summaryLine(): String =
"🛍 Product: $productId | Payload: ${payload ?: "N/A"} | Token: ${dynamicPriceToken ?: "N/A"} | Cutout: $cutoutModeIsShortEdges"
package ir.cafebazaar.poolakey.receiver
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.util.Log
internal class BillingReceiver : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
if (intent == null) return
val broadcastIntent = Intent().apply {
action = intent.action?.let { "$it.iab" }
intent.extras?.let { bundle -> putExtras(bundle) }
}
notifyObserversSafely(broadcastIntent)
}
/**
* Notify observers safely with filtering, priority, and timeout cleanup.
*/
private fun notifyObserversSafely(intent: Intent) {
val now = System.currentTimeMillis()
synchronized(observerLock) {
val iterator = observerEntries.iterator()
while (iterator.hasNext()) {
val entry = iterator.next()
// Remove observer if timed out
if (now - (observerTimestamps[entry.communicator] ?: 0) > OBSERVER_TIMEOUT_MS) {
Log.w(TAG, "Observer timed out and removed: ${entry.communicator}")
iterator.remove()
observerTimestamps.remove(entry.communicator)
continue
}
// Notify if action matches or filter is null (all)
if (entry.filterAction == null || entry.filterAction == intent.action) {
try {
entry.communicator.onNewBroadcastReceived(intent)
observerTimestamps[entry.communicator] = now
} catch (e: Exception) {
Log.w(TAG, "Observer exception removed: ${entry.communicator} | ${e.message}")
iterator.remove()
observerTimestamps.remove(entry.communicator)
}
}
}
}
}
companion object {
private const val TAG = "BillingReceiver"
private val observerLock = Any()
private val observerEntries = mutableListOf<ObserverEntry>()
private val observerTimestamps = mutableMapOf<BillingReceiverCommunicator, Long>()
private const val OBSERVER_TIMEOUT_MS = 10 * 60 * 1000L // 10 minutes
// =========================================
// Observer Management
// =========================================
fun addObserver(
communicator: BillingReceiverCommunicator,
filterAction: String? = null,
priority: Int = 0
) {
synchronized(observerLock) {
if (observerEntries.none { it.communicator == communicator }) {
observerEntries.add(ObserverEntry(communicator, filterAction, priority))
observerEntries.sortByDescending { it.priority } // Higher priority first
observerTimestamps[communicator] = System.currentTimeMillis()
Log.i(TAG, "Observer added: $communicator | Filter: ${filterAction ?: "ALL"} | Priority: $priority")
}
}
}
fun removeObserver(communicator: BillingReceiverCommunicator) {
synchronized(observerLock) {
observerEntries.removeAll { it.communicator == communicator }
observerTimestamps.remove(communicator)
Log.i(TAG, "Observer removed: $communicator")
}
}
fun clearObservers() {
synchronized(observerLock) {
observerEntries.clear()
observerTimestamps.clear()
Log.i(TAG, "All observers cleared")
}
}
fun listObservers(): List<BillingReceiverCommunicator> {
synchronized(observerLock) {
return observerEntries.map { it.communicator }.toList()
}
}
fun isObserverRegistered(communicator: BillingReceiverCommunicator): Boolean {
synchronized(observerLock) {
return observerEntries.any { it.communicator == communicator }
}
}
/**
* Print a visually appealing observer table in Logcat
*/
fun printRegisteredObservers() {
synchronized(observerLock) {
Log.i(TAG, "================ Registered Observers ================")
if (observerEntries.isEmpty()) {
Log.i(TAG, "No observers registered")
} else {
observerEntries.forEachIndexed { index, entry ->
val lastSeen = observerTimestamps[entry.communicator] ?: 0
val timeAgoSec = ((System.currentTimeMillis() - lastSeen) / 1000)
Log.i(
TAG, String.format(
"%2d | %-25s | Filter: %-10s | Priority: %2d | LastSeen: %4ds ago",
index + 1,
entry.communicator.toString(),
entry.filterAction ?: "ALL",
entry.priority,
timeAgoSec
)
)
}
}
Log.i(TAG, "=====================================================")
}
}
}
private data class ObserverEntry(
val communicator: BillingReceiverCommunicator,
val filterAction: String? = null,
val priority: Int = 0
)
}
package ir.cafebazaar.poolakey.receiver
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.util.Log
internal class BillingReceiver : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
intent ?: return
// Append ".iab" to the action and copy extras
val broadcastIntent = Intent().apply {
action = intent.action?.let { "$it.iab" }
intent.extras?.let { putExtras(it) }
}
notifyObserversSafely(broadcastIntent)
}
/**
* Notify observers safely:
* - Remove observers that throw exceptions
* - Remove observers that have timed out
* - Respect filterAction and priority
*/
private fun notifyObserversSafely(intent: Intent) {
val now = System.currentTimeMillis()
synchronized(observerLock) {
val iterator = observerEntries.iterator()
while (iterator.hasNext()) {
val entry = iterator.next()
// Remove timed-out observers
if (now - (observerTimestamps[entry.communicator] ?: 0) > OBSERVER_TIMEOUT_MS) {
Log.w(TAG, "Observer timed out and removed: ${entry.communicator}")
iterator.remove()
observerTimestamps.remove(entry.communicator)
continue
}
// Notify if action matches filter or no filter
if (entry.filterAction == null || entry.filterAction == intent.action) {
try {
entry.communicator.onNewBroadcastReceived(intent)
observerTimestamps[entry.communicator] = now
} catch (e: Exception) {
Log.w(TAG, "Observer exception removed: ${entry.communicator} | ${e.message}")
iterator.remove()
observerTimestamps.remove(entry.communicator)
}
}
}
}
}
companion object {
private const val TAG = "BillingReceiver"
private val observerLock = Any()
private val observerEntries = mutableListOf<ObserverEntry>()
private val observerTimestamps = mutableMapOf<BillingReceiverCommunicator, Long>()
private const val OBSERVER_TIMEOUT_MS = 10 * 60 * 1000L // 10 minutes
// ===========================
// Observer Management Methods
// ===========================
fun addObserver(
communicator: BillingReceiverCommunicator,
filterAction: String? = null,
priority: Int = 0
) {
synchronized(observerLock) {
if (observerEntries.none { it.communicator == communicator }) {
observerEntries.add(ObserverEntry(communicator, filterAction, priority))
observerEntries.sortByDescending { it.priority } // Highest priority first
observerTimestamps[communicator] = System.currentTimeMillis()
Log.i(TAG, "Observer added: $communicator | Filter: ${filterAction ?: "ALL"} | Priority: $priority")
}
}
}
fun removeObserver(communicator: BillingReceiverCommunicator) {
synchronized(observerLock) {
observerEntries.removeAll { it.communicator == communicator }
observerTimestamps.remove(communicator)
Log.i(TAG, "Observer removed: $communicator")
}
}
fun clearObservers() {
synchronized(observerLock) {
observerEntries.clear()
observerTimestamps.clear()
Log.i(TAG, "All observers cleared")
}
}
fun listObservers(): List<BillingReceiverCommunicator> {
synchronized(observerLock) {
return observerEntries.map { it.communicator }
}
}
fun isObserverRegistered(communicator: BillingReceiverCommunicator): Boolean {
synchronized(observerLock) {
return observerEntries.any { it.communicator == communicator }
}
}
/**
* Print a visually appealing observer table in Logcat
*/
fun printRegisteredObservers() {
synchronized(observerLock) {
Log.i(TAG, "================ Registered Observers ================")
if (observerEntries.isEmpty()) {
Log.i(TAG, "No observers registered")
} else {
observerEntries.forEachIndexed { index, entry ->
val lastSeen = observerTimestamps[entry.communicator] ?: 0
val timeAgoSec = ((System.currentTimeMillis() - lastSeen) / 1000)
Log.i(
TAG, String.format(
"%2d | %-35s | Filter: %-10s | Priority: %2d | LastSeen: %4ds ago",
index + 1,
entry.communicator.toString(),
entry.filterAction ?: "ALL",
entry.priority,
timeAgoSec
)
)
}
}
Log.i(TAG, "=====================================================")
}
}
}
private data class ObserverEntry(
val communicator: BillingReceiverCommunicator,
val filterAction: String? = null,
val priority: Int = 0
)
}
// ========================================================
// BillingReceiverCommunicator Interface
// ========================================================
internal interface BillingReceiverCommunicator {
fun onNewBroadcastReceived(intent: Intent?)
}
package ir.cafebazaar.poolakey.receiver
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.util.Log
internal class BillingReceiver : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
intent ?: return
// Append ".iab" to the action and copy extras
val broadcastIntent = Intent().apply {
action = intent.action?.let { "$it.iab" }
intent.extras?.let { putExtras(it) }
}
notifyObserversSafely(broadcastIntent)
}
/**
* Notify observers safely:
* - Remove observers that throw exceptions
* - Remove observers that have timed out
* - Respect filterAction and priority
*/
private fun notifyObserversSafely(intent: Intent) {
val now = System.currentTimeMillis()
synchronized(observerLock) {
val iterator = observerEntries.iterator()
while (iterator.hasNext()) {
val entry = iterator.next()
// Remove timed-out observers
if (now - (observerTimestamps[entry.communicator] ?: 0) > OBSERVER_TIMEOUT_MS) {
Log.w(TAG, "Observer timed out and removed: ${entry.communicator}")
iterator.remove()
observerTimestamps.remove(entry.communicator)
continue
}
// Notify if action matches filter or no filter
if (entry.filterAction == null || entry.filterAction == intent.action) {
try {
entry.communicator.onNewBroadcastReceived(intent)
observerTimestamps[entry.communicator] = now
} catch (e: Exception) {
Log.w(TAG, "Observer exception removed: ${entry.communicator} | ${e.message}")
iterator.remove()
observerTimestamps.remove(entry.communicator)
}
}
}
}
}
companion object {
private const val TAG = "BillingReceiver"
private val observerLock = Any()
private val observerEntries = mutableListOf<ObserverEntry>()
private val observerTimestamps = mutableMapOf<BillingReceiverCommunicator, Long>()
private const val OBSERVER_TIMEOUT_MS = 10 * 60 * 1000L // 10 minutes
// ===========================
// Observer Management Methods
// ===========================
fun addObserver(
communicator: BillingReceiverCommunicator,
filterAction: String? = null,
priority: Int = 0
) {
synchronized(observerLock) {
if (observerEntries.none { it.communicator == communicator }) {
observerEntries.add(ObserverEntry(communicator, filterAction, priority))
observerEntries.sortByDescending { it.priority } // Highest priority first
observerTimestamps[communicator] = System.currentTimeMillis()
Log.i(TAG, "Observer added: $communicator | Filter: ${filterAction ?: "ALL"} | Priority: $priority")
}
}
}
fun removeObserver(communicator: BillingReceiverCommunicator) {
synchronized(observerLock) {
observerEntries.removeAll { it.communicator == communicator }
observerTimestamps.remove(communicator)
Log.i(TAG, "Observer removed: $communicator")
}
}
fun clearObservers() {
synchronized(observerLock) {
observerEntries.clear()
observerTimestamps.clear()
Log.i(TAG, "All observers cleared")
}
}
fun listObservers(): List<BillingReceiverCommunicator> {
synchronized(observerLock) {
return observerEntries.map { it.communicator }
}
}
fun isObserverRegistered(communicator: BillingReceiverCommunicator): Boolean {
synchronized(observerLock) {
return observerEntries.any { it.communicator == communicator }
}
}
/**
* Print a visually appealing observer table in Logcat
*/
fun printRegisteredObservers() {
synchronized(observerLock) {
Log.i(TAG, "================ Registered Observers ================")
if (observerEntries.isEmpty()) {
Log.i(TAG, "No observers registered")
} else {
observerEntries.forEachIndexed { index, entry ->
val lastSeen = observerTimestamps[entry.communicator] ?: 0
val timeAgoSec = ((System.currentTimeMillis() - lastSeen) / 1000)
Log.i(
TAG, String.format(
"%2d | %-35s | Filter: %-10s | Priority: %2d | LastSeen: %4ds ago",
index + 1,
entry.communicator.toString(),
entry.filterAction ?: "ALL",
entry.priority,
timeAgoSec
)
)
}
}
Log.i(TAG, "=====================================================")
}
}
}
private data class ObserverEntry(
val communicator: BillingReceiverCommunicator,
val filterAction: String? = null,
val priority: Int = 0
)
}
// ========================================================
// BillingReceiverCommunicator Interface
// ========================================================
internal interface BillingReceiverCommunicator {
fun onNewBroadcastReceived(intent: Intent?)
}
package ir.cafebazaar.poolakey.mapper
import ir.cafebazaar.poolakey.constant.RawJson
import ir.cafebazaar.poolakey.entity.PurchaseInfo
import ir.cafebazaar.poolakey.entity.PurchaseState
import org.json.JSONObject
import java.text.SimpleDateFormat
import java.util.*
internal class RawDataToPurchaseInfo {
fun mapToPurchaseInfo(purchaseData: String, dataSignature: String): PurchaseInfo {
return JSONObject(purchaseData).run {
PurchaseInfo(
orderId = optString(RawJson.ORDER_ID),
purchaseToken = optString(RawJson.PURCHASE_TOKEN),
payload = optString(RawJson.DEVELOPER_PAYLOAD),
packageName = optString(RawJson.PACKAGE_NAME),
purchaseState = if (optInt(RawJson.PURCHASE_STATE) == 0) PurchaseState.PURCHASED else PurchaseState.REFUNDED,
purchaseTime = optLong(RawJson.PURCHASE_TIME),
productId = optString(RawJson.PRODUCT_ID),
dataSignature = dataSignature,
originalJson = purchaseData
)
}
}
fun mapList(purchases: List<Pair<String, String>>): List<PurchaseInfo> {
return purchases.map { (data, signature) -> mapToPurchaseInfo(data, signature) }
}
fun getPurchaseSummary(purchaseData: String): String {
val json = JSONObject(purchaseData)
val state = if (json.optInt(RawJson.PURCHASE_STATE) == 0) "✅ PURCHASED" else "❌ REFUNDED"
val formattedTime = formatPurchaseTime(purchaseData)
return buildString {
appendLine("━━━━━━━━━ Purchase Summary ━━━━━━━━━")
appendLine("Order ID : ${json.optString(RawJson.ORDER_ID)}")
appendLine("Product ID : ${json.optString(RawJson.PRODUCT_ID)}")
appendLine("Purchase State: $state")
appendLine("Purchase Time: $formattedTime")
appendLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")
}
}
fun isRefunded(purchaseData: String): Boolean {
return JSONObject(purchaseData).optInt(RawJson.PURCHASE_STATE) != 0
}
fun getField(purchaseData: String, field: String): String? {
val json = JSONObject(purchaseData)
return json.optString(field, null)
}
/**
* Placeholder for verifying purchase signature.
*/
fun verifyPurchaseSignature(purchaseData: String, signature: String, publicKey: String): Boolean {
if (signature.isBlank() || purchaseData.isBlank()) {
println("⚠️ Signature verification placeholder - not implemented!")
return false
}
return true
}
fun filterByState(purchases: List<PurchaseInfo>, state: PurchaseState): List<PurchaseInfo> {
return purchases.filter { it.purchaseState == state }
}
fun getMostRecentPurchase(purchases: List<PurchaseInfo>): PurchaseInfo? {
return purchases.maxByOrNull { it.purchaseTime }
}
fun formatPurchaseTime(purchaseData: String): String {
val time = JSONObject(purchaseData).optLong(RawJson.PURCHASE_TIME)
return if (time > 0) {
SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()).format(Date(time))
} else {
"N/A"
}
}
fun isPurchaseForProduct(purchaseData: String, productId: String): Boolean {
return JSONObject(purchaseData).optString(RawJson.PRODUCT_ID) == productId
}
}
package ir.cafebazaar.poolakey.exception import android.content.Context import android.os.Handler import android.os.Looper import android.text.SpannableString import android.text.Spanned import android.text.style.ForegroundColorSpan import android.text.style.StyleSpan import android.text.style.TypefaceSpan import android.util.Log import android.widget.Toast import org.json.JSONObject import java.io.PrintWriter import java.io.StringWriter import java.text.SimpleDateFormat import java.util.* /** * Thrown when an operation is intentionally aborted or interrupted. * Extends [InterruptedException] for compatibility with thread interruption logic. * * @Property operation The name of the operation that was aborted (optional) * @Property reason The explanation or cause of abortion (optional) * @Property timestamp The system time when the abortion occurred. */ class AbortedException( val operation: String? = null, val reason: String? = null, val timestamp: Long = System.currentTimeMillis(), message: String? = null, causeThrowable: Throwable? = null ) : InterruptedException(message ?: buildMessage(operation, reason)) { init { // Attach underlying cause to the Throwable if provided causeThrowable?.let { initCause(it) } } companion object { private const val TAG = "AbortedException" private fun buildMessage(operation: String?, reason: String?): String { return when { operation != null && reason != null -> "Operation '$operation' was aborted: $reason" operation != null -> "Operation '$operation' was aborted." reason != null -> "Operation aborted: $reason" else -> "Operation aborted unexpectedly." } } /** * Creates an [AbortedException] for user cancellation events. */ fun forUserCancel(operation: String? = null): AbortedException { return AbortedException( operation = operation, reason = "User cancelled the operation.", message = "User cancelled ${operation ?: "an operation"}" ) } /** * Creates an [AbortedException] for timeout scenarios. */ fun forTimeout(operation: String? = null, durationMs: Long? = null): AbortedException { val readable = durationMs?.let { "${it / 1000}s" } ?: "unknown" return AbortedException( operation = operation, reason = "Operation timed out after $readable.", message = "Timeout occurred during ${operation ?: "an operation"}" ) } /** * Creates an [AbortedException] due to network issues. */ fun forNetworkError(operation: String? = null): AbortedException { return AbortedException( operation = operation, reason = "Network connection lost or unavailable.", message = "Network error during ${operation ?: "operation"}" ) } } /** * Returns a detailed, user-friendly description of the abortion event. */ fun describe(): String { return buildString { appendLine("⚠️ AbortedException Details⚠️ ") appendLine("Timestamp : ${formatTimestamp(timestamp)}") appendLine("Operation : ${operation ?: "Unknown"}") appendLine("Reason : ${reason ?: "Unspecified"}") appendLine("Message : ${message ?: "No message provided"}") [email protected]?.let { appendLine("Caused by : ${it.javaClass.simpleName}: ${it.message}") } } } /** * Logs this exception in a clean, structured, and visually enhanced way. */ fun log(tag: String = TAG) { Log.e(tag, "❌ ${message ?: "AbortedException occurred"}") Log.e(tag, "• Operation: ${operation ?: "Unknown"}") Log.e(tag, "• Reason : ${reason ?: "Unspecified"}") Log.e(tag, "• Time : ${formatTimestamp(timestamp)}") this.cause?.let { Log.e(tag, "• Cause : ${it.javaClass.simpleName}: ${it.message}") Log.d(tag, getStackTraceAsString(it)) } } /** * Returns true if the abortion reason is due to user cancellation. */ fun isUserCancelled(): Boolean { return reason?.contains("cancel", ignoreCase = true) == true } /** * Suggests possible recovery actions based on the abortion context. */ fun getRecoverySuggestion(): String { return when { isUserCancelled() -> "User canceled the operation. No further action needed." reason?.contains("timeout", ignoreCase = true) == true -> "Try increasing the timeout duration and retry." reason?.contains("network", ignoreCase = true) == true -> "Please check your internet connection and retry." else -> "Check logs or contact support if this issue persists." } } /** * Converts the stack trace of the cause (if any) into a string for logging. */ private fun getStackTraceAsString(throwable: Throwable): String { val writer = StringWriter() throwable.printStackTrace(PrintWriter(writer)) return writer.toString() } /** * Converts the timestamp into a human-readable date/time format. */ private fun formatTimestamp(time: Long): String { val sdf = SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS", Locale.getDefault()) return sdf.format(Date(time)) } /** * Converts this exception into a map structure for analytics or crash reporting. */ fun toMap(): Map<String, Any?> { return mapOf( "operation" to operation, "reason" to reason, "timestamp" to timestamp, "message" to message, "isUserCancelled" to isUserCancelled(), "recoverySuggestion" to getRecoverySuggestion(), "cause" to this.cause?.javaClass?.simpleName ) } /** * Converts this exception to JSON for structured logging or remote reporting. * Uses JSONObject to ensure proper escaping of strings. */ fun toJson(pretty: Boolean = true): String { val obj = JSONObject() obj.put("operation", operation ?: JSONObject.NULL) obj.put("reason", reason ?: JSONObject.NULL) obj.put("timestamp", timestamp) obj.put("message", message ?: JSONObject.NULL) obj.put("isUserCancelled", isUserCancelled()) obj.put("recoverySuggestion", getRecoverySuggestion()) obj.put("cause", this.cause?.javaClass?.simpleName ?: JSONObject.NULL) return if (pretty) obj.toString(2) else obj.toString() } override fun toString(): String { return "AbortedException(operation=$operation, reason=$reason, timestamp=$timestamp, message=$message)" } // ======================================================= // 🎨 UI Helpers — these make presenting the exception more pleasant // (lightweight helpers that don't force UI dependencies) // ======================================================= /** * Returns a styled [SpannableString] suitable for showing in a TextView. * Title (operation) will be bold and primary colored; body will be secondary colored; time will be dim + italic. * * Example usage: * textView.text = abortedException.toSpannableMessage(primaryColor, secondaryColor) */ fun toSpannableMessage(primaryColor: Int, secondaryColor: Int): SpannableString { val title = operation ?: "Operation aborted" val body = reason ?: (message ?: "The operation was aborted.") val time = formatTimestamp(timestamp) val full = "$title\n$body\n$time" val spannable = SpannableString(full) // title bold + primary color spannable.setSpan(StyleSpan(android.graphics.Typeface.BOLD), 0, title.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) spannable.setSpan(ForegroundColorSpan(primaryColor), 0, title.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) // body colored secondary val bodyStart = title.length + 1 val bodyEnd = bodyStart + body.length if (bodyStart < bodyEnd) { spannable.setSpan(ForegroundColorSpan(secondaryColor), bodyStart, bodyEnd, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) } // time dim + italic val timeStart = bodyEnd + 1 val timeEnd = full.length if (timeStart < timeEnd) { spannable.setSpan(ForegroundColorSpan(adjustAlpha(secondaryColor, 0.7f)), timeStart, timeEnd, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) spannable.setSpan(StyleSpan(android.graphics.Typeface.ITALIC), timeStart, timeEnd, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) } return spannable } private fun adjustAlpha(@androidx.annotation.ColorInt color: Int, factor: Float): Int { val a = (android.graphics.Color.alpha(color) * factor).toInt() val r = android.graphics.Color.red(color) val g = android.graphics.Color.green(color) val b = android.graphics.Color.blue(color) return android.graphics.Color.argb(a, r, g, b) } /** * Shows a short Toast to the user with a friendly message extracted from this exception. * Safe to call from any thread (it will post to main Looper). */ fun showAsToast(context: Context, duration: Int = Toast.LENGTH_SHORT) { val toastText = when { isUserCancelled() -> "Action cancelled." reason != null -> reason message != null -> message else -> "Operation aborted." } ?: "Operation aborted." if (Looper.myLooper() == Looper.getMainLooper()) { Toast.makeText(context.applicationContext, toastText, duration).show() } else { Handler(Looper.getMainLooper()).post { Toast.makeText(context.applicationContext, toastText, duration).show() } } } }
package ir.cafebazaar.poolakey.exception
import android.app.AlertDialog
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.graphics.Typeface
import android.net.Uri
import android.os.Handler
import android.os.Looper
import android.text.SpannableString
import android.text.Spanned
import android.text.style.ForegroundColorSpan
import android.text.style.StyleSpan
import android.util.Log
import android.widget.Toast
import androidx.core.content.ContextCompat
import org.json.JSONObject
import java.text.SimpleDateFormat
import java.util.*
/**
* Thrown when the Bazaar app (Cafebazaar) is not found on the user's device.
* This typically occurs when trying to use in-app billing APIs or intents that require Bazaar.
*/
class BazaarNotFoundException(
val packageName: String = "com.farsitel.bazaar",
val timestamp: Long = System.currentTimeMillis(),
val recoveryHint: String? = "Please install or update Bazaar to continue."
) : IllegalStateException() {
override val message: String?
get() = "Bazaar is not installed"
companion object {
private const val TAG = "BazaarNotFoundException"
/**
* Creates a default instance when Bazaar is missing.
*/
fun create(): BazaarNotFoundException = BazaarNotFoundException()
/**
* Returns the official Bazaar install URI.
*/
fun getInstallUri(): Uri = Uri.parse("bazaar://details?id=com.farsitel.bazaar")
/**
* Returns a web fallback URL for Bazaar download.
*/
fun getWebInstallUri(): Uri =
Uri.parse("https://cafebazaar.ir/app/com.farsitel.bazaar?l=en")
/**
* Checks whether Bazaar is installed on the device.
*/
fun isBazaarInstalled(context: Context): Boolean {
return try {
context.packageManager.getPackageInfo("com.farsitel.bazaar", 0)
true
} catch (e: Exception) {
false
}
}
}
// ===============================================================
// 📋 Helper Methods
// ===============================================================
fun describe(): String {
val date = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()).format(Date(timestamp))
return buildString {
appendLine("⚠️ BazaarNotFoundException")
appendLine("Time : $date")
appendLine("Message : ${message ?: "Unknown error"}")
appendLine("Package : $packageName")
appendLine("Hint : ${recoveryHint ?: "Install Bazaar manually"}")
}
}
fun log(tag: String = TAG) {
Log.e(tag, "❌ BazaarNotFoundException: ${message}")
Log.e(tag, "• Package: $packageName")
Log.e(tag, "• Time : ${SimpleDateFormat("HH:mm:ss", Locale.getDefault()).format(Date(timestamp))}")
Log.e(tag, "• Hint : ${recoveryHint ?: "Install Bazaar manually"}")
}
fun toJson(pretty: Boolean = true): String {
val obj = JSONObject()
obj.put("error", "BazaarNotFoundException")
obj.put("message", message)
obj.put("timestamp", timestamp)
obj.put("package", packageName)
obj.put("recoveryHint", recoveryHint)
return if (pretty) obj.toString(2) else obj.toString()
}
fun getRecoverySuggestion(): String =
recoveryHint ?: "Please install or update Bazaar and try again."
// ===============================================================
// 🎨 User Interface Helpers
// ===============================================================
fun toSpannableMessage(primaryColor: Int, secondaryColor: Int): SpannableString {
val title = "⚠️ Bazaar Not Installed"
val body = recoveryHint ?: "Please install Bazaar to continue."
val fullText = "$title\n\n$body"
val spannable = SpannableString(fullText)
spannable.setSpan(StyleSpan(Typeface.BOLD), 0, title.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
spannable.setSpan(ForegroundColorSpan(primaryColor), 0, title.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
val bodyStart = title.length + 2
if (bodyStart < fullText.length) {
spannable.setSpan(ForegroundColorSpan(secondaryColor), bodyStart, fullText.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
}
return spannable
}
fun showAsToast(context: Context, duration: Int = Toast.LENGTH_LONG) {
val messageToShow = "⚠️ " + getRecoverySuggestion()
if (Looper.myLooper() == Looper.getMainLooper()) {
Toast.makeText(context.applicationContext, messageToShow, duration).show()
} else {
Handler(Looper.getMainLooper()).post {
Toast.makeText(context.applicationContext, messageToShow, duration).show()
}
}
}
/**
* A beautiful and polished dialog prompting the user to install Bazaar.
*/
fun showBeautifulDialog(
context: Context,
accentColor: Int = ContextCompat.getColor(context, android.R.color.holo_green_dark),
onInstall: (() -> Unit)? = null,
onCancel: (() -> Unit)? = null
) {
if (Looper.myLooper() != Looper.getMainLooper()) {
Handler(Looper.getMainLooper()).post {
showBeautifulDialog(context, accentColor, onInstall, onCancel)
}
return
}
try {
val dialogBuilder = AlertDialog.Builder(context)
val spannableMsg = toSpannableMessage(accentColor, ContextCompat.getColor(context, android.R.color.darker_gray))
dialogBuilder.setTitle("✨ Bazaar Required ✨")
.setMessage(spannableMsg)
.setIcon(android.R.drawable.ic_dialog_info)
.setCancelable(false)
.setPositiveButton("🚀 Install Bazaar") { dialog, _ ->
openBazaarInstallPage(context)
onInstall?.invoke()
dialog.dismiss()
}
.setNegativeButton("❌ Cancel") { dialog, _ ->
onCancel?.invoke()
dialog.dismiss()
}
dialogBuilder.create().show()
} catch (e: Exception) {
Log.w(TAG, "Dialog failed: ${e.message}")
showAsToast(context)
}
}
// ===============================================================
// 🧠 Behavior & Recovery Functions
// ===============================================================
fun attemptRecovery(context: Context): Boolean {
return if (!isBazaarInstalled(context)) {
openBazaarInstallPage(context)
true
} else false
}
fun getLocalizedRecoveryHint(locale: Locale = Locale.getDefault()): String {
return when (locale.language.lowercase(Locale.ROOT)) {
"fa" -> "لطفاً برنامه بازار را نصب یا بهروزرسانی کنید تا بتوانید ادامه دهید."
else -> recoveryHint ?: "Please install or update Bazaar to continue."
}
}
fun runIfBazaarAvailable(
context: Context,
onAvailable: () -> Unit,
onMissing: (() -> Unit)? = null
) {
if (isBazaarInstalled(context)) onAvailable()
else onMissing?.invoke() ?: showBeautifulDialog(context)
}
fun openBazaarInstallPage(context: Context) {
val pm: PackageManager = context.packageManager ?: return
val intent = Intent(Intent.ACTION_VIEW, getInstallUri()).apply {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
val resolved = pm.queryIntentActivities(intent, 0)
if (resolved?.isNotEmpty() == true) {
try {
context.startActivity(intent)
return
} catch (e: Exception) {
Log.w(TAG, "Failed to open Bazaar install page: ${e.message}")
}
}
try {
val webIntent = Intent(Intent.ACTION_VIEW, getWebInstallUri()).apply {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
context.startActivity(webIntent)
} catch (e: Exception) {
Log.e(TAG, "Could not launch Bazaar install fallback: ${e.message}")
}
}
fun showInstallToastIfMissing(context: Context, duration: Int = Toast.LENGTH_LONG) {
if (!isBazaarInstalled(context)) showAsToast(context, duration)
}
fun toMap(): Map<String, Any?> = mapOf(
"type" to "BazaarNotFoundException",
"message" to message,
"package" to packageName,
"timestamp" to timestamp,
"hint" to recoveryHint
)
fun toDebugString(): String {
val time = SimpleDateFormat("HH:mm:ss", Locale.getDefault()).format(Date(timestamp))
return "[BazaarMissing] ($time) ${message ?: "No message"}"
}
override fun toString(): String {
return "BazaarNotFoundException(packageName=$packageName, message=$message, timestamp=$timestamp)"
}
}
package ir.cafebazaar.poolakey.exception
import android.app.AlertDialog
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.graphics.Typeface
import android.net.Uri
import android.os.Handler
import android.os.Looper
import android.text.SpannableString
import android.text.Spanned
import android.text.style.ForegroundColorSpan
import android.text.style.StyleSpan
import android.util.Log
import android.widget.Toast
import androidx.core.content.ContextCompat
import org.json.JSONObject
import java.text.SimpleDateFormat
import java.util.*
/**
* Thrown when the Bazaar app (Cafebazaar) is not found on the user's device.
* This typically occurs when trying to use in-app billing APIs or intents that require Bazaar.
*/
class BazaarNotFoundException(
val packageName: String = "com.farsitel.bazaar",
val timestamp: Long = System.currentTimeMillis(),
val recoveryHint: String? = "Please install or update Bazaar to continue."
) : IllegalStateException() {
override val message: String?
get() = "Bazaar is not installed"
companion object {
private const val TAG = "BazaarNotFoundException"
/**
* Creates a default instance when Bazaar is missing.
*/
fun create(): BazaarNotFoundException = BazaarNotFoundException()
/**
* Returns the official Bazaar install URI.
*/
fun getInstallUri(): Uri = Uri.parse("bazaar://details?id=com.farsitel.bazaar")
/**
* Returns a web fallback URL for Bazaar download.
*/
fun getWebInstallUri(): Uri =
Uri.parse("https://cafebazaar.ir/app/com.farsitel.bazaar?l=en")
/**
* Checks whether Bazaar is installed on the device.
*/
fun isBazaarInstalled(context: Context): Boolean {
return try {
context.packageManager.getPackageInfo("com.farsitel.bazaar", 0)
true
} catch (e: Exception) {
false
}
}
}
// ===============================================================
// 📋 Helper Methods
// ===============================================================
fun describe(): String {
val date = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()).format(Date(timestamp))
return buildString {
appendLine("⚠️ BazaarNotFoundException")
appendLine("Time : $date")
appendLine("Message : ${message ?: "Unknown error"}")
appendLine("Package : $packageName")
appendLine("Hint : ${recoveryHint ?: "Install Bazaar manually"}")
}
}
fun log(tag: String = TAG) {
Log.e(tag, "❌ BazaarNotFoundException: ${message}")
Log.e(tag, "• Package: $packageName")
Log.e(tag, "• Time : ${SimpleDateFormat("HH:mm:ss", Locale.getDefault()).format(Date(timestamp))}")
Log.e(tag, "• Hint : ${recoveryHint ?: "Install Bazaar manually"}")
}
fun toJson(pretty: Boolean = true): String {
val obj = JSONObject()
obj.put("error", "BazaarNotFoundException")
obj.put("message", message)
obj.put("timestamp", timestamp)
obj.put("package", packageName)
obj.put("recoveryHint", recoveryHint)
return if (pretty) obj.toString(2) else obj.toString()
}
fun getRecoverySuggestion(): String =
recoveryHint ?: "Please install or update Bazaar and try again."
// ===============================================================
// 🎨 User Interface Helpers
// ===============================================================
fun toSpannableMessage(primaryColor: Int, secondaryColor: Int): SpannableString {
val title = "⚠️ Bazaar Not Installed"
val body = recoveryHint ?: "Please install Bazaar to continue."
val fullText = "$title\n\n$body"
val spannable = SpannableString(fullText)
spannable.setSpan(StyleSpan(Typeface.BOLD), 0, title.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
spannable.setSpan(ForegroundColorSpan(primaryColor), 0, title.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
val bodyStart = title.length + 2
if (bodyStart < fullText.length) {
spannable.setSpan(ForegroundColorSpan(secondaryColor), bodyStart, fullText.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
}
return spannable
}
fun showAsToast(context: Context, duration: Int = Toast.LENGTH_LONG) {
val messageToShow = "⚠️ " + getRecoverySuggestion()
if (Looper.myLooper() == Looper.getMainLooper()) {
Toast.makeText(context.applicationContext, messageToShow, duration).show()
} else {
Handler(Looper.getMainLooper()).post {
Toast.makeText(context.applicationContext, messageToShow, duration).show()
}
}
}
/**
* A beautiful and polished dialog prompting the user to install Bazaar.
*/
fun showBeautifulDialog(
context: Context,
accentColor: Int = ContextCompat.getColor(context, android.R.color.holo_green_dark),
onInstall: (() -> Unit)? = null,
onCancel: (() -> Unit)? = null
) {
if (Looper.myLooper() != Looper.getMainLooper()) {
Handler(Looper.getMainLooper()).post {
showBeautifulDialog(context, accentColor, onInstall, onCancel)
}
return
}
try {
val dialogBuilder = AlertDialog.Builder(context)
val spannableMsg = toSpannableMessage(accentColor, ContextCompat.getColor(context, android.R.color.darker_gray))
dialogBuilder.setTitle("✨ Bazaar Required ✨")
.setMessage(spannableMsg)
.setIcon(android.R.drawable.ic_dialog_info)
.setCancelable(false)
.setPositiveButton("🚀 Install Bazaar") { dialog, _ ->
openBazaarInstallPage(context)
onInstall?.invoke()
dialog.dismiss()
}
.setNegativeButton("❌ Cancel") { dialog, _ ->
onCancel?.invoke()
dialog.dismiss()
}
dialogBuilder.create().show()
} catch (e: Exception) {
Log.w(TAG, "Dialog failed: ${e.message}")
showAsToast(context)
}
}
// ===============================================================
// 🧠 Behavior & Recovery Functions
// ===============================================================
fun attemptRecovery(context: Context): Boolean {
return if (!isBazaarInstalled(context)) {
openBazaarInstallPage(context)
true
} else false
}
fun getLocalizedRecoveryHint(locale: Locale = Locale.getDefault()): String {
return when (locale.language.lowercase(Locale.ROOT)) {
"fa" -> "لطفاً برنامه بازار را نصب یا بهروزرسانی کنید تا بتوانید ادامه دهید."
else -> recoveryHint ?: "Please install or update Bazaar to continue."
}
}
fun runIfBazaarAvailable(
context: Context,
onAvailable: () -> Unit,
onMissing: (() -> Unit)? = null
) {
if (isBazaarInstalled(context)) onAvailable()
else onMissing?.invoke() ?: showBeautifulDialog(context)
}
fun openBazaarInstallPage(context: Context) {
val pm: PackageManager = context.packageManager ?: return
val intent = Intent(Intent.ACTION_VIEW, getInstallUri()).apply {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
val resolved = pm.queryIntentActivities(intent, 0)
if (resolved?.isNotEmpty() == true) {
try {
context.startActivity(intent)
return
} catch (e: Exception) {
Log.w(TAG, "Failed to open Bazaar install page: ${e.message}")
}
}
try {
val webIntent = Intent(Intent.ACTION_VIEW, getWebInstallUri()).apply {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
context.startActivity(webIntent)
} catch (e: Exception) {
Log.e(TAG, "Could not launch Bazaar install fallback: ${e.message}")
}
}
fun showInstallToastIfMissing(context: Context, duration: Int = Toast.LENGTH_LONG) {
if (!isBazaarInstalled(context)) showAsToast(context, duration)
}
fun toMap(): Map<String, Any?> = mapOf(
"type" to "BazaarNotFoundException",
"message" to message,
"package" to packageName,
"timestamp" to timestamp,
"hint" to recoveryHint
)
fun toDebugString(): String {
val time = SimpleDateFormat("HH:mm:ss", Locale.getDefault()).format(Date(timestamp))
return "[BazaarMissing] ($time) ${message ?: "No message"}"
}
override fun toString(): String {
return "BazaarNotFoundException(packageName=$packageName, message=$message, timestamp=$timestamp)"
}
}
package ir.cafebazaar.poolakey.exception
import android.app.AlertDialog
import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.graphics.Typeface
import android.net.ConnectivityManager
import android.net.NetworkCapabilities
import android.net.Uri
import android.os.Build
import android.os.Handler
import android.os.Looper
import android.util.Log
import android.widget.LinearLayout
import android.widget.TextView
import android.widget.Toast
import androidx.core.content.ContextCompat
import org.json.JSONObject
import java.text.SimpleDateFormat
import java.util.*
/**
* Exception thrown when Bazaar app is installed but not updated
* to a version that supports the required billing or service features.
*/
class BazaarNotSupportedException(
val packageName: String = "com.farsitel.bazaar",
val requiredVersion: Int? = null,
val currentVersion: Int? = null,
val timestamp: Long = System.currentTimeMillis(),
val recoveryHint: String? = "Please update Bazaar to the latest version to continue."
) : IllegalStateException() {
override val message: String?
get() = "Bazaar is not updated"
companion object {
private const val TAG = "BazaarNotSupportedException"
private const val BAZAAR_PACKAGE = "com.farsitel.bazaar"
/** Bazaar in-app URI */
fun getUpdateUri(): Uri = Uri.parse("bazaar://details?id=$BAZAAR_PACKAGE")
/** Web fallback URI */
fun getWebUpdateUri(): Uri =
Uri.parse("https://cafebazaar.ir/app/$BAZAAR_PACKAGE?l=en")
/** Check if Bazaar is installed */
fun isBazaarInstalled(context: Context): Boolean {
return try {
context.packageManager.getPackageInfo(BAZAAR_PACKAGE, 0)
true
} catch (_: PackageManager.NameNotFoundException) {
false
} catch (_: Exception) {
false
}
}
/** Get installed Bazaar version code (supports all API levels) */
fun getInstalledVersionCode(context: Context): Long? {
return try {
val info = context.packageManager.getPackageInfo(BAZAAR_PACKAGE, 0)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) info.longVersionCode
else @Suppress("DEPRECATION") info.versionCode.toLong()
} catch (_: Exception) {
null
}
}
/** Check if Bazaar supports the required version */
fun isVersionSupported(context: Context, requiredVersion: Int): Boolean {
val current = getInstalledVersionCode(context) ?: return false
return current >= requiredVersion
}
}
// ===============================================================
// 📋 Core Diagnostic & Info Utilities
// ===============================================================
fun describe(): String {
val date = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()).format(Date(timestamp))
return """
⚠️ BazaarNotSupportedException
──────────────────────────────
Time : $date
Message : ${message ?: "Unknown"}
Package : $packageName
Required Ver: ${requiredVersion ?: "Unknown"}
Current Ver : ${currentVersion ?: "Unknown"}
Hint : ${recoveryHint ?: "Please update Bazaar manually"}
""".trimIndent()
}
fun log(tag: String = TAG) {
Log.e(tag, "❌ BazaarNotSupportedException: $message")
Log.e(tag, "• Package: $packageName")
Log.e(tag, "• Required version: ${requiredVersion ?: "?"}")
Log.e(tag, "• Current version : ${currentVersion ?: "?"}")
Log.e(tag, "• Hint : $recoveryHint")
}
fun toJson(pretty: Boolean = true): String {
val obj = JSONObject().apply {
put("error", "BazaarNotSupportedException")
put("message", message)
put("timestamp", timestamp)
put("package", packageName)
put("requiredVersion", requiredVersion)
put("currentVersion", currentVersion)
put("recoveryHint", recoveryHint)
}
return if (pretty) obj.toString(2) else obj.toString()
}
fun getRecoverySuggestion(): String =
recoveryHint ?: "Please update Bazaar to the latest version."
// ===============================================================
// 🎨 Modern, Beautiful UI Helpers
// ===============================================================
fun showAsToast(context: Context, duration: Int = Toast.LENGTH_LONG) {
val msg = getRecoverySuggestion()
val runnable = Runnable {
Toast.makeText(context.applicationContext, msg, duration).show()
}
if (Looper.myLooper() == Looper.getMainLooper()) runnable.run()
else Handler(Looper.getMainLooper()).post(runnable)
}
/**
* A stylish, user-friendly update dialog with better typography and color scheme.
*/
fun showUpdateDialog(
context: Context,
onUpdate: (() -> Unit)? = null,
onCancel: (() -> Unit)? = null
) {
val showDialog = {
try {
val titleView = TextView(context).apply {
text = "⚠️ Bazaar Update Required"
textSize = 20f
setTypeface(null, Typeface.BOLD)
setTextColor(ContextCompat.getColor(context, android.R.color.holo_orange_dark))
setPadding(50, 40, 50, 20)
}
val messageView = TextView(context).apply {
text = """
Your version of Bazaar is outdated.
Please update to continue using all features.
${getRecoverySuggestion()}
""".trimIndent()
textSize = 16f
setTextColor(ContextCompat.getColor(context, android.R.color.secondary_text_dark))
setPadding(50, 0, 50, 20)
}
val layout = LinearLayout(context).apply {
orientation = LinearLayout.VERTICAL
addView(titleView)
addView(messageView)
}
val builder = AlertDialog.Builder(context)
.setView(layout)
.setCancelable(false)
.setPositiveButton("Update Now 🚀") { dialog, _ ->
openBazaarUpdatePage(context)
onUpdate?.invoke()
dialog.dismiss()
}
.setNegativeButton("Cancel ❌") { dialog, _ ->
onCancel?.invoke()
dialog.dismiss()
}
val dialog = builder.create()
dialog.setOnShowListener {
dialog.getButton(AlertDialog.BUTTON_POSITIVE)?.setTextColor(
ContextCompat.getColor(context, android.R.color.holo_green_light)
)
dialog.getButton(AlertDialog.BUTTON_NEGATIVE)?.setTextColor(
ContextCompat.getColor(context, android.R.color.holo_red_light)
)
}
dialog.show()
} catch (e: Exception) {
Log.w(TAG, "Dialog failed: ${e.message}")
showAsToast(context)
}
}
if (Looper.myLooper() == Looper.getMainLooper()) showDialog()
else Handler(Looper.getMainLooper()).post(showDialog)
}
fun openBazaarUpdatePage(context: Context) {
try {
val intent = Intent(Intent.ACTION_VIEW, getUpdateUri()).apply {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
context.startActivity(intent)
} catch (_: ActivityNotFoundException) {
// Fallback to web
try {
val webIntent = Intent(Intent.ACTION_VIEW, getWebUpdateUri()).apply {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
context.startActivity(webIntent)
} catch (e: Exception) {
Log.e(TAG, "Failed to open web update page: ${e.message}")
showAsToast(context)
}
}
}
// ===============================================================
// 🧩 Smart Logic & Helper Tools
// ===============================================================
fun hasInternetConnection(context: Context): Boolean {
val cm = context.getSystemService(Context.CONNECTIVITY_SERVICE) as? ConnectivityManager ?: return false
val network = cm.activeNetwork ?: return false
val caps = cm.getNetworkCapabilities(network)
return caps?.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) == true
}
fun needsUpdate(): Boolean =
requiredVersion != null && currentVersion != null && currentVersion < requiredVersion
fun elapsedSinceThrown(): Long =
(System.currentTimeMillis() - timestamp) / 1000
fun getReadableElapsedTime(): String {
val seconds = elapsedSinceThrown()
val minutes = seconds / 60
val remaining = seconds % 60
return if (minutes > 0) "${minutes}m ${remaining}s ago" else "${remaining}s ago"
}
fun getLocalizedRecoveryHint(locale: Locale = Locale.getDefault()): String =
when (locale.language.lowercase(Locale.getDefault())) {
"fa" -> "لطفاً بازار را بهروزرسانی کنید تا بتوانید ادامه دهید."
else -> recoveryHint ?: "Please update Bazaar to continue."
}
fun smartRecoveryStrategy(context: Context): String = when {
!isBazaarInstalled(context) -> "Bazaar not installed. Please install it first."
!hasInternetConnection(context) -> "No internet connection. Please connect and try again."
needsUpdate() -> "Your Bazaar version is outdated. Please update now."
else -> "Unknown issue. Try restarting your device or reinstalling Bazaar."
}
fun toAnalyticsBundle(): Map<String, String> = mapOf(
"exception" to "BazaarNotSupportedException",
"package" to packageName,
"required_version" to (requiredVersion?.toString() ?: "unknown"),
"current_version" to (currentVersion?.toString() ?: "unknown"),
"elapsed_seconds" to elapsedSinceThrown().toString(),
"device" to "${Build.MANUFACTURER} ${Build.MODEL}",
"android_version" to Build.VERSION.RELEASE
)
fun compactSummary(): String =
"[$packageName] Bazaar outdated (req=$requiredVersion, cur=$currentVersion)"
fun getErrorId(): String = UUID.randomUUID().toString().substring(0, 8)
fun diagnosticReport(): String = buildString {
appendLine(toDebugString())
appendLine(describe())
appendLine("Elapsed: ${getReadableElapsedTime()}")
}
fun toDebugString(): String {
val time = SimpleDateFormat("HH:mm:ss", Locale.getDefault()).format(Date(timestamp))
return "[BazaarNotSupported] ($time) ${message ?: "No message"}"
}
override fun toString(): String =
"BazaarNotSupportedException(packageName=$packageName, requiredVersion=$requiredVersion, currentVersion=$currentVersion, message=$message, timestamp=$timestamp)"
}
package ir.cafebazaar.poolakey.exception
import android.app.AlertDialog
import android.content.Context
import android.os.Handler
import android.os.Looper
import android.os.RemoteException
import android.os.SystemClock
import android.util.Log
import android.widget.Toast
import androidx.core.content.ContextCompat
import kotlinx.coroutines.delay
import org.json.JSONObject
import java.text.SimpleDateFormat
import java.util.*
import kotlin.math.pow
/**
* Thrown when a "consume" request to Bazaar fails,
* typically due to a network, billing, or remote service issue.
*/
class ConsumeFailedException(
val productId: String? = null,
val purchaseToken: String? = null,
val reason: String? = null,
val timestamp: Long = System.currentTimeMillis()
) : RemoteException() {
override val message: String?
get() = reason ?: "Consume request failed: It's from Bazaar"
companion object {
private const val TAG = "ConsumeFailedException"
/**
* Creates a preconfigured exception for a known common cause.
*/
fun fromNetworkError(): ConsumeFailedException {
return ConsumeFailedException(reason = "Network connection error")
}
fun fromInvalidToken(token: String): ConsumeFailedException {
return ConsumeFailedException(
purchaseToken = token,
reason = "Invalid or expired purchase token"
)
}
fun fromTimeout(): ConsumeFailedException {
return ConsumeFailedException(reason = "Consume request timed out")
}
fun fromUnknownError(): ConsumeFailedException {
return ConsumeFailedException(reason = "Unknown internal error")
}
}
// ===============================================================
// 📋 Diagnostic & Logging Tools
// ===============================================================
fun describe(): String {
val date = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault())
.format(Date(timestamp))
return buildString {
appendLine("⚠️ ConsumeFailedException")
appendLine("Time : $date")
appendLine("Product ID : ${productId ?: "Unknown"}")
appendLine("Token : ${purchaseToken ?: "Unknown"}")
appendLine("Reason : ${reason ?: "Unknown failure"}")
appendLine("Message : ${message}")
}
}
fun log(tag: String = TAG, detailed: Boolean = false) {
Log.e(tag, "❌ ConsumeFailedException: ${message}")
if (detailed) {
Log.e(tag, describe())
} else {
if (productId != null) Log.e(tag, "• Product ID: $productId")
if (purchaseToken != null) Log.e(tag, "• Token: $purchaseToken")
Log.e(tag, "• Timestamp: $timestamp")
}
}
fun toJson(pretty: Boolean = true): String {
val obj = JSONObject().apply {
put("error", "ConsumeFailedException")
put("message", message)
put("productId", productId)
put("purchaseToken", purchaseToken)
put("reason", reason)
put("timestamp", timestamp)
put("retryCount", retryCount)
put("elapsedSeconds", timeSinceFirstFailure())
}
return if (pretty) obj.toString(2) else obj.toString()
}
fun toMap(): Map<String, Any?> = mapOf(
"type" to "ConsumeFailedException",
"message" to message,
"productId" to productId,
"purchaseToken" to purchaseToken,
"reason" to reason,
"timestamp" to timestamp,
"retryCount" to retryCount,
"elapsedSeconds" to timeSinceFirstFailure()
)
// ===============================================================
// 🎨 User Feedback Helpers
// ===============================================================
/**
* Safely show a toast on main thread.
*/
fun showAsToast(context: Context, duration: Int = Toast.LENGTH_LONG) {
val msg = message ?: "Consume operation failed."
runOnMain {
Toast.makeText(context.applicationContext, msg, duration).show()
}
}
fun getReadableError(): String {
return when {
reason?.contains("network", true) == true ->
"Network issue detected. Please check your connection."
reason?.contains("timeout", true) == true ->
"The operation took too long. Please try again."
reason?.contains("token", true) == true ->
"Purchase token invalid. Please refresh your purchase list."
else -> "Unable to complete purchase consumption. Try again later."
}
}
fun showFriendlyToast(context: Context, duration: Int = Toast.LENGTH_LONG) {
runOnMain {
Toast.makeText(context.applicationContext, getReadableError(), duration).show()
}
}
/**
* Shows a retry dialog (main thread). Buttons styled using system colors.
* - onRetry: invoked when user chooses to retry
* - onCancel: invoked on cancel
*/
fun showRetryDialog(
context: Context,
title: String = "Consume Failed",
messageText: String = getReadableError(),
positiveLabel: String = "Retry",
negativeLabel: String = "Cancel",
onRetry: (() -> Unit)? = null,
onCancel: (() -> Unit)? = null
) {
runOnMain {
try {
val builder = AlertDialog.Builder(context)
.setTitle(title)
.setMessage(messageText)
.setCancelable(true)
.setPositiveButton(positiveLabel) { dialog, _ ->
onRetry?.invoke()
dialog.dismiss()
}
.setNegativeButton(negativeLabel) { dialog, _ ->
onCancel?.invoke()
dialog.dismiss()
}
val dialog = builder.create()
dialog.setOnShowListener {
dialog.getButton(AlertDialog.BUTTON_POSITIVE)?.let {
try {
it.setTextColor(ContextCompat.getColor(context, android.R.color.holo_green_dark))
} catch (_: Exception) { /* ignore */ }
}
dialog.getButton(AlertDialog.BUTTON_NEGATIVE)?.let {
try {
it.setTextColor(ContextCompat.getColor(context, android.R.color.holo_red_dark))
} catch (_: Exception) { /* ignore */ }
}
}
dialog.show()
} catch (e: Exception) {
Log.w(TAG, "Dialog failed: ${e.message}")
showFriendlyToast(context)
}
}
}
// ===============================================================
// ⏱ Timing & Retry Utilities
// ===============================================================
// protected by 'synchronized' on methods that mutate them
private var retryCount: Int = 0
private var firstFailureTime: Long = SystemClock.elapsedRealtime()
@synchronized
fun incrementRetry() {
retryCount++
}
@synchronized
fun getRetryCount(): Int = retryCount
@synchronized
fun resetRetryTracking() {
retryCount = 0
firstFailureTime = SystemClock.elapsedRealtime()
}
fun timeSinceFirstFailure(): Long =
(SystemClock.elapsedRealtime() - firstFailureTime) / 1000
fun getReadableElapsedTime(): String {
val seconds = timeSinceFirstFailure()
val minutes = seconds / 60
val remaining = seconds % 60
return if (minutes > 0) "${minutes}m ${remaining}s" else "${remaining}s"
}
// ===============================================================
// 🩺 Recovery & Resilience Helpers
// ===============================================================
/**
* Suggests a recovery action based on the cause.
*/
fun getRecoverySuggestion(): String {
return when {
reason?.contains("network", true) == true ->
"Please ensure your device is connected to the internet."
reason?.contains("token", true) == true ->
"The purchase token may have expired. Requery or refresh the purchase list."
reason?.contains("timeout", true) == true ->
"Try increasing timeout or retrying later."
else -> "Try restarting the Bazaar app and retry the operation."
}
}
/**
* Returns `true` if this failure is likely temporary and worth retrying.
*/
fun isTemporary(): Boolean {
return reason?.contains("network", true) == true ||
reason?.contains("timeout", true) == true
}
/**
* Returns `true` if this failure is likely permanent (e.g., invalid token).
*/
fun isPermanent(): Boolean {
return reason?.contains("token", true) == true ||
reason?.contains("invalid", true) == true
}
/**
* Suggests whether a retry should be attempted.
*/
fun shouldRetry(maxRetries: Int = 3): Boolean {
return isTemporary() && getRetryCount() < maxRetries
}
// ===============================================================
// 📊 Analytics & Summary
// ===============================================================
fun toAnalyticsBundle(): Map<String, String> = mapOf(
"exception" to "ConsumeFailedException",
"product_id" to (productId ?: "unknown"),
"reason" to (reason ?: "unknown"),
"elapsed_seconds" to timeSinceFirstFailure().toString(),
"timestamp" to timestamp.toString(),
"retries" to getRetryCount().toString()
)
fun compactSummary(): String =
"ConsumeFailed(product=${productId ?: "?"}, reason=${reason ?: "unknown"}, retries=${getRetryCount()})"
fun diagnosticReport(): String = buildString {
appendLine("==== Consume Failure Diagnostic ====")
appendLine(describe())
appendLine("Retry Count : ${getRetryCount()}")
appendLine("Elapsed Time: ${getReadableElapsedTime()}")
appendLine("Temporary : ${isTemporary()}")
appendLine("Permanent : ${isPermanent()}")
appendLine("Suggestion : ${getRecoverySuggestion()}")
appendLine("====================================")
}
// ===============================================================
// 🧰 Utility
// ===============================================================
/**
* Converts this exception into a human-readable log block.
*/
fun prettyPrint(): String {
return """
|🚨 ConsumeFailedException
|Product ID : ${productId ?: "N/A"}
|Reason : ${reason ?: "Unknown"}
|Retries : ${getRetryCount()}
|Temporary : ${isTemporary()}
|Timestamp : $timestamp
|Message : $message
""".trimMargin()
}
/**
* Emits this exception as a standardized debug message.
*/
fun debugLog(tag: String = TAG) {
Log.w(tag, prettyPrint())
}
// ===============================================================
// 🔁 Async Retry Helper (suspend) with exponential backoff
// ===============================================================
/**
* Attempts the provided suspend [action] repeatedly with exponential backoff until it returns true
* or until [maxAttempts] is reached. Returns the success boolean and increments retry counter.
*
* Example usage:
* ```
* val success = consumeFailedException.retryWithBackoff({
* // attempt retry logic (suspend) -> Boolean
* }, maxAttempts = 4, baseDelayMs = 500L)
* ```
*/
suspend fun retryWithBackoff(
action: suspend () -> Boolean,
maxAttempts: Int = 4,
baseDelayMs: Long = 500L
): Boolean {
resetRetryTracking()
repeat(maxAttempts) { attempt ->
val ok = try {
action()
} catch (e: Exception) {
false
}
if (ok) {
return true
} else {
incrementRetry()
// exponential backoff (2^attempt * baseDelay)
val delayMs = baseDelayMs * (2.0.pow(attempt.toDouble())).toLong()
delay(delayMs)
}
}
return false
}
// ===============================================================
// Helper: post to main looper safely
// ===============================================================
private fun runOnMain(runnable: () -> Unit) {
if (Looper.myLooper() == Looper.getMainLooper()) runnable() else Handler(Looper.getMainLooper()).post { runnable() }
}
override fun toString(): String {
return "ConsumeFailedException(productId=$productId, purchaseToken=$purchaseToken, reason=$reason, message=$message, timestamp=$timestamp)"
}
}
package ir.cafebazaar.poolakey.exception
import android.app.AlertDialog
import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.net.ConnectivityManager
import android.net.NetworkCapabilities
import android.net.Uri
import android.os.Build
import android.os.Handler
import android.os.Looper
import android.os.RemoteException
import android.os.SystemClock
import android.util.Log
import android.widget.Toast
import androidx.core.content.ContextCompat
import org.json.JSONObject
import java.text.SimpleDateFormat
import java.util.*
/* ===============================================================
1️⃣ BazaarNotSupportedException
=============================================================== */
class BazaarNotSupportedException(
val packageName: String = "com.farsitel.bazaar",
val requiredVersion: Int? = null,
val currentVersion: Int? = null,
val timestamp: Long = System.currentTimeMillis(),
val recoveryHint: String? = "Please update Bazaar to the latest version to continue."
) : IllegalStateException() {
override val message: String?
get() = "Bazaar is not updated"
companion object {
private const val TAG = "BazaarNotSupportedException"
private const val BAZAAR_PACKAGE = "com.farsitel.bazaar"
fun getUpdateUri(): Uri = Uri.parse("bazaar://details?id=$BAZAAR_PACKAGE")
fun getWebUpdateUri(): Uri = Uri.parse("https://cafebazaar.ir/app/$BAZAAR_PACKAGE?l=en")
fun isBazaarInstalled(context: Context): Boolean =
try { context.packageManager.getPackageInfo(BAZAAR_PACKAGE, 0); true } catch (e: Exception) { false }
fun getInstalledVersionCode(context: Context): Long? =
try {
val info = context.packageManager.getPackageInfo(BAZAAR_PACKAGE, 0)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) info.longVersionCode else @Suppress("DEPRECATION") info.versionCode.toLong()
} catch (e: Exception) { null }
fun isVersionSupported(context: Context, requiredVersion: Int): Boolean {
val current = getInstalledVersionCode(context) ?: return false
return current >= requiredVersion
}
}
fun showAsToast(context: Context) {
runOnMain { Toast.makeText(context, "⚠️ ${getRecoveryHint()}", Toast.LENGTH_LONG).show() }
}
fun showUpdateDialog(context: Context, onUpdate: (() -> Unit)? = null, onCancel: (() -> Unit)? = null) {
val uiAction = {
try {
val builder = AlertDialog.Builder(context)
.setTitle("⚠️ Bazaar Update Required")
.setMessage("Your Bazaar app is outdated.\n\n${getRecoveryHint()}")
.setCancelable(false)
.setPositiveButton("Update Now 🚀") { dialog, _ ->
openBazaarUpdatePage(context); onUpdate?.invoke(); dialog.dismiss()
}
.setNegativeButton("Cancel ❌") { dialog, _ -> onCancel?.invoke(); dialog.dismiss() }
val dialog = builder.create()
dialog.setOnShowListener {
dialog.getButton(AlertDialog.BUTTON_POSITIVE)?.setTextColor(ContextCompat.getColor(context, android.R.color.holo_green_dark))
dialog.getButton(AlertDialog.BUTTON_NEGATIVE)?.setTextColor(ContextCompat.getColor(context, android.R.color.holo_red_dark))
}
dialog.show()
} catch (e: Exception) {
Log.w(TAG, "Dialog failed: ${e.message}")
showAsToast(context)
}
}
runOnMain(uiAction)
}
fun openBazaarUpdatePage(context: Context) {
try {
val intent = Intent(Intent.ACTION_VIEW, getUpdateUri()).apply { addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) }
context.startActivity(intent)
} catch (e: ActivityNotFoundException) {
try {
val webIntent = Intent(Intent.ACTION_VIEW, getWebUpdateUri()).apply { addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) }
context.startActivity(webIntent)
} catch (ex: Exception) {
Log.e(TAG, "Failed to open web update page: ${ex.message}")
showAsToast(context)
}
}
}
fun getRecoveryHint(): String = recoveryHint ?: "Please update Bazaar to continue."
fun runOnMain(runnable: () -> Unit) { if (Looper.myLooper() == Looper.getMainLooper()) runnable() else Handler(Looper.getMainLooper()).post(runnable) }
fun toJson(pretty: Boolean = true): String {
val obj = JSONObject().apply {
put("error", "BazaarNotSupportedException")
put("package", packageName)
put("requiredVersion", requiredVersion)
put("currentVersion", currentVersion)
put("timestamp", timestamp)
put("hint", recoveryHint)
}
return if (pretty) obj.toString(2) else obj.toString()
}
fun toMap(): Map<String, Any?> = mapOf(
"type" to "BazaarNotSupportedException",
"package" to packageName,
"requiredVersion" to requiredVersion,
"currentVersion" to currentVersion,
"timestamp" to timestamp,
"hint" to recoveryHint
)
}
/* ===============================================================
2️⃣ ConsumeFailedException
=============================================================== */
class ConsumeFailedException(
val productId: String? = null,
val purchaseToken: String? = null,
val reason: String? = null,
val timestamp: Long = System.currentTimeMillis()
) : RemoteException() {
override val message: String? get() = reason ?: "Consume request failed: It's from Bazaar"
companion object {
fun fromNetworkError() = ConsumeFailedException(reason = "Network connection error")
fun fromInvalidToken(token: String) = ConsumeFailedException(purchaseToken = token, reason = "Invalid or expired purchase token")
fun fromTimeout() = ConsumeFailedException(reason = "Consume request timed out")
fun fromUnknownError() = ConsumeFailedException(reason = "Unknown internal error")
}
private var retryCount: Int = 0
private var firstFailureTime: Long = SystemClock.elapsedRealtime()
fun incrementRetry() { retryCount++ }
fun getRetryCount(): Int = retryCount
fun resetRetryTracking() { retryCount = 0; firstFailureTime = SystemClock.elapsedRealtime() }
fun isTemporary(): Boolean = reason?.contains("network", true) == true || reason?.contains("timeout", true) == true
fun isPermanent(): Boolean = reason?.contains("token", true) == true || reason?.contains("invalid", true) == true
fun shouldRetry(maxRetries: Int = 3): Boolean = isTemporary() && retryCount < maxRetries
fun getRecoverySuggestion(): String = when {
reason?.contains("network", true) == true -> "Check your internet connection."
reason?.contains("token", true) == true -> "Purchase token may have expired."
reason?.contains("timeout", true) == true -> "Try increasing timeout or retrying later."
else -> "Restart Bazaar and retry the operation."
}
fun showFriendlyToast(context: Context) { runOnMain { Toast.makeText(context, "⚠️ ${getRecoverySuggestion()}", Toast.LENGTH_LONG).show() } }
private fun runOnMain(runnable: () -> Unit) { if (Looper.myLooper() == Looper.getMainLooper()) runnable() else Handler(Looper.getMainLooper()).post(runnable) }
fun toJson(pretty: Boolean = true): String {
val obj = JSONObject().apply {
put("error", "ConsumeFailedException")
put("productId", productId)
put("purchaseToken", purchaseToken)
put("reason", reason)
put("timestamp", timestamp)
put("retryCount", retryCount)
put("elapsedSeconds", (SystemClock.elapsedRealtime() - firstFailureTime)/1000)
}
return if (pretty) obj.toString(2) else obj.toString()
}
fun toMap(): Map<String, Any?> = mapOf(
"type" to "ConsumeFailedException",
"productId" to productId,
"purchaseToken" to purchaseToken,
"reason" to reason,
"timestamp" to timestamp,
"retryCount" to retryCount
)
}
/* ===============================================================
3️⃣ DisconnectException
=============================================================== */
class DisconnectException : IllegalStateException() {
override val message: String? get() = "We can't communicate with Bazaar: Service is disconnected"
fun showAsToast(context: Context) { Toast.makeText(context, "⚠️ $message", Toast.LENGTH_LONG).show() }
fun toJson(pretty: Boolean = true): String {
val obj = JSONObject().apply { put("error", "DisconnectException"); put("message", message) }
return if (pretty) obj.toString(2) else obj.toString()
}
fun toMap(): Map<String, Any?> = mapOf("type" to "DisconnectException", "message" to message)
}
package ir.cafebazaar.poolakey.exception
import android.app.AlertDialog
import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
import android.net.ConnectivityManager
import android.net.NetworkCapabilities
import android.os.Build
import android.os.Handler
import android.os.Looper
import android.os.RemoteException
import android.os.SystemClock
import android.widget.Toast
import androidx.core.content.ContextCompat
import org.json.JSONObject
import java.text.SimpleDateFormat
import java.util.*
/* ===============================================================
1️⃣ BazaarNotSupportedException
=============================================================== */
class BazaarNotSupportedException(
val packageName: String = "com.farsitel.bazaar",
val requiredVersion: Int? = null,
val currentVersion: Int? = null,
val timestamp: Long = System.currentTimeMillis(),
val recoveryHint: String? = "Please update Bazaar to the latest version to continue."
) : IllegalStateException() {
override val message: String
get() = "Bazaar is not updated"
companion object {
private const val BAZAAR_PACKAGE = "com.farsitel.bazaar"
fun getUpdateUri(): android.net.Uri = android.net.Uri.parse("bazaar://details?id=$BAZAAR_PACKAGE")
fun getWebUpdateUri(): android.net.Uri = android.net.Uri.parse("https://cafebazaar.ir/app/$BAZAAR_PACKAGE?l=en")
}
fun needsUpdate(): Boolean = requiredVersion != null && currentVersion != null && currentVersion < requiredVersion
fun elapsedSinceThrown(): Long = (System.currentTimeMillis() - timestamp) / 1000
fun getReadableElapsedTime(): String {
val seconds = elapsedSinceThrown()
val minutes = seconds / 60
val remaining = seconds % 60
return if (minutes > 0) "${minutes}m ${remaining}s ago" else "${remaining}s ago"
}
fun hasInternetConnection(context: Context): Boolean {
val cm = context.getSystemService(Context.CONNECTIVITY_SERVICE) as? ConnectivityManager ?: return false
val network = cm.activeNetwork ?: return false
val caps = cm.getNetworkCapabilities(network) ?: return false
return caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
}
fun showAsToast(context: Context) {
runOnMain { Toast.makeText(context, getRecoveryHint(), Toast.LENGTH_LONG).show() }
}
fun showUpdateDialog(context: Context, onUpdate: (() -> Unit)? = null, onCancel: (() -> Unit)? = null) {
runOnMain {
try {
val builder = AlertDialog.Builder(context)
.setTitle("⚠️ Bazaar Update Required")
.setMessage(getRecoveryHint())
.setCancelable(false)
.setPositiveButton("Update Now 🚀") { dialog, _ ->
openBazaarUpdatePage(context)
onUpdate?.invoke()
dialog.dismiss()
}
.setNegativeButton("Cancel ❌") { dialog, _ ->
onCancel?.invoke()
dialog.dismiss()
}
val dialog = builder.create()
dialog.setOnShowListener {
dialog.getButton(AlertDialog.BUTTON_POSITIVE)?.setTextColor(ContextCompat.getColor(context, android.R.color.holo_green_dark))
dialog.getButton(AlertDialog.BUTTON_NEGATIVE)?.setTextColor(ContextCompat.getColor(context, android.R.color.holo_red_dark))
}
dialog.show()
} catch (e: Exception) {
showAsToast(context)
}
}
}
fun openBazaarUpdatePage(context: Context) {
try {
val intent = Intent(Intent.ACTION_VIEW, getUpdateUri()).apply { addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) }
context.startActivity(intent)
} catch (e: ActivityNotFoundException) {
try {
val webIntent = Intent(Intent.ACTION_VIEW, getWebUpdateUri()).apply { addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) }
context.startActivity(webIntent)
} catch (_: Exception) {
showAsToast(context)
}
}
}
fun getRecoveryHint(): String = recoveryHint ?: "Please update Bazaar to continue."
fun diagnosticReport(context: Context): String = buildString {
appendLine("=== BazaarNotSupportedException Diagnostic ===")
appendLine("Package : $packageName")
appendLine("Required Version: ${requiredVersion ?: "Unknown"}")
appendLine("Current Version : ${currentVersion ?: "Unknown"}")
appendLine("Hint : ${getRecoveryHint()}")
appendLine("Elapsed Time : ${getReadableElapsedTime()}")
appendLine("Internet : ${if (hasInternetConnection(context)) "Available" else "Unavailable"}")
appendLine("==============================================")
}
fun toJson(pretty: Boolean = true): String {
val obj = JSONObject().apply {
put("error", "BazaarNotSupportedException")
put("package", packageName)
put("requiredVersion", requiredVersion)
put("currentVersion", currentVersion)
put("timestamp", timestamp)
put("hint", getRecoveryHint())
}
return if (pretty) obj.toString(2) else obj.toString()
}
fun toMap(): Map<String, Any?> = mapOf(
"type" to "BazaarNotSupportedException",
"package" to packageName,
"requiredVersion" to requiredVersion,
"currentVersion" to currentVersion,
"timestamp" to timestamp,
"hint" to getRecoveryHint()
)
private fun runOnMain(runnable: () -> Unit) {
if (Looper.myLooper() == Looper.getMainLooper()) runnable() else Handler(Looper.getMainLooper()).post(runnable)
}
}
/* ===============================================================
2️⃣ ConsumeFailedException
=============================================================== */
class ConsumeFailedException(
val productId: String? = null,
val purchaseToken: String? = null,
val reason: String? = null,
val timestamp: Long = System.currentTimeMillis()
) : RemoteException() {
override val message: String
get() = reason ?: "Consume request failed: It's from Bazaar"
private var retryCount: Int = 0
private var firstFailureTime: Long = SystemClock.elapsedRealtime()
fun incrementRetry() { retryCount++ }
fun getRetryCount(): Int = retryCount
fun resetRetryTracking() { retryCount = 0; firstFailureTime = SystemClock.elapsedRealtime() }
fun timeSinceFirstFailureSeconds(): Long = (SystemClock.elapsedRealtime() - firstFailureTime) / 1000
fun getReadableTimeSinceFailure(): String {
val seconds = timeSinceFirstFailureSeconds()
val minutes = seconds / 60
val remaining = seconds % 60
return if (minutes > 0) "${minutes}m ${remaining}s ago" else "${remaining}s ago"
}
fun isTemporary(): Boolean = reason?.contains("network", true) == true || reason?.contains("timeout", true) == true
fun isPermanent(): Boolean = reason?.contains("token", true) == true || reason?.contains("invalid", true) == true
fun isRetryable(maxRetries: Int = 3): Boolean = isTemporary() && retryCount < maxRetries
fun getRecoverySuggestion(): String = when {
reason?.contains("network", true) == true -> "Check your internet connection."
reason?.contains("token", true) == true -> "Purchase token may have expired."
reason?.contains("timeout", true) == true -> "Try increasing timeout or retrying later."
else -> "Restart Bazaar and retry the operation."
}
fun showFriendlyToast(context: Context) {
runOnMain { Toast.makeText(context, getRecoverySuggestion(), Toast.LENGTH_LONG).show() }
}
fun fullDiagnosticReport(): String = buildString {
appendLine("=== ConsumeFailedException Diagnostic ===")
appendLine("Product ID : ${productId ?: "Unknown"}")
appendLine("Purchase Token : ${purchaseToken ?: "Unknown"}")
appendLine("Reason : ${reason ?: "Unknown"}")
appendLine("Retry Count : $retryCount")
appendLine("Elapsed Time : ${getReadableTimeSinceFailure()}")
appendLine("Temporary : ${isTemporary()}")
appendLine("Permanent : ${isPermanent()}")
appendLine("Recovery Hint : ${getRecoverySuggestion()}")
appendLine("=========================================")
}
fun toJson(pretty: Boolean = true): String {
val obj = JSONObject().apply {
put("error", "ConsumeFailedException")
put("productId", productId)
put("purchaseToken", purchaseToken)
put("reason", reason)
put("timestamp", timestamp)
put("retryCount", retryCount)
put("elapsedSeconds", timeSinceFirstFailureSeconds())
}
return if (pretty) obj.toString(2) else obj.toString()
}
fun toMap(): Map<String, Any?> = mapOf(
"type" to "ConsumeFailedException",
"productId" to productId,
"purchaseToken" to purchaseToken,
"reason" to reason,
"timestamp" to timestamp,
"retryCount" to retryCount
)
private fun runOnMain(runnable: () -> Unit) {
if (Looper.myLooper() == Looper.getMainLooper()) runnable() else Handler(Looper.getMainLooper()).post(runnable)
}
companion object {
fun fromNetworkError() = ConsumeFailedException(reason = "Network connection error")
fun fromInvalidToken(token: String) = ConsumeFailedException(purchaseToken = token, reason = "Invalid or expired purchase token")
fun fromTimeout() = ConsumeFailedException(reason = "Consume request timed out")
fun fromUnknownError() = ConsumeFailedException(reason = "Unknown internal error")
}
}
/* ===============================================================
3️⃣ DisconnectException
=============================================================== */
class DisconnectException : IllegalStateException() {
override val message: String
get() = "We can't communicate with Bazaar: Service is disconnected"
fun showAsToast(context: Context) {
Toast.makeText(context, message, Toast.LENGTH_LONG).show()
}
fun showEnhancedDialog(context: Context, onRetry: (() -> Unit)? = null) {
runOnMain {
try {
val builder = AlertDialog.Builder(context)
.setTitle("⚠️ Bazaar Service Disconnected")
.setMessage(message)
.setCancelable(false)
.setPositiveButton("Retry 🔄") { dialog, _ ->
onRetry?.invoke()
dialog.dismiss()
}
.setNegativeButton("Dismiss ❌") { dialog, _ ->
dialog.dismiss()
}
val dialog = builder.create()
dialog.setOnShowListener {
dialog.getButton(AlertDialog.BUTTON_POSITIVE)?.setTextColor(ContextCompat.getColor(context, android.R.color.holo_blue_dark))
dialog.getButton(AlertDialog.BUTTON_NEGATIVE)?.setTextColor(ContextCompat.getColor(context, android.R.color.holo_red_dark))
}
dialog.show()
} catch (e: Exception) {
showAsToast(context)
}
}
}
fun isRecoverable(): Boolean = true
fun toJson(pretty: Boolean = true): String = JSONObject().apply {
put("error", "DisconnectException")
put("message", message)
}.let { if (pretty) it.toString(2) else it.toString() }
fun toMap(): Map<String, Any?> = mapOf("type" to "DisconnectException", "message" to message)
fun toAnalyticsMap(): Map<String, String> = mapOf("type" to "DisconnectException", "message" to message, "timestamp" to System.currentTimeMillis().toString())
private fun runOnMain(runnable: () -> Unit) {
if (Looper.myLooper() == Looper.getMainLooper()) runnable() else Handler(Looper.getMainLooper()).post(runnable)
}
}
/* ===============================================================
4️⃣ DynamicPriceNotSupportedException
=============================================================== */
class DynamicPriceNotSupportedException : IllegalStateException() {
override val message: String
get() = "Dynamic price not supported"
fun showDialog(context: Context) {
runOnMain {
try {
AlertDialog.Builder(context)
.setTitle("⚠️ Unsupported Feature")
.setMessage(message)
.setPositiveButton("OK") { dialog, _ -> dialog.dismiss() }
.show()
} catch (e: Exception) {
Toast.makeText(context, message, Toast.LENGTH_LONG).show()
}
}
}
fun toAnalyticsMap(): Map<String, String> = mapOf("type" to "DynamicPriceNotSupportedException", "message" to message, "timestamp" to System.currentTimeMillis().toString())
private fun runOnMain(runnable: () -> Unit) {
if (Looper.myLooper() == Looper.getMainLooper()) runnable() else Handler(Looper.getMainLooper()).post(runnable)
}
}
package ir.cafebazaar.poolakey.exception
import android.app.AlertDialog
import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
import android.net.ConnectivityManager
import android.net.NetworkCapabilities
import android.os.Build
import android.os.Handler
import android.os.Looper
import android.os.RemoteException
import android.os.SystemClock
import android.widget.Toast
import androidx.core.content.ContextCompat
import org.json.JSONObject
private fun runOnMain(runnable: () -> Unit) {
if (Looper.myLooper() == Looper.getMainLooper()) runnable() else Handler(Looper.getMainLooper()).post(runnable)
}
/* ===============================================================
1️⃣ BazaarNotSupportedException
=============================================================== */
class BazaarNotSupportedException(
val packageName: String = "com.farsitel.bazaar",
val requiredVersion: Int? = null,
val currentVersion: Int? = null,
val timestamp: Long = System.currentTimeMillis(),
val recoveryHint: String? = "Please update Bazaar to the latest version to continue."
) : IllegalStateException() {
override val message: String get() = "Bazaar is not updated"
companion object {
private const val BAZAAR_PACKAGE = "com.farsitel.bazaar"
fun getUpdateUri(): android.net.Uri = android.net.Uri.parse("bazaar://details?id=$BAZAAR_PACKAGE")
fun getWebUpdateUri(): android.net.Uri = android.net.Uri.parse("https://cafebazaar.ir/app/$BAZAAR_PACKAGE?l=en")
}
fun needsUpdate(): Boolean = requiredVersion != null && currentVersion != null && currentVersion < requiredVersion
fun hasInternetConnection(context: Context): Boolean {
val cm = context.getSystemService(Context.CONNECTIVITY_SERVICE) as? ConnectivityManager ?: return false
val network = cm.activeNetwork ?: return false
val caps = cm.getNetworkCapabilities(network) ?: return false
return caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
}
fun showUpdateDialog(context: Context, onUpdate: (() -> Unit)? = null, onCancel: (() -> Unit)? = null) {
runOnMain {
try {
val builder = AlertDialog.Builder(context)
.setTitle("⚠️ Bazaar Update Required")
.setMessage(recoveryHint)
.setCancelable(false)
.setIcon(android.R.drawable.ic_dialog_alert)
.setPositiveButton("Update Now 🚀") { dialog, _ ->
openBazaarUpdatePage(context)
onUpdate?.invoke()
dialog.dismiss()
}
.setNegativeButton("Cancel ❌") { dialog, _ ->
onCancel?.invoke()
dialog.dismiss()
}
val dialog = builder.create()
dialog.setOnShowListener {
dialog.getButton(AlertDialog.BUTTON_POSITIVE)?.setTextColor(ContextCompat.getColor(context, android.R.color.holo_green_dark))
dialog.getButton(AlertDialog.BUTTON_NEGATIVE)?.setTextColor(ContextCompat.getColor(context, android.R.color.holo_red_dark))
}
dialog.show()
} catch (e: Exception) {
showAsToast(context)
}
}
}
fun openBazaarUpdatePage(context: Context) {
try {
context.startActivity(Intent(Intent.ACTION_VIEW, getUpdateUri()).apply { addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) })
} catch (e: ActivityNotFoundException) {
try {
context.startActivity(Intent(Intent.ACTION_VIEW, getWebUpdateUri()).apply { addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) })
} catch (_: Exception) {
showAsToast(context)
}
}
}
fun showAsToast(context: Context) = runOnMain { Toast.makeText(context, recoveryHint, Toast.LENGTH_LONG).show() }
fun toJson(pretty: Boolean = true): String = JSONObject().apply {
put("error", "BazaarNotSupportedException")
put("package", packageName)
put("requiredVersion", requiredVersion)
put("currentVersion", currentVersion)
put("timestamp", timestamp)
put("hint", recoveryHint)
}.let { if (pretty) it.toString(2) else it.toString() }
}
/* ===============================================================
2️⃣ ConsumeFailedException
=============================================================== */
class ConsumeFailedException(
val productId: String? = null,
val purchaseToken: String? = null,
val reason: String? = null,
val timestamp: Long = System.currentTimeMillis()
) : RemoteException() {
override val message: String get() = reason ?: "Consume request failed"
private var retryCount = 0
private var firstFailureTime = SystemClock.elapsedRealtime()
fun incrementRetry() { retryCount++ }
fun isRetryable(maxRetries: Int = 3) = reason?.contains("network", true) == true && retryCount < maxRetries
fun retryOperationIfNeeded(maxRetries: Int = 3, operation: () -> Unit) {
if (isRetryable(maxRetries)) {
incrementRetry()
runOnMain { Handler().postDelayed({ operation() }, 2000) } // Non-blocking
}
}
fun showFriendlyToast(context: Context) = runOnMain { Toast.makeText(context, recoverySuggestion(), Toast.LENGTH_LONG).show() }
fun recoverySuggestion(): String = when {
reason?.contains("network", true) == true -> "Check your internet connection."
reason?.contains("token", true) == true -> "Purchase token may have expired."
reason?.contains("timeout", true) == true -> "Try again later."
else -> "Restart Bazaar and retry."
}
fun toJson(pretty: Boolean = true): String = JSONObject().apply {
put("error", "ConsumeFailedException")
put("productId", productId)
put("purchaseToken", purchaseToken)
put("reason", reason)
put("timestamp", timestamp)
put("retryCount", retryCount)
}.let { if (pretty) it.toString(2) else it.toString() }
}
/* ===============================================================
3️⃣ DisconnectException
=============================================================== */
class DisconnectException : IllegalStateException() {
override val message: String get() = "Bazaar service disconnected"
fun showEnhancedDialog(context: Context, onRetry: (() -> Unit)? = null) {
runOnMain {
AlertDialog.Builder(context)
.setTitle("⚠️ Service Disconnected")
.setMessage(message)
.setCancelable(false)
.setIcon(android.R.drawable.ic_dialog_alert)
.setPositiveButton("Retry 🔄") { dialog, _ ->
onRetry?.invoke()
dialog.dismiss()
}
.setNegativeButton("Dismiss ❌") { dialog, _ -> dialog.dismiss() }
.show()
}
}
fun toJson(pretty: Boolean = true): String = JSONObject().apply {
put("error", "DisconnectException")
put("message", message)
}.let { if (pretty) it.toString(2) else it.toString() }
}
/* ===============================================================
4️⃣ DynamicPriceNotSupportedException
=============================================================== */
class DynamicPriceNotSupportedException : IllegalStateException() {
override val message: String get() = "Dynamic pricing not supported"
fun showDialog(context: Context) {
runOnMain {
AlertDialog.Builder(context)
.setTitle("⚠️ Unsupported Feature")
.setMessage(message)
.setPositiveButton("OK") { dialog, _ -> dialog.dismiss() }
.setIcon(android.R.drawable.ic_dialog_info)
.show()
}
}
}
/* ===============================================================
5️⃣ IAPNotSupportedException
=============================================================== */
class IAPNotSupportedException : IllegalAccessException() {
override val message: String? get() = "In-app billing not supported"
fun notifyUser(context: Context) = runOnMain { Toast.makeText(context, message, Toast.LENGTH_LONG).show() }
}
package ir.cafebazaar.poolakey.exception
import android.app.AlertDialog
import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
import android.net.ConnectivityManager
import android.net.NetworkCapabilities
import android.os.Handler
import android.os.Looper
import android.os.RemoteException
import android.os.SystemClock
import android.widget.Toast
import androidx.core.content.ContextCompat
import org.json.JSONObject
import java.lang.Exception
// Centralized utility function to run code on the main thread
private fun runOnMain(runnable: () -> Unit) {
if (Looper.myLooper() == Looper.getMainLooper()) runnable() else Handler(Looper.getMainLooper()).post(runnable)
}
/* ===============================================================
1️⃣ BazaarNotSupportedException
=============================================================== */
class BazaarNotSupportedException(
val packageName: String = "com.farsitel.bazaar",
val requiredVersion: Int? = null,
val currentVersion: Int? = null,
val timestamp: Long = System.currentTimeMillis(),
val recoveryHint: String? = "Please update Bazaar to the latest version to continue."
) : IllegalStateException() {
override val message: String get() = "Bazaar is not updated"
companion object {
private const val BAZAAR_PACKAGE = "com.farsitel.bazaar"
fun getUpdateUri() = android.net.Uri.parse("bazaar://details?id=$BAZAAR_PACKAGE")
fun getWebUpdateUri() = android.net.Uri.parse("https://cafebazaar.ir/app/$BAZAAR_PACKAGE?l=en")
}
fun needsUpdate(): Boolean = requiredVersion != null && currentVersion != null && currentVersion < requiredVersion
fun hasInternetConnection(context: Context): Boolean {
val cm = context.getSystemService(Context.CONNECTIVITY_SERVICE) as? ConnectivityManager ?: return false
val network = cm.activeNetwork ?: return false
val caps = cm.getNetworkCapabilities(network) ?: return false
return caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
}
fun showUpdateDialog(context: Context, onUpdate: (() -> Unit)? = null, onCancel: (() -> Unit)? = null) {
runOnMain {
try {
val builder = AlertDialog.Builder(context)
.setTitle("⚠️ Bazaar Update Required")
.setMessage(recoveryHint)
.setCancelable(false)
.setIcon(android.R.drawable.ic_dialog_alert)
.setPositiveButton("Update Now 🚀") { dialog, _ ->
openBazaarUpdatePage(context)
onUpdate?.invoke()
dialog.dismiss()
}
.setNegativeButton("Cancel ❌") { dialog, _ ->
onCancel?.invoke()
dialog.dismiss()
}
val dialog = builder.create()
dialog.setOnShowListener {
dialog.getButton(AlertDialog.BUTTON_POSITIVE)?.setTextColor(ContextCompat.getColor(context, android.R.color.holo_green_dark))
dialog.getButton(AlertDialog.BUTTON_NEGATIVE)?.setTextColor(ContextCompat.getColor(context, android.R.color.holo_red_dark))
}
dialog.show()
} catch (e: Exception) {
showAsToast(context)
}
}
}
fun openBazaarUpdatePage(context: Context) {
try {
context.startActivity(Intent(Intent.ACTION_VIEW, getUpdateUri()).apply { addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) })
} catch (e: ActivityNotFoundException) {
try {
context.startActivity(Intent(Intent.ACTION_VIEW, getWebUpdateUri()).apply { addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) })
} catch (_: Exception) {
showAsToast(context)
}
}
}
fun showAsToast(context: Context) = runOnMain { Toast.makeText(context, recoveryHint ?: "Update Bazaar to continue", Toast.LENGTH_LONG).show() }
fun toJson(pretty: Boolean = true): String = JSONObject().apply {
put("error", "BazaarNotSupportedException")
put("package", packageName)
put("requiredVersion", requiredVersion)
put("currentVersion", currentVersion)
put("timestamp", timestamp)
put("hint", recoveryHint)
}.let { if (pretty) it.toString(2) else it.toString() }
}
/* ===============================================================
2️⃣ ConsumeFailedException
=============================================================== */
class ConsumeFailedException(
val productId: String? = null,
val purchaseToken: String? = null,
val reason: String? = null,
val timestamp: Long = System.currentTimeMillis()
) : RemoteException() {
override val message: String get() = reason ?: "Consume request failed"
private var retryCount = 0
private var firstFailureTime = SystemClock.elapsedRealtime()
fun incrementRetry() { retryCount++ }
fun isRetryable(maxRetries: Int = 3) = reason?.contains("network", true) == true && retryCount < maxRetries
fun retryOperationIfNeeded(maxRetries: Int = 3, operation: () -> Unit) {
if (isRetryable(maxRetries)) {
incrementRetry()
runOnMain { Handler().postDelayed({ operation() }, 2000) } // Non-blocking retry
}
}
fun showFriendlyToast(context: Context) = runOnMain { Toast.makeText(context, recoverySuggestion(), Toast.LENGTH_LONG).show() }
fun recoverySuggestion(): String = when {
reason?.contains("network", true) == true -> "Check your internet connection."
reason?.contains("token", true) == true -> "Purchase token may have expired."
reason?.contains("timeout", true) == true -> "Try again later."
else -> "Restart Bazaar and retry."
}
fun toJson(pretty: Boolean = true): String = JSONObject().apply {
put("error", "ConsumeFailedException")
put("productId", productId)
put("purchaseToken", purchaseToken)
put("reason", reason)
put("timestamp", timestamp)
put("retryCount", retryCount)
}.let { if (pretty) it.toString(2) else it.toString() }
}
/* ===============================================================
3️⃣ DisconnectException
=============================================================== */
class DisconnectException : IllegalStateException() {
override val message: String get() = "Bazaar service disconnected"
fun showEnhancedDialog(context: Context, onRetry: (() -> Unit)? = null) {
runOnMain {
AlertDialog.Builder(context)
.setTitle("⚠️ Service Disconnected")
.setMessage(message)
.setCancelable(false)
.setIcon(android.R.drawable.ic_dialog_alert)
.setPositiveButton("Retry 🔄") { dialog, _ ->
onRetry?.invoke()
dialog.dismiss()
}
.setNegativeButton("Dismiss ❌") { dialog, _ -> dialog.dismiss() }
.show()
}
}
fun toJson(pretty: Boolean = true): String = JSONObject().apply {
put("error", "DisconnectException")
put("message", message)
}.let { if (pretty) it.toString(2) else it.toString() }
}
/* ===============================================================
4️⃣ DynamicPriceNotSupportedException
=============================================================== */
class DynamicPriceNotSupportedException : IllegalStateException() {
override val message: String get() = "Dynamic pricing not supported"
fun showDialog(context: Context) {
runOnMain {
AlertDialog.Builder(context)
.setTitle("⚠️ Unsupported Feature")
.setMessage(message)
.setPositiveButton("OK") { dialog, _ -> dialog.dismiss() }
.setIcon(android.R.drawable.ic_dialog_info)
.show()
}
}
}
/* ===============================================================
5️⃣ IAPNotSupportedException
=============================================================== */
class IAPNotSupportedException : IllegalAccessException() {
override val message: String? get() = "In-app billing not supported"
fun notifyUser(context: Context) = runOnMain { Toast.makeText(context, message, Toast.LENGTH_LONG).show() }
}
/* ===============================================================
6️⃣ PurchaseHijackedException
=============================================================== */
class PurchaseHijackedException : Exception() {
override val message: String? get() = "The purchase was hijacked and it's not a valid purchase"
}
package ir.cafebazaar.poolakey.exception
import android.app.AlertDialog
import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
import android.net.ConnectivityManager
import android.net.NetworkCapabilities
import android.os.Build
import android.os.Handler
import android.os.Looper
import android.os.RemoteException
import android.os.SystemClock
import android.widget.Toast
import androidx.core.content.ContextCompat
import org.json.JSONObject
import java.lang.Exception
// Run safely on main thread
private fun runOnMain(runnable: () -> Unit) {
if (Looper.myLooper() == Looper.getMainLooper()) runnable()
else Handler(Looper.getMainLooper()).post(runnable)
}
/* ===============================================================
1️⃣ BazaarNotSupportedException
=============================================================== */
class BazaarNotSupportedException(
val packageName: String = "com.farsitel.bazaar",
val requiredVersion: Int? = null,
val currentVersion: Int? = null,
val timestamp: Long = System.currentTimeMillis(),
val recoveryHint: String? = "Please update Bazaar to the latest version to continue."
) : IllegalStateException() {
override val message: String get() = "Bazaar is not updated"
companion object {
private const val BAZAAR_PACKAGE = "com.farsitel.bazaar"
fun getUpdateUri() = android.net.Uri.parse("bazaar://details?id=$BAZAAR_PACKAGE")
fun getWebUpdateUri() = android.net.Uri.parse("https://cafebazaar.ir/app/$BAZAAR_PACKAGE?l=en")
}
fun needsUpdate(): Boolean = requiredVersion != null && currentVersion != null && currentVersion < requiredVersion
fun hasInternetConnection(context: Context): Boolean {
val cm = context.getSystemService(Context.CONNECTIVITY_SERVICE) as? ConnectivityManager ?: return false
val network = cm.activeNetwork ?: return false
val caps = cm.getNetworkCapabilities(network) ?: return false
return caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
}
fun showUpdateDialog(context: Context, onUpdate: (() -> Unit)? = null, onCancel: (() -> Unit)? = null) {
runOnMain {
try {
val builder = AlertDialog.Builder(context)
.setTitle("⚠️ Bazaar Update Required")
.setMessage(recoveryHint)
.setCancelable(false)
.setIcon(android.R.drawable.ic_dialog_alert)
.setPositiveButton("Update Now 🚀") { dialog, _ ->
openBazaarUpdatePage(context)
onUpdate?.invoke()
dialog.dismiss()
}
.setNegativeButton("Cancel ❌") { dialog, _ ->
onCancel?.invoke()
dialog.dismiss()
}
val dialog = builder.create()
dialog.setOnShowListener {
dialog.getButton(AlertDialog.BUTTON_POSITIVE)?.setTextColor(ContextCompat.getColor(context, android.R.color.holo_green_dark))
dialog.getButton(AlertDialog.BUTTON_NEGATIVE)?.setTextColor(ContextCompat.getColor(context, android.R.color.holo_red_dark))
}
dialog.show()
} catch (e: Exception) {
showAsToast(context)
}
}
}
fun openBazaarUpdatePage(context: Context) {
try {
context.startActivity(Intent(Intent.ACTION_VIEW, getUpdateUri()).apply { addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) })
} catch (e: ActivityNotFoundException) {
try {
context.startActivity(Intent(Intent.ACTION_VIEW, getWebUpdateUri()).apply { addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) })
} catch (_: Exception) {
showAsToast(context)
}
}
}
fun showAsToast(context: Context) = runOnMain {
Toast.makeText(context, recoveryHint ?: "Update Bazaar to continue", Toast.LENGTH_LONG).show()
}
fun toJson(pretty: Boolean = true): String = JSONObject().apply {
put("error", "BazaarNotSupportedException")
put("package", packageName)
put("requiredVersion", requiredVersion)
put("currentVersion", currentVersion)
put("timestamp", timestamp)
put("hint", recoveryHint)
}.let { if (pretty) it.toString(2) else it.toString() }
}
/* ===============================================================
2️⃣ ConsumeFailedException
=============================================================== */
class ConsumeFailedException(
val productId: String? = null,
val purchaseToken: String? = null,
val reason: String? = null,
val timestamp: Long = System.currentTimeMillis()
) : RemoteException() {
override val message: String get() = reason ?: "Consume request failed"
private var retryCount = 0
private var firstFailureTime = SystemClock.elapsedRealtime()
fun incrementRetry() { retryCount++ }
fun isRetryable(maxRetries: Int = 3) = reason?.contains("network", true) == true && retryCount < maxRetries
fun retryOperationIfNeeded(maxRetries: Int = 3, operation: () -> Unit) {
if (isRetryable(maxRetries)) {
incrementRetry()
runOnMain { Handler().postDelayed({ operation() }, 2000) }
}
}
fun showFriendlyToast(context: Context) = runOnMain {
Toast.makeText(context, recoverySuggestion(), Toast.LENGTH_LONG).show()
}
fun recoverySuggestion(): String = when {
reason?.contains("network", true) == true -> "Check your internet connection."
reason?.contains("token", true) == true -> "Purchase token may have expired."
reason?.contains("timeout", true) == true -> "Try again later."
else -> "Restart Bazaar and retry."
}
fun toJson(pretty: Boolean = true): String = JSONObject().apply {
put("error", "ConsumeFailedException")
put("productId", productId)
put("purchaseToken", purchaseToken)
put("reason", reason)
put("timestamp", timestamp)
put("retryCount", retryCount)
}.let { if (pretty) it.toString(2) else it.toString() }
}
/* ===============================================================
3️⃣ DisconnectException
=============================================================== */
class DisconnectException : IllegalStateException() {
override val message: String get() = "Bazaar service disconnected"
fun showEnhancedDialog(context: Context, onRetry: (() -> Unit)? = null) {
runOnMain {
AlertDialog.Builder(context)
.setTitle("⚠️ Service Disconnected")
.setMessage(message)
.setCancelable(false)
.setIcon(android.R.drawable.ic_dialog_alert)
.setPositiveButton("Retry 🔄") { dialog, _ ->
onRetry?.invoke()
dialog.dismiss()
}
.setNegativeButton("Dismiss ❌") { dialog, _ -> dialog.dismiss() }
.show()
}
}
fun toJson(pretty: Boolean = true): String = JSONObject().apply {
put("error", "DisconnectException")
put("message", message)
}.let { if (pretty) it.toString(2) else it.toString() }
}
/* ===============================================================
4️⃣ DynamicPriceNotSupportedException
=============================================================== */
class DynamicPriceNotSupportedException : IllegalStateException() {
override val message: String get() = "Dynamic pricing not supported"
fun showDialog(context: Context) {
runOnMain {
AlertDialog.Builder(context)
.setTitle("⚠️ Unsupported Feature")
.setMessage(message)
.setPositiveButton("OK") { dialog, _ -> dialog.dismiss() }
.setIcon(android.R.drawable.ic_dialog_info)
.show()
}
}
}
/* ===============================================================
5️⃣ IAPNotSupportedException
=============================================================== */
class IAPNotSupportedException : IllegalAccessException() {
override val message: String? get() = "In-app billing not supported"
fun notifyUser(context: Context) = runOnMain {
Toast.makeText(context, message, Toast.LENGTH_LONG).show()
}
}
/* ===============================================================
6️⃣ PurchaseHijackedException
=============================================================== */
class PurchaseHijackedException : Exception() {
override val message: String? get() = "The purchase was hijacked and it's not a valid purchase"
}
/* ===============================================================
7️⃣ ResultNotOkayException
=============================================================== */
class ResultNotOkayException : IllegalStateException() {
override val message: String? get() = "Failed to receive response from Bazaar"
}
package ir.cafebazaar.poolakey.exception
import android.app.AlertDialog
import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
import android.net.ConnectivityManager
import android.net.NetworkCapabilities
import android.os.Handler
import android.os.Looper
import android.os.RemoteException
import android.os.SystemClock
import android.widget.Toast
import androidx.core.content.ContextCompat
import org.json.JSONObject
import java.lang.Exception
// Centralized utility function to run code on the main thread
private fun runOnMain(runnable: () -> Unit) {
if (Looper.myLooper() == Looper.getMainLooper()) runnable() else Handler(Looper.getMainLooper()).post(runnable)
}
/* ===============================================================
1️⃣ BazaarNotSupportedException
=============================================================== */
class BazaarNotSupportedException(
val packageName: String = "com.farsitel.bazaar",
val requiredVersion: Int? = null,
val currentVersion: Int? = null,
val timestamp: Long = System.currentTimeMillis(),
val recoveryHint: String? = "Please update Bazaar to the latest version to continue."
) : IllegalStateException() {
override val message: String get() = "Bazaar is not updated"
companion object {
private const val BAZAAR_PACKAGE = "com.farsitel.bazaar"
fun getUpdateUri() = android.net.Uri.parse("bazaar://details?id=$BAZAAR_PACKAGE")
fun getWebUpdateUri() = android.net.Uri.parse("https://cafebazaar.ir/app/$BAZAAR_PACKAGE?l=en")
}
fun needsUpdate(): Boolean = requiredVersion != null && currentVersion != null && currentVersion < requiredVersion
fun hasInternetConnection(context: Context): Boolean {
val cm = context.getSystemService(Context.CONNECTIVITY_SERVICE) as? ConnectivityManager ?: return false
val network = cm.activeNetwork ?: return false
val caps = cm.getNetworkCapabilities(network) ?: return false
return caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
}
fun showUpdateDialog(context: Context, onUpdate: (() -> Unit)? = null, onCancel: (() -> Unit)? = null) {
runOnMain {
try {
val builder = AlertDialog.Builder(context)
.setTitle("⚠️ Bazaar Update Required")
.setMessage(recoveryHint)
.setCancelable(false)
.setIcon(android.R.drawable.ic_dialog_alert)
.setPositiveButton("Update Now 🚀") { dialog, _ ->
openBazaarUpdatePage(context)
onUpdate?.invoke()
dialog.dismiss()
}
.setNegativeButton("Cancel ❌") { dialog, _ ->
onCancel?.invoke()
dialog.dismiss()
}
val dialog = builder.create()
dialog.setOnShowListener {
dialog.getButton(AlertDialog.BUTTON_POSITIVE)?.setTextColor(ContextCompat.getColor(context, android.R.color.holo_green_dark))
dialog.getButton(AlertDialog.BUTTON_NEGATIVE)?.setTextColor(ContextCompat.getColor(context, android.R.color.holo_red_dark))
}
dialog.show()
} catch (e: Exception) {
showAsToast(context)
}
}
}
fun openBazaarUpdatePage(context: Context) {
try {
context.startActivity(Intent(Intent.ACTION_VIEW, getUpdateUri()).apply { addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) })
} catch (e: ActivityNotFoundException) {
try {
context.startActivity(Intent(Intent.ACTION_VIEW, getWebUpdateUri()).apply { addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) })
} catch (_: Exception) {
showAsToast(context)
}
}
}
fun showAsToast(context: Context) = runOnMain {
Toast.makeText(context, recoveryHint ?: "Update Bazaar to continue", Toast.LENGTH_LONG).show()
}
fun toJson(pretty: Boolean = true): String = JSONObject().apply {
put("error", "BazaarNotSupportedException")
put("package", packageName)
put("requiredVersion", requiredVersion)
put("currentVersion", currentVersion)
put("timestamp", timestamp)
put("hint", recoveryHint)
}.let { if (pretty) it.toString(2) else it.toString() }
}
/* ===============================================================
2️⃣ ConsumeFailedException
=============================================================== */
class ConsumeFailedException(
val productId: String? = null,
val purchaseToken: String? = null,
val reason: String? = null,
val timestamp: Long = System.currentTimeMillis()
) : RemoteException() {
override val message: String get() = reason ?: "Consume request failed"
private var retryCount = 0
private var firstFailureTime = SystemClock.elapsedRealtime()
fun incrementRetry() { retryCount++ }
fun isRetryable(maxRetries: Int = 3) = reason?.contains("network", true) == true && retryCount < maxRetries
fun retryOperationIfNeeded(maxRetries: Int = 3, operation: () -> Unit) {
if (isRetryable(maxRetries)) {
incrementRetry()
runOnMain { Handler().postDelayed({ operation() }, 2000) }
}
}
fun showFriendlyToast(context: Context) = runOnMain {
Toast.makeText(context, recoverySuggestion(), Toast.LENGTH_LONG).show()
}
fun recoverySuggestion(): String = when {
reason?.contains("network", true) == true -> "Check your internet connection."
reason?.contains("token", true) == true -> "Purchase token may have expired."
reason?.contains("timeout", true) == true -> "Try again later."
else -> "Restart Bazaar and retry."
}
fun toJson(pretty: Boolean = true): String = JSONObject().apply {
put("error", "ConsumeFailedException")
put("productId", productId)
put("purchaseToken", purchaseToken)
put("reason", reason)
put("timestamp", timestamp)
put("retryCount", retryCount)
}.let { if (pretty) it.toString(2) else it.toString() }
}
/* ===============================================================
3️⃣ DisconnectException
=============================================================== */
class DisconnectException : IllegalStateException() {
override val message: String get() = "Bazaar service disconnected"
fun showEnhancedDialog(context: Context, onRetry: (() -> Unit)? = null) {
runOnMain {
AlertDialog.Builder(context)
.setTitle("⚠️ Service Disconnected")
.setMessage(message)
.setCancelable(false)
.setIcon(android.R.drawable.ic_dialog_alert)
.setPositiveButton("Retry 🔄") { dialog, _ ->
onRetry?.invoke()
dialog.dismiss()
}
.setNegativeButton("Dismiss ❌") { dialog, _ -> dialog.dismiss() }
.show()
}
}
fun toJson(pretty: Boolean = true): String = JSONObject().apply {
put("error", "DisconnectException")
put("message", message)
}.let { if (pretty) it.toString(2) else it.toString() }
}
/* ===============================================================
4️⃣ DynamicPriceNotSupportedException
=============================================================== */
class DynamicPriceNotSupportedException : IllegalStateException() {
override val message: String get() = "Dynamic pricing not supported"
fun showDialog(context: Context) {
runOnMain {
AlertDialog.Builder(context)
.setTitle("⚠️ Unsupported Feature")
.setMessage(message)
.setPositiveButton("OK") { dialog, _ -> dialog.dismiss() }
.setIcon(android.R.drawable.ic_dialog_info)
.show()
}
}
}
/* ===============================================================
5️⃣ IAPNotSupportedException
=============================================================== */
class IAPNotSupportedException : IllegalAccessException() {
override val message: String? get() = "In-app billing not supported"
fun notifyUser(context: Context) = runOnMain {
Toast.makeText(context, message, Toast.LENGTH_LONG).show()
}
}
/* ===============================================================
6️⃣ PurchaseHijackedException
=============================================================== */
class PurchaseHijackedException : Exception() {
override val message: String? get() = "The purchase was hijacked and it's not a valid purchase"
}
/* ===============================================================
7️⃣ ResultNotOkayException
=============================================================== */
class ResultNotOkayException : IllegalStateException() {
override val message: String? get() = "Failed to receive response from Bazaar"
}
/* ===============================================================
8️⃣ SubsNotSupportedException
=============================================================== */
class SubsNotSupportedException : IllegalAccessException() {
override val message: String?
get() = "Subscription is not supported in this version of installed Bazaar"
}
android_exception_manager.py
`InAppPurchaseModels.kt`
subscription_manager.py
`BazaarConstants.kt`
BazaarConstants.py
purchase_analysis.py
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Hello,
I have developed an updated and unified version of the subscription and purchase management system for Android apps, which includes advanced handling of subscriptions, user behavior analytics, refund prediction, and machine learning models to optimize revenue and trial conversion rates. This version is designed with readability, efficiency, and future extensibility in mind, providing developers with enhanced analytical and visualization tools. Merging these changes into your repository can improve code quality, automate data analysis, and offer advanced decision-making capabilities for the team. I hope you find these improvements valuable and consider integrating them into the main project.