/* * Copyright (C) 2010 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. */ package com.geeksville.billing; import android.app.Activity; import android.app.AlertDialog; import android.content.Context; import android.content.DialogInterface; import android.content.SharedPreferences; import android.database.Cursor; import android.os.Handler; import android.preference.PreferenceManager; import android.util.Log; import android.widget.Toast; import com.flurry.android.FlurryAgent; import com.geeksville.billing.BillingService.RequestPurchase; import com.geeksville.billing.BillingService.RestoreTransactions; import com.geeksville.billing.Consts.PurchaseState; import com.geeksville.billing.Consts.ResponseCode; import com.geeksville.gaggle.R; /** * A sample application that demonstrates in-app billing. */ public class Donate { private static final String TAG = "Donate"; /** * Used for storing the log text. */ private static final String LOG_TEXT_KEY = "DUNGEONS_LOG_TEXT"; /** * The SharedPreferences key for recording whether we initialized the * database. If false, then we perform a RestoreTransactions request to get * all the purchases for this user. */ private static final String DB_INITIALIZED = "db_initialized"; private DonatePurchaseObserver mPurchaseObserver; private Handler mHandler; private BillingService mBillingService; private PurchaseDatabase mPurchaseDatabase; private Activity context; /** * The developer payload that is sent with subsequent purchase requests. */ private String mPayloadContents = null; /** * Each product in the catalog is either MANAGED or UNMANAGED. MANAGED means * that the product can be purchased only once per user (such as a new level * in a game). The purchase is remembered by Android Market and can be * restored if this application is uninstalled and then re-installed. * UNMANAGED is used for products that can be used up and purchased multiple * times (such as poker chips). It is up to the application to keep track of * UNMANAGED products for the user. */ private enum Managed { MANAGED, UNMANAGED } /** * A {@link PurchaseObserver} is used to get callbacks when Android Market * sends messages to this application so that we can update the UI. */ private class DonatePurchaseObserver extends PurchaseObserver { public DonatePurchaseObserver(Handler handler) { super(context, handler); } @Override public void onBillingSupported(boolean supported) { if (Consts.DEBUG) { Log.i(TAG, "supported: " + supported); } if (supported) { restoreDatabase(); } } @Override public void onPurchaseStateChange(PurchaseState purchaseState, String itemId, int quantity, long purchaseTime, String developerPayload) { if (Consts.DEBUG) { Log.i(TAG, "onPurchaseStateChange() itemId: " + itemId + " " + purchaseState); } if (purchaseState == PurchaseState.PURCHASED) { PreferenceManager.getDefaultSharedPreferences(context).edit() .putBoolean("has_donated", true).commit(); thanksForDonating(); } close(); // We don't need our service anymore } @Override public void onRequestPurchaseResponse(RequestPurchase request, ResponseCode responseCode) { if (Consts.DEBUG) { Log.d(TAG, request.mProductId + ": " + responseCode); } if (responseCode == ResponseCode.RESULT_OK) { if (Consts.DEBUG) { Log.i(TAG, "purchase was successfully sent to server"); } } else if (responseCode == ResponseCode.RESULT_USER_CANCELED) { if (Consts.DEBUG) { Log.i(TAG, "user canceled purchase"); } } else { if (Consts.DEBUG) { Log.i(TAG, "purchase failed"); } } } @Override public void onRestoreTransactionsResponse(RestoreTransactions request, ResponseCode responseCode) { if (responseCode == ResponseCode.RESULT_OK) { if (Consts.DEBUG) { Log.d(TAG, "completed RestoreTransactions request"); } // Update the shared preferences so that we don't perform // a RestoreTransactions again. SharedPreferences prefs = context.getPreferences(Context.MODE_PRIVATE); SharedPreferences.Editor edit = prefs.edit(); edit.putBoolean(DB_INITIALIZED, true); edit.commit(); } else { if (Consts.DEBUG) { Log.d(TAG, "RestoreTransactions error: " + responseCode); } } } } private static class CatalogEntry { public String sku; public int nameId; public Managed managed; public CatalogEntry(String sku, int nameId, Managed managed) { this.sku = sku; this.nameId = nameId; this.managed = managed; } } /** An array of product list entries for the products that can be purchased. */ private static final CatalogEntry[] CATALOG = new CatalogEntry[] { new CatalogEntry( "donate_basic", R.string.donation, Managed.MANAGED), }; private String mItemName; private String mSku; private static boolean isBillingSupported; private void promptToDonate() { AlertDialog.Builder builder = new AlertDialog.Builder(context); builder .setMessage(R.string.donate_text) .setCancelable(false) .setPositiveButton(R.string.yes_i_d_like_to_donate, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int id) { FlurryAgent.onEvent("DonateYes"); String sku = "donate_basic"; Log.d(TAG, "buying: " + sku); dialog.cancel(); mBillingService.requestPurchase(sku, null); // We don't call close, because we want to leave the server // running a bit longer (to get the reply about this purchase) } }) .setNegativeButton(R.string.no_not_right_now, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int id) { FlurryAgent.onEvent("DonateNo"); close(); dialog.cancel(); } }); AlertDialog alert = builder.create(); alert.show(); } /** * Has the user donated? * * @return */ public static boolean isDonated(Context context) { // Even though this is an open source app, don't turn off this check and // release a version without // this donation reminder. The small amount of totally optional donations I // receive repays me for // the huge amount of time I have invested. -kevin return PreferenceManager.getDefaultSharedPreferences(context).getBoolean( "has_donated", false); } public static boolean canPromptToUpdate(Context context) { return context.getString(R.string.should_prompt_to_donate).equals("true") && isBillingSupported; } /** * Is it time to show the nag screen? * * @return */ private boolean isPromptToUpdate() { // Even though this is an open source app, don't turn off this check and // release a version without // this donation reminder. The small amount of totally optional donations I // receive repays me for // the huge amount of time I have invested. -kevin if (!canPromptToUpdate(context)) return false; SharedPreferences prefs = PreferenceManager .getDefaultSharedPreferences(context); long lastCheckDate = prefs.getLong("donate_check_date", -1); long now = System.currentTimeMillis(); if (lastCheckDate == -1) { // Don't nag the first time we launch prefs.edit().putLong("donate_check_date", now).commit(); return false; } long span = now - lastCheckDate; long checkInterval = 3 * 24 * 60 * 60 * 1000; // Only nag once every three // days if (span < checkInterval) return false; prefs.edit().putLong("donate_check_date", now).commit(); // Check to see if the market server says we already have donated Cursor c = mPurchaseDatabase.queryAllPurchasedItems(); int numPurchased = c.getCount(); c.close(); if (numPurchased > 0) { PreferenceManager.getDefaultSharedPreferences(context).edit() .putBoolean("has_donated", true).commit(); thanksForDonating(); return false; } return true; } private void thanksForDonating() { // Even though this is an open source app, don't turn off this check and // release a version without // this donation reminder. The small amount of totally optional donations I // receive repays me for // the huge amount of time I have invested. -kevin Toast.makeText(context, "Thank you for donating!", Toast.LENGTH_LONG) .show(); } public void perhapsSplash() { // Even though this is an open source app, don't turn off this check and // release a version without // this donation reminder. The small amount of totally optional donations I // receive repays me for // the huge amount of time I have invested. -kevin // Keep stats on # of emails sent if (isDonated(context)) { FlurryAgent.onEvent("DonateStart"); thanksForDonating(); close(); } else if (isPromptToUpdate()) promptToDonate(); else { FlurryAgent.onEvent("NonDonateStart"); close(); // Not bothering the user this time } } public void splash() { if (isDonated(context)) { thanksForDonating(); close(); } else promptToDonate(); } public Donate(Activity context) { this.context = context; mHandler = new Handler(); try { mPurchaseObserver = new DonatePurchaseObserver(mHandler); // If we've already donate no need to start the service if (!isDonated(context)) { mBillingService = new BillingService(); mBillingService.setContext(context); mPurchaseDatabase = new PurchaseDatabase(context); // Check if billing is supported. ResponseHandler.register(mPurchaseObserver); isBillingSupported = mBillingService.checkBillingSupported(); } } catch (Throwable t) { // Android 1.5 throws a VerifyError loading the library Log.e(TAG, "Skipping donate due to " + t); } } /** Release critical resources */ private void close() { ResponseHandler.unregister(mPurchaseObserver); if (mPurchaseDatabase != null) mPurchaseDatabase.close(); if (mBillingService != null) mBillingService.unbind(); } /** * If the database has not been initialized, we send a RESTORE_TRANSACTIONS * request to Android Market to get the list of purchased items for this user. * This happens if the application has just been installed or the user wiped * data. We do not want to do this on every startup, rather, we want to do * only when the database needs to be initialized. */ private void restoreDatabase() { SharedPreferences prefs = PreferenceManager .getDefaultSharedPreferences(context); boolean initialized = prefs.getBoolean(DB_INITIALIZED, false); if (!initialized) { mBillingService.restoreTransactions(); // Toast.makeText(context, R.string.restoring_transactions, // Toast.LENGTH_LONG).show(); } } }