Java tutorial
/* * Copyright 2014 Uwe Trottmann * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.battlelancer.seriesguide.billing; import android.app.AlertDialog; 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.v4.app.NotificationCompat; import android.support.v4.app.TaskStackBuilder; import android.support.v7.app.ActionBar; 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.BuildConfig; import com.battlelancer.seriesguide.R; 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 = "x_sub_2014_02"; public static final String SKU_X_SUB_LEGACY = "x_subscription"; // (arbitrary) request code for the purchase flow private static final int RC_REQUEST = 749758; private static final String SOME_STRING = "SURPTk9UQ0FSRUlGWU9VUElSQVRFVEhJUw=="; private IabHelper mHelper; 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(); // do not query IAB if user has key boolean hasUpgrade = Utils.hasXpass(this); updateViewStates(hasUpgrade); // no need to go further if user has a key if (hasUpgrade) { setWaitMode(false); return; } setWaitMode(true); mHelper = new IabHelper(this, getPublicKey()); // enable debug logging (for a production application, you should set // this to false). mHelper.enableDebugLogging(BuildConfig.DEBUG); Timber.i("Starting setup."); mHelper.startSetup(new IabHelper.OnIabSetupFinishedListener() { public void onIabSetupFinished(IabResult result) { Timber.d("Setup finished."); if (!result.isSuccess()) { // Oh noes, there was a problem. complain("Problem setting up In-app Billing: " + result); enableFallBackMode(); setWaitMode(false); return; } // Have we been disposed of in the meantime? If so, quit. if (mHelper == null) { return; } // Hooray, IAB is fully set up. // Get an inventory of stuff we own, also get SKU details for pricing info Timber.d("Setup successful. Querying inventory."); List<String> detailSkus = new ArrayList<>(); detailSkus.add(SKU_X_SUB); mHelper.queryInventoryAsync(true, detailSkus, mGotInventoryListener); } }); } @Override protected void setupActionBar() { super.setupActionBar(); ActionBar actionBar = getSupportActionBar(); 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"); Utils.trackAction(BillingActivity.this, "X Features", "Get 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(" + requestCode + "," + resultCode + "," + data); // Have we been disposed of in the meantime? If so, quit. if (mHelper == null) { return; } // Pass on the activity result to the helper for handling if (!mHelper.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 onDestroy() { super.onDestroy(); Timber.i("Disposing of IabHelper."); if (mHelper != null) { mHelper.dispose(); } mHelper = null; } // Listener that's called when we finish querying the items and // subscriptions we own IabHelper.QueryInventoryFinishedListener mGotInventoryListener = new IabHelper.QueryInventoryFinishedListener() { public void onQueryInventoryFinished(IabResult result, Inventory inventory) { Timber.d("Query inventory finished."); // Have we been disposed of in the meantime? If so, quit. if (mHelper == null) { return; } if (result.isFailure()) { complain("Could not query inventory: " + result); return; } Timber.d("Query inventory was successful."); // get sub state boolean hasUpgrade = checkForSubscription(BillingActivity.this, inventory); // get local sub price SkuDetails skuDetails = inventory.getSkuDetails(SKU_X_SUB); if (skuDetails != null) { mSubPrice = skuDetails.getPrice(); } updateViewStates(hasUpgrade); setWaitMode(false); Timber.d("Initial inventory query finished; enabling main UI."); } }; /** * 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#setSubscriptionState(Context, boolean)}. */ public static boolean checkForSubscription(Context context, 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? He gets * X for life. */ Purchase premiumPurchase = inventory.getPurchase(SKU_X); boolean hasXUpgrade = (premiumPurchase != null && verifyDeveloperPayload(premiumPurchase)); // Does the user subscribe to the X features? Purchase xSubLegacy = inventory.getPurchase(SKU_X_SUB_LEGACY); Purchase xSub = inventory.getPurchase(SKU_X_SUB); boolean isSubscribedToX = (xSubLegacy != null && verifyDeveloperPayload(xSubLegacy)) || (xSub != null && verifyDeveloperPayload(xSub)); if (hasXUpgrade) { Timber.d("User has X SUBSCRIPTION for life."); } else { Timber.d("User has " + (isSubscribedToX ? "X SUBSCRIPTION" : "NO X SUBSCRIPTION")); } // notify the user about a change in subscription state boolean isSubscribedOld = AdvancedSettings.getLastSubscriptionState(context); boolean isSubscribed = hasXUpgrade || isSubscribedToX; if (!isSubscribedOld && isSubscribed) { Toast.makeText(context, R.string.subscription_activated, Toast.LENGTH_SHORT).show(); } else if (isSubscribedOld && !isSubscribed) { onExpiredNotification(context); } // Save current state until we query again AdvancedSettings.setSubscriptionState(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(R.string.subscription_expired, 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); mHelper.launchSubscriptionPurchaseFlow(this, SKU_X_SUB, RC_REQUEST, mPurchaseFinishedListener, payload); Utils.trackAction(this, "X Features", "Subscribe"); } // Callback for when a purchase is finished IabHelper.OnIabPurchaseFinishedListener mPurchaseFinishedListener = new IabHelper.OnIabPurchaseFinishedListener() { public void onIabPurchaseFinished(IabResult result, Purchase purchase) { Timber.d("Purchase finished: " + result + ", purchase: " + purchase); // Have we been disposed of in the meantime? If so, quit. if (mHelper == null) { return; } if (result.isFailure()) { if (result.getResponse() != IabHelper.IABHELPER_USER_CANCELLED) { complain("Error purchasing: " + result); } setWaitMode(false); return; } if (!verifyDeveloperPayload(purchase)) { complain("Error purchasing. Authenticity verification failed."); setWaitMode(false); return; } Timber.d("Purchase successful."); if (purchase.getSku().equals(SKU_X_SUB)) { Timber.d("Purchased X subscription. Congratulating user."); // Save current state until we query again AdvancedSettings.setSubscriptionState(BillingActivity.this, true); updateViewStates(true); setWaitMode(false); } } }; /** * Returns the public key used for verification of purchases by {@link IabHelper}. */ public static String getPublicKey() { return BuildConfig.IAP_KEY_A + BuildConfig.IAP_KEY_B + BuildConfig.IAP_KEY_C + BuildConfig.IAP_KEY_D; } 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 complain(String message) { Timber.e(message); alert("Error: " + message); } private void alert(String message) { AlertDialog.Builder bld = new AlertDialog.Builder(this); bld.setMessage(message); bld.setNeutralButton(android.R.string.ok, null); Timber.d("Showing alert dialog: " + message); bld.create().show(); } }