package com.battlelancer.seriesguide.billing; import android.app.Notification; import android.app.NotificationManager; import android.app.PendingIntent; import android.content.Context; import android.content.Intent; import android.os.Bundle; import android.support.annotation.NonNull; import android.support.v4.app.NotificationCompat; import android.support.v4.app.TaskStackBuilder; import android.support.v7.app.ActionBar; import android.support.v7.app.AlertDialog; import android.view.MenuItem; import android.view.View; import android.view.View.OnClickListener; import android.widget.Button; import android.widget.TextView; import android.widget.Toast; import com.battlelancer.seriesguide.R; import com.battlelancer.seriesguide.SgApp; import com.battlelancer.seriesguide.settings.AdvancedSettings; import com.battlelancer.seriesguide.ui.BaseActivity; import com.battlelancer.seriesguide.ui.SeriesGuidePreferences; import com.battlelancer.seriesguide.ui.ShowsActivity; import com.battlelancer.seriesguide.util.Utils; import java.util.ArrayList; import java.util.List; import timber.log.Timber; public class BillingActivity extends BaseActivity { public static final String TAG = "BillingActivity"; // The SKU product ids as set in the Developer Console public static final String SKU_X = "x_upgrade"; public static final String SKU_X_SUB_2016_05 = "x_sub_2016_05"; public static final String SKU_X_SUB_2014_02 = "x_sub_2014_02"; public static final String SKU_X_SUB_LEGACY = "x_subscription"; public static final String SKU_X_SUB_NEW_PURCHASES = SKU_X_SUB_2016_05; // (arbitrary) request code for the purchase flow private static final int RC_REQUEST = 21; private static final String SOME_STRING = "SURPTk9UQ0FSRUlGWU9VUElSQVRFVEhJUw=="; private IabHelper billingHelper; private View mProgressScreen; private View mContentContainer; private Button mButtonSub; private Button mButtonPass; private TextView mTextViewPriceSub; private String mSubPrice; private View mTextHasUpgrade; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_billing); setupActionBar(); setupViews(); } @Override protected void setupActionBar() { super.setupActionBar(); ActionBar actionBar = getSupportActionBar(); if (actionBar != null) { actionBar.setDisplayHomeAsUpEnabled(true); } } private void setupViews() { mButtonSub = (Button) findViewById(R.id.buttonBillingGetSubscription); mButtonSub.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { onSubscribeToXButtonClicked(); } }); mTextViewPriceSub = (TextView) findViewById(R.id.textViewBillingPriceSubscription); mButtonPass = (Button) findViewById(R.id.buttonBillingGetPass); mButtonPass.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { Utils.launchWebsite(BillingActivity.this, getString(R.string.url_x_pass), TAG, "X Pass"); } }); mTextHasUpgrade = findViewById(R.id.textViewBillingExisting); mProgressScreen = findViewById(R.id.progressBarBilling); mContentContainer = findViewById(R.id.containerBilling); findViewById(R.id.textViewBillingMoreInfo).setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { Utils.launchWebsite(BillingActivity.this, getString(R.string.url_whypay), TAG, "WhyPayWebsite"); } }); } @Override public boolean onOptionsItemSelected(MenuItem item) { if (item.getItemId() == android.R.id.home) { super.onBackPressed(); return true; } return false; } @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { Timber.d("onActivityResult(%s,%s,%s)", requestCode, resultCode, data); // Have we been disposed of in the meantime? If so, quit. if (billingHelper == null) { return; } // Pass on the activity result to the helper for handling if (!billingHelper.handleActivityResult(requestCode, resultCode, data)) { // not handled, so handle it ourselves (here's where you'd // perform any handling of activity results not related to in-app // billing... super.onActivityResult(requestCode, resultCode, data); } else { Timber.d("onActivityResult handled by IABUtil."); } } @Override protected void onStart() { super.onStart(); // do not query IAB if user has key boolean hasUpgrade = Utils.hasXpass(this); updateViewStates(hasUpgrade); if (hasUpgrade) { setWaitMode(false); } else { setWaitMode(true); billingHelper = new IabHelper(this); billingHelper.startSetup(billingSetupListener); } } @Override protected void onStop() { super.onStop(); if (billingHelper != null) { billingHelper.dispose(); billingHelper = null; } } private IabHelper.OnIabSetupFinishedListener billingSetupListener = new IabHelper.OnIabSetupFinishedListener() { public void onIabSetupFinished(IabResult result) { if (billingHelper == null) { // disposed return; } if (!result.isSuccess()) { logAndShowAlertDialog(R.string.subscription_unavailable, "Problem setting up In-app Billing: " + result.getMessage()); enableFallBackMode(); setWaitMode(false); return; } Timber.d("onIabSetupFinished: Successful. Querying inventory."); List<String> detailSkus = new ArrayList<>(); detailSkus.add(SKU_X_SUB_NEW_PURCHASES); billingHelper.queryInventoryAsync(true, detailSkus, queryInventoryFinishedListener); } }; // Listener that's called when we finish querying the items and // subscriptions we own IabHelper.QueryInventoryFinishedListener queryInventoryFinishedListener = new IabHelper.QueryInventoryFinishedListener() { public void onQueryInventoryFinished(IabResult result, Inventory inventory) { if (billingHelper == null) { // disposed return; } if (result.isFailure()) { logAndShowAlertDialog(R.string.subscription_unavailable, "Could not query inventory: " + result.getMessage()); return; } // get sub state boolean hasUpgrade = checkForSubscription(BillingActivity.this, inventory); // get local sub price SkuDetails skuDetails = inventory.getSkuDetails(SKU_X_SUB_NEW_PURCHASES); if (skuDetails != null) { mSubPrice = skuDetails.getPrice(); } updateViewStates(hasUpgrade); setWaitMode(false); } }; /** * Checks if the user is subscribed to X features or has the deprecated X upgrade (so he gets * the subscription for life). Also sets the current state through {@link * AdvancedSettings#setSupporterState(Context, boolean)}. */ public static boolean checkForSubscription(@NonNull Context context, @NonNull Inventory inventory) { /* * Check for items we own. Notice that for each purchase, we check the * developer payload to see if it's correct! See * verifyDeveloperPayload(). */ // Does the user have the deprecated X Upgrade in-app purchase? If so unlock all features. Purchase premiumPurchase = inventory.getPurchase(SKU_X); boolean hasXUpgrade = (premiumPurchase != null && verifyDeveloperPayload(premiumPurchase)); // Does the user have an active unlock all subscription? Purchase subscriptionPurchase = inventory.getPurchase(SKU_X_SUB_LEGACY); if (subscriptionPurchase == null) { subscriptionPurchase = inventory.getPurchase(SKU_X_SUB_2014_02); } if (subscriptionPurchase == null) { subscriptionPurchase = inventory.getPurchase(SKU_X_SUB_2016_05); } boolean isSubscribedToX = subscriptionPurchase != null && verifyDeveloperPayload(subscriptionPurchase); if (hasXUpgrade) { Timber.d("User has X SUBSCRIPTION for life."); } else { Timber.d("User has %s", isSubscribedToX ? "X SUBSCRIPTION" : "NO X SUBSCRIPTION"); } // notify the user about a change in subscription state boolean isSubscribedOld = AdvancedSettings.getLastSupporterState(context); boolean isSubscribed = hasXUpgrade || isSubscribedToX; if (!isSubscribedOld && isSubscribed) { Toast.makeText(context, R.string.upgrade_success, Toast.LENGTH_SHORT).show(); } else if (isSubscribedOld && !isSubscribed) { onExpiredNotification(context); } // Save current state until we query again AdvancedSettings.setSupporterState(context, isSubscribed); return isSubscribed; } /** * Displays a notification that the subscription has expired. Its action opens {@link * BillingActivity}. */ public static void onExpiredNotification(Context context) { NotificationCompat.Builder nb = new NotificationCompat.Builder(context); // set required attributes nb.setSmallIcon(R.drawable.ic_notification); nb.setContentTitle(context.getString(R.string.subscription_expired)); nb.setContentText(context.getString(R.string.subscription_expired_details)); // set additional attributes nb.setDefaults(Notification.DEFAULT_LIGHTS); nb.setAutoCancel(true); nb.setTicker(context.getString(R.string.subscription_expired_details)); nb.setPriority(NotificationCompat.PRIORITY_DEFAULT); // build task stack Intent notificationIntent = new Intent(context, BillingActivity.class); PendingIntent contentIntent = TaskStackBuilder .create(context) .addNextIntent(new Intent(context, ShowsActivity.class)) .addNextIntent(new Intent(context, SeriesGuidePreferences.class)) .addNextIntent(notificationIntent) .getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT); nb.setContentIntent(contentIntent); // build the notification Notification notification = nb.build(); // show the notification final NotificationManager nm = (NotificationManager) context .getSystemService(Context.NOTIFICATION_SERVICE); nm.notify(SgApp.NOTIFICATION_SUBSCRIPTION_ID, notification); } /** * Verifies the developer payload of a purchase. */ public static boolean verifyDeveloperPayload(Purchase p) { String payload = p.getDeveloperPayload(); /* * Not doing anything sophisticated here, * this is open source anyhow. */ return SOME_STRING.equals(payload); } // User clicked the "Subscribe" button. private void onSubscribeToXButtonClicked() { Timber.d("Subscribe button clicked; launching purchase flow for X subscription."); String payload = SOME_STRING; setWaitMode(true); billingHelper.launchSubscriptionPurchaseFlow(this, SKU_X_SUB_NEW_PURCHASES, RC_REQUEST, purchaseFinishedListener, payload); } // Callback for when a purchase is finished IabHelper.OnIabPurchaseFinishedListener purchaseFinishedListener = new IabHelper.OnIabPurchaseFinishedListener() { public void onIabPurchaseFinished(IabResult result, Purchase purchase) { Timber.d("Purchase finished: %s, purchase: %s", result, purchase); // Have we been disposed of in the meantime? If so, quit. if (billingHelper == null) { return; } if (result.isFailure()) { if (result.getResponse() != IabHelper.IABHELPER_USER_CANCELLED) { logAndShowAlertDialog(R.string.subscription_failed, result.getMessage()); } setWaitMode(false); return; } if (!verifyDeveloperPayload(purchase)) { logAndShowAlertDialog(R.string.subscription_failed, "Authenticity verification failed."); setWaitMode(false); return; } Timber.d("Purchase successful."); if (purchase.getSku().equals(SKU_X_SUB_NEW_PURCHASES)) { Timber.d("Purchased X subscription. Congratulating user."); // Save current state until we query again AdvancedSettings.setSupporterState(BillingActivity.this, true); updateViewStates(true); setWaitMode(false); } } }; private void updateViewStates(boolean hasUpgrade) { // Only enable purchase button if the user does not have the upgrade yet mButtonSub.setEnabled(!hasUpgrade); mTextViewPriceSub.setText( getString(R.string.billing_price_subscribe, mSubPrice != null ? mSubPrice : "--")); mButtonPass.setEnabled(!hasUpgrade); mTextHasUpgrade.setVisibility(hasUpgrade ? View.VISIBLE : View.GONE); } /** * Disables the purchase button and hides the subscribed message. */ private void enableFallBackMode() { mButtonSub.setEnabled(false); mButtonPass.setEnabled(true); mTextHasUpgrade.setVisibility(View.GONE); } private void setWaitMode(boolean isActive) { mProgressScreen.setVisibility(isActive ? View.VISIBLE : View.GONE); mContentContainer.setVisibility(isActive ? View.GONE : View.VISIBLE); } private void logAndShowAlertDialog(int errorResId, String message) { Timber.e(message); new AlertDialog.Builder(this) .setMessage(getString(errorResId) + "\n\n" + message) .setPositiveButton(android.R.string.ok, null) .create() .show(); } }